mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-31 11:38:47 -05:00
Compare commits
446 Commits
v2.17.4
...
fix-chapte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4d99a118f | ||
|
|
5ca12eee19 | ||
|
|
ebdf377fc1 | ||
|
|
808d23561c | ||
|
|
a34813b3ab | ||
|
|
725192fbc0 | ||
|
|
2915c072b5 | ||
|
|
03a1d7da32 | ||
|
|
1be1ce6f87 | ||
|
|
21b27c432c | ||
|
|
cbe5e3db8a | ||
|
|
08b4d4d7a2 | ||
|
|
ac8324e595 | ||
|
|
a14c6a3a8b | ||
|
|
74b35ea9d1 | ||
|
|
78d8c83e6d | ||
|
|
bf795d3662 | ||
|
|
1fbd090441 | ||
|
|
70621e72e8 | ||
|
|
d30a09f503 | ||
|
|
39567c6c22 | ||
|
|
ed3af5bdcd | ||
|
|
9e54b4f7ca | ||
|
|
ec65376569 | ||
|
|
4e8cd6fba0 | ||
|
|
1a3d70d041 | ||
|
|
14e92435ec | ||
|
|
0ccb88904a | ||
|
|
4cc300d6e9 | ||
|
|
068ba84a8c | ||
|
|
36ef675556 | ||
|
|
0dd57a8912 | ||
|
|
ef45f844e5 | ||
|
|
9a261195b7 | ||
|
|
3d08a35aa0 | ||
|
|
a13143245b | ||
|
|
52bb28669a | ||
|
|
25ae6dd59a | ||
|
|
a37fe3c3d2 | ||
|
|
59bcbe0dfa | ||
|
|
b5e69630de | ||
|
|
0bba709124 | ||
|
|
e93bb5cb07 | ||
|
|
3f7af8acfb | ||
|
|
5e5a604d03 | ||
|
|
201e12ecc3 | ||
|
|
24d6e390f0 | ||
|
|
0cf7a6abec | ||
|
|
76ac0d001b | ||
|
|
00343a953b | ||
|
|
82ab95ab02 | ||
|
|
a1d8ebc01b | ||
|
|
eeaae5f934 | ||
|
|
4464276a6e | ||
|
|
3465790fe9 | ||
|
|
5fa4c5a2c3 | ||
|
|
13f353596b | ||
|
|
3d9100e5b8 | ||
|
|
b62309ead2 | ||
|
|
1fce94ad4a | ||
|
|
9abd6698ae | ||
|
|
88c10ad619 | ||
|
|
c62a6fbffd | ||
|
|
989388d3ed | ||
|
|
4cc97a22f6 | ||
|
|
8bd336a4ba | ||
|
|
437c8dd09c | ||
|
|
f82697cbbf | ||
|
|
74c87a0bbd | ||
|
|
35eb5bcfc0 | ||
|
|
0a29b549df | ||
|
|
a38a92b948 | ||
|
|
d245c93da4 | ||
|
|
bcf8f6b732 | ||
|
|
40e11db5e5 | ||
|
|
aebb3ff413 | ||
|
|
a58d486c44 | ||
|
|
4a76ba0226 | ||
|
|
7afff57b5e | ||
|
|
2e13c5bd50 | ||
|
|
344de941ff | ||
|
|
c3aad9486c | ||
|
|
5c0cd98cb3 | ||
|
|
8974c582fc | ||
|
|
5ee6005112 | ||
|
|
6a7469851d | ||
|
|
1d57daa9f9 | ||
|
|
caf2b664f1 | ||
|
|
b3b2bd7772 | ||
|
|
95864705dc | ||
|
|
0fbba3efbd | ||
|
|
575927c101 | ||
|
|
45aaaf9f0b | ||
|
|
51704f41aa | ||
|
|
e701a0a32e | ||
|
|
fbe186a925 | ||
|
|
6ed2b575b0 | ||
|
|
558173e086 | ||
|
|
23067e1818 | ||
|
|
9b4732c207 | ||
|
|
e096da1b4d | ||
|
|
a4d0f95ecc | ||
|
|
922a5039ce | ||
|
|
f258782e2e | ||
|
|
1ea1e60d4b | ||
|
|
7c4bcfb4f9 | ||
|
|
3eefe937d9 | ||
|
|
d4ba8b9d9f | ||
|
|
c735fea8ba | ||
|
|
9e3010681e | ||
|
|
c6f724edff | ||
|
|
358c3a15b5 | ||
|
|
32819860aa | ||
|
|
7dff571fd5 | ||
|
|
36dd96fd87 | ||
|
|
e6244b8676 | ||
|
|
9b561e4367 | ||
|
|
d25b46e9fa | ||
|
|
7a89836c3e | ||
|
|
a9dd67cf75 | ||
|
|
6f2384e4f2 | ||
|
|
254558f7a6 | ||
|
|
a4a7cddcff | ||
|
|
fc116ce1ed | ||
|
|
f77dd6b1ad | ||
|
|
647a722b06 | ||
|
|
6ec33f4bfa | ||
|
|
bb0cc1bb6f | ||
|
|
abb5bd3a2d | ||
|
|
79acc41d16 | ||
|
|
9fbf57bbef | ||
|
|
598a93d224 | ||
|
|
286185329d | ||
|
|
c3c846f82d | ||
|
|
66b90e0841 | ||
|
|
9b21812feb | ||
|
|
e9d8b62858 | ||
|
|
6d5aeaa42f | ||
|
|
3fd9721da6 | ||
|
|
63b2c6a3ea | ||
|
|
1506589ec8 | ||
|
|
035590236b | ||
|
|
eea446e217 | ||
|
|
63dc819728 | ||
|
|
ff537de132 | ||
|
|
56550157d1 | ||
|
|
28681d3783 | ||
|
|
24ce4a208d | ||
|
|
b816c0e7c4 | ||
|
|
a8b92819d1 | ||
|
|
54a4b09592 | ||
|
|
f13283b950 | ||
|
|
78994b3589 | ||
|
|
6745efc4d6 | ||
|
|
bdd8e5bb58 | ||
|
|
6c540ad789 | ||
|
|
64992b3308 | ||
|
|
ea9552e9a9 | ||
|
|
60add37ba0 | ||
|
|
6182764340 | ||
|
|
d8de61437c | ||
|
|
ca5c8a4d41 | ||
|
|
152683ff9c | ||
|
|
0ac92b6dc1 | ||
|
|
831f9ab9e7 | ||
|
|
3a33553aec | ||
|
|
94df14f0cb | ||
|
|
1d1bdb2f00 | ||
|
|
3aa6b358b3 | ||
|
|
6052bb9fda | ||
|
|
76b270ddf6 | ||
|
|
318e57170d | ||
|
|
5294335bca | ||
|
|
68af5933e5 | ||
|
|
bc2d7ff14d | ||
|
|
7d278ebc56 | ||
|
|
47247323cf | ||
|
|
77ad9c8a16 | ||
|
|
58ca26436d | ||
|
|
4a3254d338 | ||
|
|
ebaae98a12 | ||
|
|
4701b3ed0c | ||
|
|
4843be89e7 | ||
|
|
9a2fb49950 | ||
|
|
ecbcc8470b | ||
|
|
32b886a0c3 | ||
|
|
2463c62bbf | ||
|
|
d55faabb6d | ||
|
|
222ce6ca00 | ||
|
|
be5dc6d2ec | ||
|
|
804b446dae | ||
|
|
5897aee3b7 | ||
|
|
1e5e507eb0 | ||
|
|
760af51c5d | ||
|
|
24705ca06a | ||
|
|
56cba44154 | ||
|
|
9360165f6b | ||
|
|
adef6ede12 | ||
|
|
b8afcd1664 | ||
|
|
d8da793bca | ||
|
|
1856d68299 | ||
|
|
89247f1786 | ||
|
|
5995c52ab7 | ||
|
|
07264544ef | ||
|
|
6057930507 | ||
|
|
9bbb23b853 | ||
|
|
e865241258 | ||
|
|
1a67f57551 | ||
|
|
9b5bdc1fdb | ||
|
|
acda776e3e | ||
|
|
8c4a9280ab | ||
|
|
1812282946 | ||
|
|
64e9ac9d8f | ||
|
|
0da9a04d8e | ||
|
|
11178f58bd | ||
|
|
08b2d07f65 | ||
|
|
3c210170b2 | ||
|
|
03d35421b4 | ||
|
|
a176ba53e0 | ||
|
|
e34dff8f30 | ||
|
|
0881ab4bfb | ||
|
|
20c32efd62 | ||
|
|
e2b8127a5b | ||
|
|
90f32cefca | ||
|
|
ab2e661e22 | ||
|
|
a073aedca2 | ||
|
|
b440a22ec9 | ||
|
|
ec695e5f48 | ||
|
|
69ad0bf113 | ||
|
|
88f464398a | ||
|
|
6fce501389 | ||
|
|
559fab0d90 | ||
|
|
69c428802b | ||
|
|
6da631fa4f | ||
|
|
f83b081791 | ||
|
|
a6ce5fdd98 | ||
|
|
0a2e725bd3 | ||
|
|
c07c4a3341 | ||
|
|
422773e745 | ||
|
|
7a298aa6f5 | ||
|
|
41daf557aa | ||
|
|
de5bc63d88 | ||
|
|
5e2282ef76 | ||
|
|
c819afc53b | ||
|
|
37221a0446 | ||
|
|
0f20ed101e | ||
|
|
b0dbccd283 | ||
|
|
7001adb4dd | ||
|
|
9668b49df9 | ||
|
|
02ecf7ccfe | ||
|
|
05ff5f1956 | ||
|
|
1649fb40db | ||
|
|
052e0059ff | ||
|
|
5edd799b3e | ||
|
|
1632d8edee | ||
|
|
e6181196a7 | ||
|
|
bea9d6aff4 | ||
|
|
d410b13c9b | ||
|
|
8286aad7a4 | ||
|
|
ed5960825b | ||
|
|
7fd8178dde | ||
|
|
db17a5c88b | ||
|
|
2ec84edb5e | ||
|
|
0eed38b771 | ||
|
|
977bdbf0bb | ||
|
|
a1ec10bd0d | ||
|
|
57d742b862 | ||
|
|
108eaba022 | ||
|
|
ac159bea72 | ||
|
|
d5ce7b4939 | ||
|
|
e64302f1d4 | ||
|
|
fdbca4feb6 | ||
|
|
f366dfa909 | ||
|
|
5d1a17ffa8 | ||
|
|
0ed4ea9138 | ||
|
|
1e9470b840 | ||
|
|
726a9eaea5 | ||
|
|
6d52f88a96 | ||
|
|
7fae25a726 | ||
|
|
d8823c8b1c | ||
|
|
43d8d9b286 | ||
|
|
4a398f6113 | ||
|
|
69d1744496 | ||
|
|
0357dc90d4 | ||
|
|
6cd874dffc | ||
|
|
6467a92de6 | ||
|
|
63466ec48b | ||
|
|
de7296eaab | ||
|
|
c251f1899d | ||
|
|
f70f21455f | ||
|
|
a6fd0c95b2 | ||
|
|
d205c6f734 | ||
|
|
5e8678f1cc | ||
|
|
12c6f2e9a5 | ||
|
|
5cd14108f9 | ||
|
|
eb853d9f09 | ||
|
|
4787e7fdb5 | ||
|
|
dd0ebdf2d8 | ||
|
|
18dfbdd983 | ||
|
|
fe2ba083be | ||
|
|
de8b0abc3a | ||
|
|
08bbe1ba02 | ||
|
|
87bac1e33b | ||
|
|
e9eeab6fb5 | ||
|
|
235d05eff3 | ||
|
|
f9f8c6d751 | ||
|
|
e175a9c533 | ||
|
|
f9130a138e | ||
|
|
ed17dd9b51 | ||
|
|
0d8d0a650b | ||
|
|
eb505a0be7 | ||
|
|
f3918a47e1 | ||
|
|
c8a05920dd | ||
|
|
e7f7d1a573 | ||
|
|
5201625d38 | ||
|
|
8c4d0c503b | ||
|
|
d3bda898d4 | ||
|
|
86809dcc62 | ||
|
|
9fa00a1904 | ||
|
|
46247ecf78 | ||
|
|
0444829a9f | ||
|
|
754c121168 | ||
|
|
1c2ee09f18 | ||
|
|
ee310d967e | ||
|
|
25b7f005c6 | ||
|
|
777c59458d | ||
|
|
9785bc02ea | ||
|
|
6780ef9b37 | ||
|
|
88a0e75576 | ||
|
|
476933a144 | ||
|
|
2464aac2bf | ||
|
|
b6b786e3a6 | ||
|
|
bacb8aeac7 | ||
|
|
ba9277cc44 | ||
|
|
3cc5fae586 | ||
|
|
da7d9c10ad | ||
|
|
aa82439125 | ||
|
|
2e0156d9fa | ||
|
|
20e0172fa3 | ||
|
|
6928f6eeb6 | ||
|
|
4cdc2a8c28 | ||
|
|
e0c674d9a9 | ||
|
|
d7830f4bfc | ||
|
|
727310ab75 | ||
|
|
f46b5a533c | ||
|
|
f3e9cfbe45 | ||
|
|
4d8501c347 | ||
|
|
b4e8f16174 | ||
|
|
7073f17cca | ||
|
|
e1c41e4e58 | ||
|
|
13f73cc79d | ||
|
|
d811ec3806 | ||
|
|
e8505cb637 | ||
|
|
94fdd99ab5 | ||
|
|
331c7c011c | ||
|
|
5fa263023f | ||
|
|
7eb315a371 | ||
|
|
780c0dcb99 | ||
|
|
004210ee02 | ||
|
|
921880445a | ||
|
|
0099ae633a | ||
|
|
91d99deba1 | ||
|
|
e21cbc9ff4 | ||
|
|
600c1e4668 | ||
|
|
aea2951b89 | ||
|
|
71b943f434 | ||
|
|
4d2241769e | ||
|
|
ed0484a8e1 | ||
|
|
5302f3225b | ||
|
|
a94a7b7940 | ||
|
|
4318f64d60 | ||
|
|
26a6618e8f | ||
|
|
c242e9d3d6 | ||
|
|
4ecb22f70d | ||
|
|
547a49e95b | ||
|
|
b6875af148 | ||
|
|
c652b5bf74 | ||
|
|
eb0b92a547 | ||
|
|
b56bcbb802 | ||
|
|
3b8af95211 | ||
|
|
a3332f0478 | ||
|
|
46421d5f2c | ||
|
|
7db28d0e98 | ||
|
|
31d26929af | ||
|
|
086da5f6a1 | ||
|
|
09421a44e2 | ||
|
|
fde51da479 | ||
|
|
f3536dc3a3 | ||
|
|
a0c93e5dec | ||
|
|
63aa6aa950 | ||
|
|
680099cab4 | ||
|
|
66f3f3eddf | ||
|
|
a400c149a6 | ||
|
|
244b5ab36d | ||
|
|
f26747627e | ||
|
|
f57a07c483 | ||
|
|
080b879d8a | ||
|
|
63b3f22504 | ||
|
|
91f17efd5f | ||
|
|
858d697d0f | ||
|
|
ba55413e63 | ||
|
|
6cef1e3f12 | ||
|
|
b39268ccb0 | ||
|
|
de8a9304d2 | ||
|
|
f8fbd3ac8c | ||
|
|
369c05936b | ||
|
|
837a180dc1 | ||
|
|
302b651e7b | ||
|
|
4c68ad46f4 | ||
|
|
e50bd93958 | ||
|
|
d576625cb7 | ||
|
|
ca2327aba3 | ||
|
|
9bd1f9e3d5 | ||
|
|
c4610e6102 | ||
|
|
329bbea043 | ||
|
|
e616b53877 | ||
|
|
eab86f90a8 | ||
|
|
f97389cb2b | ||
|
|
c5c3aab130 | ||
|
|
4610e58337 | ||
|
|
190a1000d9 | ||
|
|
455b96d1ab | ||
|
|
8aaf62f243 | ||
|
|
e6d754113e | ||
|
|
5f72e30e63 | ||
|
|
57906540fe | ||
|
|
726adbb3bf | ||
|
|
f7b7b85673 | ||
|
|
5646466aa3 | ||
|
|
b38ce41731 | ||
|
|
a8ab8badd5 | ||
|
|
61729881cb | ||
|
|
5eca43082e | ||
|
|
6fa11934be | ||
|
|
ff7edc32a1 | ||
|
|
9b8e059efe | ||
|
|
7486d6345d | ||
|
|
835490a9fc | ||
|
|
3b4a5b8785 | ||
|
|
9a1c773b7a | ||
|
|
27c9381e1d | ||
|
|
0812e189f7 | ||
|
|
588def6d33 | ||
|
|
a0b3960ee4 | ||
|
|
e55db0afdc | ||
|
|
ae9efe6359 |
42
.github/workflows/close_blank_issues.yaml
vendored
Normal file
42
.github/workflows/close_blank_issues.yaml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Close Issues not using a template
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
close_issue:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check issue headings
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const issueBody = context.payload.issue.body || "";
|
||||
|
||||
// Match Markdown headings (e.g., # Heading, ## Heading)
|
||||
const headingRegex = /^(#{1,6})\s.+/gm;
|
||||
const headings = [...issueBody.matchAll(headingRegex)];
|
||||
|
||||
if (headings.length < 3) {
|
||||
// Post a comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.issue.number,
|
||||
body: "Thank you for opening an issue! To help us review your request efficiently, please use one of the provided issue templates. If you're seeking information or have a general question, consider opening a Discussion or joining the conversation on our Discord. Thanks!"
|
||||
});
|
||||
|
||||
// Close the issue
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.issue.number,
|
||||
state: "closed"
|
||||
});
|
||||
}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@
|
||||
/podcasts/
|
||||
/media/
|
||||
/metadata/
|
||||
/plugins/
|
||||
/client/.nuxt/
|
||||
/client/dist/
|
||||
/dist/
|
||||
|
||||
@@ -46,5 +46,10 @@ RUN apk del make python3 g++
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
ENV PORT=80
|
||||
ENV CONFIG_PATH="/config"
|
||||
ENV METADATA_PATH="/metadata"
|
||||
ENV SOURCE="docker"
|
||||
|
||||
ENTRYPOINT ["tini", "--"]
|
||||
CMD ["node", "index.js"]
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
@import './absicons.css';
|
||||
|
||||
:root {
|
||||
--bookshelf-texture-img: url(/textures/wood_default.jpg);
|
||||
--bookshelf-texture-img: url(~static/textures/wood_default.jpg);
|
||||
--bookshelf-divider-bg: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%);
|
||||
}
|
||||
|
||||
@@ -92,11 +92,10 @@
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
input[type=number] {
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
|
||||
.tracksTable {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
@@ -177,6 +176,10 @@ input[type=number] {
|
||||
box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166;
|
||||
}
|
||||
|
||||
.box-shadow-progressbar {
|
||||
box-shadow: 0px -1px 4px rgb(62, 50, 2, 0.5);
|
||||
}
|
||||
|
||||
.shadow-height {
|
||||
height: calc(100% - 4px);
|
||||
}
|
||||
@@ -204,7 +207,6 @@ Bookshelf Label
|
||||
color: #fce3a6;
|
||||
}
|
||||
|
||||
|
||||
.cover-bg {
|
||||
width: calc(100% + 40px);
|
||||
height: calc(100% + 40px);
|
||||
@@ -247,4 +249,4 @@ Bookshelf Label
|
||||
|
||||
.abs-btn:disabled::before {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,4 +52,17 @@
|
||||
text-indent: 0px !important;
|
||||
text-align: start !important;
|
||||
text-align-last: start !important;
|
||||
}
|
||||
}
|
||||
|
||||
.default-style.less-spacing p {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
.default-style.less-spacing ul {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
.default-style.less-spacing ol {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -446,7 +446,7 @@ trix-editor .attachment__metadata .attachment__size {
|
||||
}
|
||||
|
||||
.trix-content {
|
||||
line-height: 1.5;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.trix-content * {
|
||||
@@ -455,6 +455,13 @@ trix-editor .attachment__metadata .attachment__size {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.trix-content p {
|
||||
box-sizing: border-box;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.trix-content h1 {
|
||||
font-size: 1.2em;
|
||||
line-height: 1.2;
|
||||
@@ -560,4 +567,4 @@ trix-editor .attachment__metadata .attachment__size {
|
||||
.trix-content .attachment-gallery.attachment-gallery--4 .attachment {
|
||||
flex-basis: 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="w-full h-16 bg-primary relative">
|
||||
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-60">
|
||||
<div id="appbar" role="toolbar" aria-label="Appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-60">
|
||||
<div class="flex h-full items-center">
|
||||
<nuxt-link to="/">
|
||||
<img src="~static/icon.svg" :alt="$strings.ButtonHome" class="w-8 min-w-8 h-8 mr-2 sm:w-10 sm:min-w-10 sm:h-10 sm:mr-4" />
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24e">
|
||||
<template v-for="(shelf, index) in supportedShelves">
|
||||
<widgets-item-slider :shelf-id="shelf.id" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" :type="shelf.type" class="bookshelf-row pl-8e my-6e" @selectEntity="(payload) => selectEntity(payload, index)">
|
||||
<p class="font-semibold text-gray-100">{{ $strings[shelf.labelStringKey] }}</p>
|
||||
<h2 class="font-semibold text-gray-100">{{ $strings[shelf.labelStringKey] }}</h2>
|
||||
</widgets-item-slider>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -37,18 +37,18 @@
|
||||
<div class="relative">
|
||||
<div class="relative text-center categoryPlacard transform z-30 top-0 left-4e md:left-8e w-44e rounded-md">
|
||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em 0.5em` }">
|
||||
<p :style="{ fontSize: 0.9 + 'em' }">{{ $strings[shelf.labelStringKey] }}</p>
|
||||
<h2 :style="{ fontSize: 0.9 + 'em' }">{{ $strings[shelf.labelStringKey] }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bookshelfDividerCategorized h-6e w-full absolute top-0 left-0 right-0 z-20"></div>
|
||||
</div>
|
||||
<div v-show="canScrollLeft && !isScrolling" class="hidden sm:flex absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollLeft">
|
||||
<button v-show="canScrollLeft && !isScrolling" :aria-label="$strings.ButtonScrollLeft" class="hidden sm:flex absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollLeft">
|
||||
<span class="material-symbols text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_left</span>
|
||||
</div>
|
||||
<div v-show="canScrollRight && !isScrolling" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollRight">
|
||||
</button>
|
||||
<button v-show="canScrollRight && !isScrolling" :aria-label="$strings.ButtonScrollRight" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollRight">
|
||||
<span class="material-symbols text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_right</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -99,6 +99,7 @@ export default {
|
||||
this.$store.commit('showEditModal', libraryItem)
|
||||
},
|
||||
editEpisode({ libraryItem, episode }) {
|
||||
this.$store.commit('setEpisodeTableEpisodeIds', [episode.id])
|
||||
this.$store.commit('setSelectedLibraryItem', libraryItem)
|
||||
this.$store.commit('globals/setSelectedEpisode', episode)
|
||||
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||
|
||||
@@ -42,8 +42,11 @@
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<p class="text-sm">{{ $strings.ButtonAdd }}</p>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<p class="text-sm">{{ $strings.ButtonDownloadQueue }}</p>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-40 flex items-center justify-end md:justify-start px-2 md:px-8">
|
||||
<div id="toolbar" role="toolbar" aria-label="Library Toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-40 flex items-center justify-end md:justify-start px-2 md:px-8">
|
||||
<!-- Series books page -->
|
||||
<template v-if="selectedSeries">
|
||||
<p class="pl-2 text-base md:text-lg">
|
||||
@@ -265,6 +268,9 @@ export default {
|
||||
isPodcastLatestPage() {
|
||||
return this.$route.name === 'library-library-podcast-latest'
|
||||
},
|
||||
isPodcastDownloadQueuePage() {
|
||||
return this.$route.name === 'library-library-podcast-download-queue'
|
||||
},
|
||||
isAuthorsPage() {
|
||||
return this.page === 'authors'
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="w-44 fixed left-0 top-16 bg-bg bg-opacity-100 md:bg-opacity-70 shadow-lg border-r border-white border-opacity-5 py-3 transform transition-transform mb-12 overflow-y-auto" :class="wrapperClass + ' ' + (streamLibraryItem ? 'h-[calc(100%-270px)]' : 'h-[calc(100%-110px)]')" v-click-outside="clickOutside">
|
||||
<div role="toolbar" aria-orientation="vertical" aria-label="Config Sidebar">
|
||||
<div role="navigation" aria-label="Config Navigation" class="w-44 fixed left-0 top-16 bg-bg bg-opacity-100 md:bg-opacity-70 shadow-lg border-r border-white border-opacity-5 py-3 transform transition-transform mb-12 overflow-y-auto" :class="wrapperClass + ' ' + (streamLibraryItem ? 'h-[calc(100%-270px)]' : 'h-[calc(100%-110px)]')" v-click-outside="clickOutside">
|
||||
<div v-show="isMobilePortrait" class="flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer">
|
||||
<span class="material-symbols text-2xl">arrow_back</span>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
<div id="bookshelf" ref="bookshelf" class="w-full overflow-y-auto" :style="{ fontSize: sizeMultiplier + 'rem' }">
|
||||
<template v-for="shelf in totalShelves">
|
||||
<div :key="shelf" :id="`shelf-${shelf - 1}`" class="w-full px-4e sm:px-8e relative" :class="{ bookshelfRow: !isAlternativeBookshelfView }" :style="{ height: shelfHeight + 'px' }">
|
||||
<!-- Card skeletons -->
|
||||
<template v-for="entityIndex in entitiesInShelf(shelf)">
|
||||
<div :key="entityIndex" class="w-full h-full absolute rounded z-5 top-0 left-0 bg-primary box-shadow-book" :style="{ transform: entityTransform(entityIndex), width: cardWidth + 'px', height: coverHeight + 'px' }" />
|
||||
</template>
|
||||
<div v-if="!isAlternativeBookshelfView" class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-20 h-6e" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -15,6 +19,14 @@
|
||||
</div>
|
||||
<div v-else-if="!totalShelves && initialized" class="w-full py-16">
|
||||
<p class="text-xl text-center">{{ emptyMessage }}</p>
|
||||
<div v-if="entityName === 'collections' || entityName === 'playlists'" class="flex justify-center mt-4">
|
||||
{{ emptyMessageHelp }}
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides/collections" target="_blank" class="inline-flex">
|
||||
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<!-- Clear filter only available on Library bookshelf -->
|
||||
<div v-if="entityName === 'items'" class="flex justify-center mt-2">
|
||||
<ui-btn v-if="hasFilter" color="primary" @click="clearFilter">{{ $strings.ButtonClearFilter }}</ui-btn>
|
||||
@@ -65,7 +77,13 @@ export default {
|
||||
tempIsScanning: false,
|
||||
cardWidth: 0,
|
||||
cardHeight: 0,
|
||||
resizeObserver: null
|
||||
coverHeight: 0,
|
||||
resizeObserver: null,
|
||||
lastScrollTop: 0,
|
||||
lastTimestamp: 0,
|
||||
postScrollTimeout: null,
|
||||
currFirstEntityIndex: -1,
|
||||
currLastEntityIndex: -1
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -99,6 +117,11 @@ export default {
|
||||
}
|
||||
return this.$strings.MessageNoResults
|
||||
},
|
||||
emptyMessageHelp() {
|
||||
if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollectionsHelp
|
||||
if (this.page === 'playlists') return this.$strings.MessageNoUserPlaylistsHelp
|
||||
return ''
|
||||
},
|
||||
entityName() {
|
||||
if (!this.page) return 'items'
|
||||
return this.page
|
||||
@@ -171,9 +194,6 @@ export default {
|
||||
bookWidth() {
|
||||
return this.cardWidth
|
||||
},
|
||||
bookHeight() {
|
||||
return this.cardHeight
|
||||
},
|
||||
shelfPadding() {
|
||||
if (this.bookshelfWidth < 640) return 32 * this.sizeMultiplier
|
||||
return 64 * this.sizeMultiplier
|
||||
@@ -184,9 +204,6 @@ export default {
|
||||
entityWidth() {
|
||||
return this.cardWidth
|
||||
},
|
||||
entityHeight() {
|
||||
return this.cardHeight
|
||||
},
|
||||
shelfPaddingHeight() {
|
||||
return 16
|
||||
},
|
||||
@@ -354,50 +371,53 @@ export default {
|
||||
}
|
||||
},
|
||||
loadPage(page) {
|
||||
this.pagesLoaded[page] = true
|
||||
this.fetchEntites(page)
|
||||
if (!this.pagesLoaded[page]) this.pagesLoaded[page] = this.fetchEntites(page)
|
||||
return this.pagesLoaded[page]
|
||||
},
|
||||
showHideBookPlaceholder(index, show) {
|
||||
var el = document.getElementById(`book-${index}-placeholder`)
|
||||
if (el) el.style.display = show ? 'flex' : 'none'
|
||||
},
|
||||
mountEntites(fromIndex, toIndex) {
|
||||
mountEntities(fromIndex, toIndex) {
|
||||
for (let i = fromIndex; i < toIndex; i++) {
|
||||
if (!this.entityIndexesMounted.includes(i)) {
|
||||
this.cardsHelpers.mountEntityCard(i)
|
||||
}
|
||||
}
|
||||
},
|
||||
handleScroll(scrollTop) {
|
||||
this.currScrollTop = scrollTop
|
||||
var firstShelfIndex = Math.floor(scrollTop / this.shelfHeight)
|
||||
var lastShelfIndex = Math.ceil((scrollTop + this.bookshelfHeight) / this.shelfHeight)
|
||||
lastShelfIndex = Math.min(this.totalShelves - 1, lastShelfIndex)
|
||||
|
||||
var firstBookIndex = firstShelfIndex * this.entitiesPerShelf
|
||||
var lastBookIndex = lastShelfIndex * this.entitiesPerShelf + this.entitiesPerShelf
|
||||
lastBookIndex = Math.min(this.totalEntities, lastBookIndex)
|
||||
|
||||
var firstBookPage = Math.floor(firstBookIndex / this.booksPerFetch)
|
||||
var lastBookPage = Math.floor(lastBookIndex / this.booksPerFetch)
|
||||
if (!this.pagesLoaded[firstBookPage]) {
|
||||
// console.log('Must load next batch', firstBookPage, 'book index', firstBookIndex)
|
||||
this.loadPage(firstBookPage)
|
||||
}
|
||||
if (!this.pagesLoaded[lastBookPage]) {
|
||||
// console.log('Must load last next batch', lastBookPage, 'book index', lastBookIndex)
|
||||
this.loadPage(lastBookPage)
|
||||
}
|
||||
|
||||
getVisibleIndices(scrollTop) {
|
||||
const firstShelfIndex = Math.floor(scrollTop / this.shelfHeight)
|
||||
const lastShelfIndex = Math.min(Math.ceil((scrollTop + this.bookshelfHeight) / this.shelfHeight), this.totalShelves - 1)
|
||||
const firstEntityIndex = firstShelfIndex * this.entitiesPerShelf
|
||||
const lastEntityIndex = Math.min(lastShelfIndex * this.entitiesPerShelf + this.entitiesPerShelf, this.totalEntities)
|
||||
return { firstEntityIndex, lastEntityIndex }
|
||||
},
|
||||
postScroll() {
|
||||
const { firstEntityIndex, lastEntityIndex } = this.getVisibleIndices(this.currScrollTop)
|
||||
this.entityIndexesMounted = this.entityIndexesMounted.filter((_index) => {
|
||||
if (_index < firstBookIndex || _index >= lastBookIndex) {
|
||||
var el = document.getElementById(`book-card-${_index}`)
|
||||
if (el) el.remove()
|
||||
if (_index < firstEntityIndex || _index >= lastEntityIndex) {
|
||||
var el = this.entityComponentRefs[_index]
|
||||
if (el && el.$el) el.$el.remove()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
this.mountEntites(firstBookIndex, lastBookIndex)
|
||||
},
|
||||
handleScroll(scrollTop) {
|
||||
this.currScrollTop = scrollTop
|
||||
const { firstEntityIndex, lastEntityIndex } = this.getVisibleIndices(scrollTop)
|
||||
if (firstEntityIndex === this.currFirstEntityIndex && lastEntityIndex === this.currLastEntityIndex) return
|
||||
this.currFirstEntityIndex = firstEntityIndex
|
||||
this.currLastEntityIndex = lastEntityIndex
|
||||
|
||||
clearTimeout(this.postScrollTimeout)
|
||||
const firstPage = Math.floor(firstEntityIndex / this.booksPerFetch)
|
||||
const lastPage = Math.floor(lastEntityIndex / this.booksPerFetch)
|
||||
Promise.all([this.loadPage(firstPage), this.loadPage(lastPage)])
|
||||
.then(() => this.mountEntities(firstEntityIndex, lastEntityIndex))
|
||||
.catch((error) => console.error('Failed to load page', error))
|
||||
|
||||
this.postScrollTimeout = setTimeout(this.postScroll, 500)
|
||||
},
|
||||
async resetEntities() {
|
||||
if (this.isFetchingEntities) {
|
||||
@@ -405,8 +425,6 @@ export default {
|
||||
return
|
||||
}
|
||||
this.destroyEntityComponents()
|
||||
this.entityIndexesMounted = []
|
||||
this.entityComponentRefs = {}
|
||||
this.pagesLoaded = {}
|
||||
this.entities = []
|
||||
this.totalShelves = 0
|
||||
@@ -416,40 +434,21 @@ export default {
|
||||
this.initialized = false
|
||||
|
||||
this.initSizeData()
|
||||
this.pagesLoaded[0] = true
|
||||
await this.fetchEntites(0)
|
||||
await this.loadPage(0)
|
||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
||||
this.mountEntites(0, lastBookIndex)
|
||||
this.mountEntities(0, lastBookIndex)
|
||||
},
|
||||
remountEntities() {
|
||||
for (const key in this.entityComponentRefs) {
|
||||
if (this.entityComponentRefs[key]) {
|
||||
this.entityComponentRefs[key].destroy()
|
||||
}
|
||||
}
|
||||
this.entityComponentRefs = {}
|
||||
this.entityIndexesMounted.forEach((i) => {
|
||||
this.cardsHelpers.mountEntityCard(i)
|
||||
})
|
||||
},
|
||||
rebuild() {
|
||||
async rebuild() {
|
||||
this.initSizeData()
|
||||
|
||||
var lastBookIndex = Math.min(this.totalEntities, this.booksPerFetch)
|
||||
this.entityIndexesMounted = []
|
||||
for (let i = 0; i < lastBookIndex; i++) {
|
||||
this.entityIndexesMounted.push(i)
|
||||
if (!this.entities[i]) {
|
||||
const page = Math.floor(i / this.booksPerFetch)
|
||||
this.loadPage(page)
|
||||
}
|
||||
}
|
||||
this.destroyEntityComponents()
|
||||
await this.loadPage(0)
|
||||
var bookshelfEl = document.getElementById('bookshelf')
|
||||
if (bookshelfEl) {
|
||||
bookshelfEl.scrollTop = 0
|
||||
}
|
||||
|
||||
this.$nextTick(this.remountEntities)
|
||||
this.mountEntities(0, lastBookIndex)
|
||||
},
|
||||
buildSearchParams() {
|
||||
if (this.page === 'search' || this.page === 'collections') {
|
||||
@@ -513,12 +512,29 @@ export default {
|
||||
if (wasUpdated) {
|
||||
this.resetEntities()
|
||||
} else if (settings.bookshelfCoverSize !== this.currentBookWidth) {
|
||||
this.executeRebuild()
|
||||
this.rebuild()
|
||||
}
|
||||
},
|
||||
getScrollRate() {
|
||||
const currentTimestamp = Date.now()
|
||||
const timeDelta = currentTimestamp - this.lastTimestamp
|
||||
const scrollDelta = this.currScrollTop - this.lastScrollTop
|
||||
const scrollRate = Math.abs(scrollDelta) / (timeDelta || 1)
|
||||
this.lastScrollTop = this.currScrollTop
|
||||
this.lastTimestamp = currentTimestamp
|
||||
return scrollRate
|
||||
},
|
||||
scroll(e) {
|
||||
if (!e || !e.target) return
|
||||
var { scrollTop } = e.target
|
||||
clearTimeout(this.scrollTimeout)
|
||||
const { scrollTop } = e.target
|
||||
const scrollRate = this.getScrollRate()
|
||||
if (scrollRate > 5) {
|
||||
this.scrollTimeout = setTimeout(() => {
|
||||
this.handleScroll(scrollTop)
|
||||
}, 25)
|
||||
return
|
||||
}
|
||||
this.handleScroll(scrollTop)
|
||||
},
|
||||
libraryItemAdded(libraryItem) {
|
||||
@@ -667,13 +683,14 @@ export default {
|
||||
},
|
||||
updatePagesLoaded() {
|
||||
let numPages = Math.ceil(this.totalEntities / this.booksPerFetch)
|
||||
this.pagesLoaded = {}
|
||||
for (let page = 0; page < numPages; page++) {
|
||||
let numEntities = Math.min(this.totalEntities - page * this.booksPerFetch, this.booksPerFetch)
|
||||
this.pagesLoaded[page] = true
|
||||
this.pagesLoaded[page] = Promise.resolve()
|
||||
for (let i = 0; i < numEntities; i++) {
|
||||
const index = page * this.booksPerFetch + i
|
||||
if (!this.entities[index]) {
|
||||
this.pagesLoaded[page] = false
|
||||
if (this.pagesLoaded[page]) delete this.pagesLoaded[page]
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -688,7 +705,6 @@ export default {
|
||||
var entitiesPerShelfBefore = this.entitiesPerShelf
|
||||
|
||||
var { clientHeight, clientWidth } = bookshelf
|
||||
// console.log('Init bookshelf width', clientWidth, 'window width', window.innerWidth)
|
||||
this.mountWindowWidth = window.innerWidth
|
||||
this.bookshelfHeight = clientHeight
|
||||
this.bookshelfWidth = clientWidth
|
||||
@@ -713,10 +729,9 @@ export default {
|
||||
this.initSizeData(bookshelf)
|
||||
this.checkUpdateSearchParams()
|
||||
|
||||
this.pagesLoaded[0] = true
|
||||
await this.fetchEntites(0)
|
||||
await this.loadPage(0)
|
||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
||||
this.mountEntites(0, lastBookIndex)
|
||||
this.mountEntities(0, lastBookIndex)
|
||||
|
||||
// Set last scroll position for this bookshelf page
|
||||
if (this.$store.state.lastBookshelfScrollData[this.page] && window.bookshelf) {
|
||||
@@ -747,7 +762,7 @@ export default {
|
||||
var bookshelf = document.getElementById('bookshelf')
|
||||
if (bookshelf) {
|
||||
this.init(bookshelf)
|
||||
bookshelf.addEventListener('scroll', this.scroll)
|
||||
bookshelf.addEventListener('scroll', this.scroll, { passive: true })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -810,10 +825,14 @@ export default {
|
||||
},
|
||||
destroyEntityComponents() {
|
||||
for (const key in this.entityComponentRefs) {
|
||||
if (this.entityComponentRefs[key] && this.entityComponentRefs[key].destroy) {
|
||||
this.entityComponentRefs[key].destroy()
|
||||
const ref = this.entityComponentRefs[key]
|
||||
if (ref && ref.destroy) {
|
||||
if (ref.$el) ref.$el.remove()
|
||||
ref.destroy()
|
||||
}
|
||||
}
|
||||
this.entityComponentRefs = {}
|
||||
this.entityIndexesMounted = []
|
||||
},
|
||||
scan() {
|
||||
this.tempIsScanning = true
|
||||
@@ -826,6 +845,14 @@ export default {
|
||||
.finally(() => {
|
||||
this.tempIsScanning = false
|
||||
})
|
||||
},
|
||||
entitiesInShelf(shelf) {
|
||||
return shelf == this.totalShelves ? this.totalEntities % this.entitiesPerShelf || this.entitiesPerShelf : this.entitiesPerShelf
|
||||
},
|
||||
entityTransform(entityIndex) {
|
||||
const shelfOffsetY = this.shelfPaddingHeight * this.sizeMultiplier
|
||||
const shelfOffsetX = (entityIndex - 1) * this.totalEntityCardWidth + this.bookshelfMarginLeft
|
||||
return `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)`
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
@showPlayerQueueItems="showPlayerQueueItemsModal = true"
|
||||
/>
|
||||
|
||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :library-item-id="libraryItemId" @select="selectBookmark" />
|
||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :playback-rate="currentPlaybackRate" :library-item-id="libraryItemId" @select="selectBookmark" />
|
||||
|
||||
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-type="sleepTimerType" :remaining="sleepTimerRemaining" :has-chapters="!!chapters.length" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
|
||||
|
||||
@@ -85,7 +85,8 @@ export default {
|
||||
displayTitle: null,
|
||||
currentPlaybackRate: 1,
|
||||
syncFailedToast: null,
|
||||
coverAspectRatio: 1
|
||||
coverAspectRatio: 1,
|
||||
lastChapterId: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -236,12 +237,16 @@ export default {
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
checkChapterEnd(time) {
|
||||
checkChapterEnd() {
|
||||
if (!this.currentChapter) return
|
||||
const chapterEndTime = this.currentChapter.end
|
||||
const tolerance = 0.75
|
||||
if (time >= chapterEndTime - tolerance) {
|
||||
this.sleepTimerEnd()
|
||||
|
||||
// Track chapter transitions by comparing current chapter with last chapter
|
||||
if (this.lastChapterId !== this.currentChapter.id) {
|
||||
// Chapter changed - if we had a previous chapter, this means we crossed a boundary
|
||||
if (this.lastChapterId) {
|
||||
this.sleepTimerEnd()
|
||||
}
|
||||
this.lastChapterId = this.currentChapter.id
|
||||
}
|
||||
},
|
||||
sleepTimerEnd() {
|
||||
@@ -301,7 +306,7 @@ export default {
|
||||
}
|
||||
|
||||
if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER && this.sleepTimerSet) {
|
||||
this.checkChapterEnd(time)
|
||||
this.checkChapterEnd()
|
||||
}
|
||||
},
|
||||
setDuration(duration) {
|
||||
@@ -374,19 +379,28 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API
|
||||
if ('mediaSession' in navigator) {
|
||||
var coverImageSrc = this.$store.getters['globals/getLibraryItemCoverSrc'](this.streamLibraryItem, '/Logo.png', true)
|
||||
const artwork = [
|
||||
{
|
||||
src: coverImageSrc
|
||||
}
|
||||
]
|
||||
const chapterInfo = []
|
||||
if (this.chapters.length) {
|
||||
this.chapters.forEach((chapter) => {
|
||||
chapterInfo.push({
|
||||
title: chapter.title,
|
||||
startTime: chapter.start
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: this.title,
|
||||
artist: this.playerHandler.displayAuthor || this.mediaMetadata.authorName || 'Unknown',
|
||||
album: this.mediaMetadata.seriesName || '',
|
||||
artwork
|
||||
artwork: [
|
||||
{
|
||||
src: this.$store.getters['globals/getLibraryItemCoverSrc'](this.streamLibraryItem, '/Logo.png', true)
|
||||
}
|
||||
],
|
||||
chapterInfo
|
||||
})
|
||||
console.log('Set media session metadata', navigator.mediaSession.metadata)
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="w-20 bg-bg h-full fixed left-0 box-shadow-side z-50" style="min-width: 80px" :style="{ top: offsetTop + 'px' }">
|
||||
<div role="toolbar" aria-orientation="vertical" aria-label="Library Sidebar" class="w-20 bg-bg h-full fixed left-0 box-shadow-side z-50" style="min-width: 80px" :style="{ top: offsetTop + 'px' }">
|
||||
<!-- ugly little workaround to cover up the shadow overlapping the bookshelf toolbar -->
|
||||
<div v-if="isShowingBookshelfToolbar" class="absolute top-0 -right-4 w-4 bg-bg h-10 pointer-events-none" />
|
||||
|
||||
<div id="siderail-buttons-container" :class="{ 'player-open': streamLibraryItem }" class="w-full overflow-y-auto overflow-x-hidden">
|
||||
<div id="siderail-buttons-container" role="navigation" aria-label="Library Navigation" :class="{ 'player-open': streamLibraryItem }" class="w-full overflow-y-auto overflow-x-hidden">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="homePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
|
||||
@@ -68,6 +68,9 @@ export default {
|
||||
cardHeight() {
|
||||
return this.height * this.sizeMultiplier
|
||||
},
|
||||
coverHeight() {
|
||||
return this.cardHeight
|
||||
},
|
||||
userToken() {
|
||||
return this.store.getters['user/getToken']
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full max-h-12 overflow-hidden">
|
||||
<p class="text-gray-500 text-xs">{{ book.description }}</p>
|
||||
<p class="text-gray-500 text-xs">{{ book.descriptionPlain }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="px-4 flex-grow">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div ref="card" :id="`book-card-${index}`" :style="{ minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }" class="absolute rounded-sm z-10 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div ref="card" :id="`book-card-${index}`" tabindex="0" :style="{ minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }" class="absolute rounded-sm z-10 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div :id="`cover-area-${index}`" class="relative w-full top-0 left-0 rounded overflow-hidden z-10 bg-primary box-shadow-book" :style="{ height: coverHeight + 'px ' }">
|
||||
<!-- When cover image does not fill -->
|
||||
<div cy-id="coverBg" v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
||||
@@ -14,32 +14,25 @@
|
||||
</div>
|
||||
|
||||
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
||||
<div cy-id="titleImageNotReady" v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: 0.5 + 'em' }">
|
||||
<div cy-id="titleImageNotReady" v-show="libraryItem && !imageReady" aria-hidden="true" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: 0.5 + 'em' }">
|
||||
<p :style="{ fontSize: 0.8 + 'em' }" class="text-gray-300 text-center">{{ title }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Cover Image -->
|
||||
<img cy-id="coverImage" v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="relative w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
||||
<img cy-id="coverImage" v-if="libraryItem" :alt="`${displayTitle}, ${$strings.LabelCover}`" ref="cover" aria-hidden="true" :src="bookCoverSrc" class="relative w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
||||
|
||||
<!-- Placeholder Cover Title & Author -->
|
||||
<div cy-id="placeholderTitle" v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'em' }">
|
||||
<div>
|
||||
<p cy-id="placeholderTitleText" class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'em' }">{{ titleCleaned }}</p>
|
||||
<p cy-id="placeholderTitleText" aria-hidden="true" class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'em' }">{{ titleCleaned }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div cy-id="placeholderAuthor" v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'em', bottom: authorBottom + 'em' }">
|
||||
<p cy-id="placeholderAuthorText" class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'em' }">{{ authorCleaned }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }" style="background-color: #78350f">
|
||||
<p :style="{ fontSize: 0.8 + 'em' }">#{{ seriesSequenceList }}</p>
|
||||
</div>
|
||||
<div v-else-if="booksInSeries" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }" style="background-color: #cd9d49dd">
|
||||
<p :style="{ fontSize: 0.8 + 'em' }">{{ booksInSeries }}</p>
|
||||
<p cy-id="placeholderAuthorText" aria-hidden="true" class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'em' }">{{ authorCleaned }}</p>
|
||||
</div>
|
||||
|
||||
<!-- No progress shown for podcasts (unless showing podcast episode) -->
|
||||
<div cy-id="progressBar" v-if="!isPodcast || episodeProgress" class="absolute bottom-0 left-0 h-1e shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * userProgressPercent + 'px' }"></div>
|
||||
<div cy-id="progressBar" v-if="!isPodcast || episodeProgress" class="absolute bottom-0 left-0 h-1e max-w-full z-20 rounded-b box-shadow-progressbar" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * userProgressPercent + 'px' }"></div>
|
||||
|
||||
<!-- Overlay is not shown if collapsing series in library -->
|
||||
<div cy-id="overlay" v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen) && !processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded md:block" :class="overlayWrapperClasslist">
|
||||
@@ -93,11 +86,11 @@
|
||||
|
||||
<!-- rss feed icon -->
|
||||
<div cy-id="rssFeed" v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" :style="{ padding: 0.375 + 'em' }">
|
||||
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">rss_feed</span>
|
||||
<span class="material-symbols" aria-hidden="true" :style="{ fontSize: 1.5 + 'em' }">rss_feed</span>
|
||||
</div>
|
||||
<!-- media item shared icon -->
|
||||
<div cy-id="mediaItemShare" v-if="mediaItemShare && !isSelectionMode && !isHovering" class="absolute text-success left-0 z-10" :style="{ padding: 0.375 + 'em', top: rssFeed ? '2em' : '0px' }">
|
||||
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">public</span>
|
||||
<span class="material-symbols" aria-hidden="true" :style="{ fontSize: 1.5 + 'em' }">public</span>
|
||||
</div>
|
||||
|
||||
<!-- Series sequence -->
|
||||
@@ -114,7 +107,7 @@
|
||||
|
||||
<!-- Podcast Num Episodes -->
|
||||
<div cy-id="numEpisodes" v-else-if="!numEpisodesIncomplete && numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', width: 1.25 + 'em', height: 1.25 + 'em' }">
|
||||
<p :style="{ fontSize: 0.8 + 'em' }">{{ numEpisodes }}</p>
|
||||
<p :style="{ fontSize: 0.8 + 'em' }" role="status" :aria-label="$strings.LabelNumberOfEpisodes">{{ numEpisodes }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Podcast Num Episodes -->
|
||||
@@ -244,6 +237,7 @@ export default {
|
||||
return this.mediaMetadata.series
|
||||
},
|
||||
seriesName() {
|
||||
if (this.collapsedSeries?.name) return this.collapsedSeries.name
|
||||
return this.series?.name || null
|
||||
},
|
||||
seriesSequence() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div ref="card" :id="`collection-card-${index}`" :style="{ width: cardWidth + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div ref="card" :id="`collection-card-${index}`" role="button" :style="{ width: cardWidth + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div class="relative" :style="{ height: coverHeight + 'px' }">
|
||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div ref="card" :id="`playlist-card-${index}`" :style="{ width: cardWidth + 'px', fontSize: sizeMultiplier + 'rem' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div ref="card" :id="`playlist-card-${index}`" role="button" :style="{ width: cardWidth + 'px', fontSize: sizeMultiplier + 'rem' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div class="relative" :style="{ height: coverHeight + 'px' }">
|
||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div cy-id="card" ref="card" :id="`series-card-${index}`" :style="{ width: cardWidth + 'px' }" class="absolute rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div cy-id="card" ref="card" :id="`series-card-${index}`" tabindex="0" :style="{ width: cardWidth + 'px' }" class="absolute rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div cy-id="covers-area" class="relative" :style="{ height: coverHeight + 'px' }">
|
||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
||||
@@ -7,12 +7,12 @@
|
||||
</div>
|
||||
|
||||
<div cy-id="seriesLengthMarker" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #cd9d49dd">
|
||||
<p :style="{ fontSize: 0.8 + 'em' }">{{ books.length }}</p>
|
||||
<p :style="{ fontSize: 0.8 + 'em' }" role="status" :aria-label="$strings.LabelNumberOfBooks">{{ books.length }}</p>
|
||||
</div>
|
||||
|
||||
<div cy-id="seriesProgressBar" v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1e shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
|
||||
<div cy-id="seriesProgressBar" v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1e shadow-sm max-w-full z-10 rounded-b w-full box-shadow-progressbar" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
|
||||
|
||||
<div cy-id="hoveringDisplayTitle" v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: '1em' }">
|
||||
<div cy-id="hoveringDisplayTitle" v-if="hasValidCovers" aria-hidden="true" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: '1em' }">
|
||||
<p :style="{ fontSize: 1.2 + 'em' }">{{ displayTitle }}</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="">
|
||||
<div class="w-full relative sm:w-80">
|
||||
<form @submit.prevent="submitSearch">
|
||||
<form role="search" @submit.prevent="submitSearch">
|
||||
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
|
||||
</form>
|
||||
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
|
||||
<button :aria-hidden="!search" class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
|
||||
<span v-if="!search" class="material-symbols" style="font-size: 1.2rem"></span>
|
||||
<span v-else class="material-symbols" style="font-size: 1.2rem">close</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full max-w-64 sm:max-w-80 sm:w-80 bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu" @mousedown.stop.prevent>
|
||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
||||
<button type="button" class="relative w-full h-full bg-bg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
||||
<span class="flex items-center justify-between">
|
||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||
</span>
|
||||
<div class="relative h-7">
|
||||
<button type="button" class="relative w-full h-full bg-bg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="menu" :aria-expanded="showMenu" @click.prevent="showMenu = !showMenu">
|
||||
<span class="flex items-center justify-between">
|
||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||
</span>
|
||||
</button>
|
||||
<span v-if="selected === 'all'" class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
<div v-else class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-200" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
|
||||
<button v-else :aria-label="$strings.ButtonClearFilter" class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-200" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
|
||||
<span class="material-symbols" style="font-size: 1.1rem">close</span>
|
||||
</div>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm libraryFilterMenu">
|
||||
<ul v-show="!sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<ul v-show="!sublist" class="h-full w-full" role="menu">
|
||||
<template v-for="item in selectItems">
|
||||
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="option" @click="clickedOption(item)">
|
||||
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" :aria-haspopup="item.sublist ? '' : 'menu'" @click="clickedOption(item)">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-normal ml-3 block truncate text-sm">{{ item.text }}</span>
|
||||
</div>
|
||||
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
|
||||
<span class="material-symbols text-2xl">arrow_right</span>
|
||||
<span class="material-symbols text-2xl" :aria-label="$strings.LabelMore">arrow_right</span>
|
||||
</div>
|
||||
<!-- selected checkmark icon -->
|
||||
<div v-if="item.value === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
|
||||
@@ -31,8 +33,8 @@
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<ul v-show="sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-white/5" role="option" @click="sublist = null">
|
||||
<ul v-show="sublist" class="h-full w-full" role="menu">
|
||||
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-white/5" role="menuitem" @click="sublist = null">
|
||||
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
|
||||
<span class="material-symbols text-2xl">arrow_left</span>
|
||||
</div>
|
||||
@@ -40,13 +42,13 @@
|
||||
<span class="font-normal block truncate">{{ $strings.ButtonBack }}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="option">
|
||||
<li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="menuitem">
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="font-normal block truncate py-2">{{ $getString('LabelLibraryFilterSublistEmpty', [selectedSublistText]) }}</span>
|
||||
</div>
|
||||
</li>
|
||||
<template v-for="item in sublistItems">
|
||||
<li :key="item.value" class="select-none relative px-2 cursor-pointer hover:bg-white/5" :class="`${sublist}.${item.value}` === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="option" @click="clickedSublistOption(item.value)">
|
||||
<li :key="item.value" class="select-none relative px-2 cursor-pointer hover:bg-white/5" :class="`${sublist}.${item.value}` === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" @click="clickedSublistOption(item.value)">
|
||||
<div class="flex items-center">
|
||||
<span class="font-normal truncate py-2 text-xs">{{ item.text }}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
||||
<button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
||||
<button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="menu" :aria-expanded="showMenu" @click.prevent="showMenu = !showMenu">
|
||||
<span class="flex items-center justify-between">
|
||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||
<span class="material-symbols text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||
<span class="material-symbols text-lg text-yellow-400" :aria-label="descending ? $strings.LabelSortDescending : $strings.LabelSortAscending">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm" role="listbox" aria-labelledby="listbox-label">
|
||||
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm" role="menu">
|
||||
<template v-for="item in selectItems">
|
||||
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="option" @click="clickedOption(item.value)">
|
||||
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" @click="clickedOption(item.value)">
|
||||
<div class="flex items-center">
|
||||
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
|
||||
</div>
|
||||
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
||||
<span class="material-symbols text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||
<span class="material-symbols text-xl" :aria-label="descending ? $strings.LabelSortDescending : $strings.LabelSortAscending">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="relative ml-4 sm:ml-8" v-click-outside="clickOutside">
|
||||
<div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="setShowMenu(true)">
|
||||
<span class="text-gray-200 text-sm sm:text-base">{{ playbackRate.toFixed(1) }}<span class="text-base">x</span></span>
|
||||
<span class="text-gray-200 text-sm sm:text-base">{{ playbackRateDisplay }}<span class="text-base">x</span></span>
|
||||
</div>
|
||||
<div v-show="showMenu" class="absolute -top-[5.5rem] z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }">
|
||||
<div class="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }">
|
||||
@@ -19,7 +19,7 @@
|
||||
<div class="w-full py-1 px-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" />
|
||||
<p class="px-2 text-2xl sm:text-3xl">{{ playbackRate }}<span class="text-2xl">x</span></p>
|
||||
<p class="px-2 text-2xl sm:text-3xl">{{ playbackRateDisplay }}<span class="text-2xl">x</span></p>
|
||||
<ui-icon-btn :disabled="!canIncrement" icon="add" @click="increment" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,6 +33,10 @@ export default {
|
||||
value: {
|
||||
type: [String, Number],
|
||||
default: 1
|
||||
},
|
||||
playbackRateIncrementDecrement: {
|
||||
type: Number,
|
||||
default: 0.1
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -58,10 +62,17 @@ export default {
|
||||
return [0.5, 1, 1.2, 1.5, 2]
|
||||
},
|
||||
canIncrement() {
|
||||
return this.playbackRate + 0.1 <= this.MAX_SPEED
|
||||
return this.playbackRate + this.playbackRateIncrementDecrement <= this.MAX_SPEED
|
||||
},
|
||||
canDecrement() {
|
||||
return this.playbackRate - 0.1 >= this.MIN_SPEED
|
||||
return this.playbackRate - this.playbackRateIncrementDecrement >= this.MIN_SPEED
|
||||
},
|
||||
playbackRateDisplay() {
|
||||
if (this.playbackRateIncrementDecrement == 0.05) return this.playbackRate.toFixed(2)
|
||||
// For 0.1 increment: Only show 2 decimal places if the playback rate is 2 decimals
|
||||
const numDecimals = String(this.playbackRate).split('.')[1]?.length || 0
|
||||
if (numDecimals <= 1) return this.playbackRate.toFixed(1)
|
||||
return this.playbackRate.toFixed(2)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -73,14 +84,14 @@ export default {
|
||||
this.$nextTick(() => this.setShowMenu(false))
|
||||
},
|
||||
increment() {
|
||||
if (this.playbackRate + 0.1 > this.MAX_SPEED) return
|
||||
var newPlaybackRate = this.playbackRate + 0.1
|
||||
this.playbackRate = Number(newPlaybackRate.toFixed(1))
|
||||
if (this.playbackRate + this.playbackRateIncrementDecrement > this.MAX_SPEED) return
|
||||
var newPlaybackRate = this.playbackRate + this.playbackRateIncrementDecrement
|
||||
this.playbackRate = Number(newPlaybackRate.toFixed(2))
|
||||
},
|
||||
decrement() {
|
||||
if (this.playbackRate - 0.1 < this.MIN_SPEED) return
|
||||
var newPlaybackRate = this.playbackRate - 0.1
|
||||
this.playbackRate = Number(newPlaybackRate.toFixed(1))
|
||||
if (this.playbackRate - this.playbackRateIncrementDecrement < this.MIN_SPEED) return
|
||||
var newPlaybackRate = this.playbackRate - this.playbackRateIncrementDecrement
|
||||
this.playbackRate = Number(newPlaybackRate.toFixed(2))
|
||||
},
|
||||
updateMenuPositions() {
|
||||
if (!this.$refs.wrapper) return
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
||||
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
||||
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none cursor-pointer" aria-haspopup="menu" :aria-expanded="showMenu" @click.prevent="showMenu = !showMenu">
|
||||
<span class="flex items-center justify-between">
|
||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||
<span class="material-symbols text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||
<span class="material-symbols text-lg text-yellow-400" :aria-label="descending ? $strings.LabelSortDescending : $strings.LabelSortAscending">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-80 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm" role="listbox" aria-labelledby="listbox-label">
|
||||
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-80 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm" role="menu">
|
||||
<template v-for="item in items">
|
||||
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="option" @click="clickedOption(item.value)">
|
||||
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" @click="clickedOption(item.value)">
|
||||
<div class="flex items-center">
|
||||
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
|
||||
</div>
|
||||
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
||||
<span class="material-symbols text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||
<span class="material-symbols text-xl" :aria-label="descending ? $strings.LabelSortDescending : $strings.LabelSortAscending">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
@@ -121,6 +121,8 @@ export default {
|
||||
|
||||
var img = document.createElement('img')
|
||||
img.src = src
|
||||
img.alt = `${this.name}, ${this.$strings.LabelCover}`
|
||||
img.ariaHidden = true
|
||||
img.className = 'absolute top-0 left-0 w-full h-full'
|
||||
img.style.objectFit = showCoverBg ? 'contain' : 'cover'
|
||||
|
||||
|
||||
@@ -90,8 +90,8 @@
|
||||
<div class="relative">
|
||||
<ui-textarea-with-label :value="prettyFfprobeData" readonly :rows="30" class="text-xs" />
|
||||
|
||||
<button class="absolute top-4 right-4" :class="copiedToClipboard ? 'text-success' : 'text-white/50 hover:text-white/80'" @click.stop="copyFfprobeData">
|
||||
<span class="material-symbols">{{ copiedToClipboard ? 'check' : 'content_copy' }}</span>
|
||||
<button class="absolute top-4 right-4" :class="hasCopied ? 'text-success' : 'text-gray-400 hover:text-white'" @click.stop="copyToClipboard">
|
||||
<span class="material-symbols">{{ hasCopied ? 'done' : 'content_copy' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,14 +113,13 @@ export default {
|
||||
return {
|
||||
probingFile: false,
|
||||
ffprobeData: null,
|
||||
copiedToClipboard: false
|
||||
hasCopied: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show(newVal) {
|
||||
if (newVal) {
|
||||
this.ffprobeData = null
|
||||
this.copiedToClipboard = false
|
||||
this.probingFile = false
|
||||
}
|
||||
}
|
||||
@@ -165,8 +164,13 @@ export default {
|
||||
this.probingFile = false
|
||||
})
|
||||
},
|
||||
async copyFfprobeData() {
|
||||
this.copiedToClipboard = await this.$copyToClipboard(this.prettyFfprobeData)
|
||||
copyToClipboard() {
|
||||
clearTimeout(this.hasCopied)
|
||||
this.$copyToClipboard(this.prettyFfprobeData).then((success) => {
|
||||
this.hasCopied = setTimeout(() => {
|
||||
this.hasCopied = null
|
||||
}, 2000)
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div v-if="show" class="w-full h-full py-4">
|
||||
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
|
||||
<div class="flex px-8 items-center py-2">
|
||||
@@ -54,8 +54,7 @@ export default {
|
||||
options: {
|
||||
provider: undefined,
|
||||
overrideDetails: true,
|
||||
overrideCover: true,
|
||||
overrideDefaults: true
|
||||
overrideCover: true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -99,8 +98,8 @@ export default {
|
||||
init() {
|
||||
// If we don't have a set provider (first open of dialog) or we've switched library, set
|
||||
// the selected provider to the current library default provider
|
||||
if (!this.options.provider || this.options.lastUsedLibrary != this.currentLibraryId) {
|
||||
this.options.lastUsedLibrary = this.currentLibraryId
|
||||
if (!this.options.provider || this.lastUsedLibrary != this.currentLibraryId) {
|
||||
this.lastUsedLibrary = this.currentLibraryId
|
||||
this.options.provider = this.libraryProvider
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,24 +5,26 @@
|
||||
<p class="text-3xl text-white truncate">{{ $strings.LabelYourBookmarks }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div v-if="show" class="w-full h-full">
|
||||
<div v-if="show" class="w-full rounded-lg bg-bg box-shadow-md relative" style="max-height: 80vh">
|
||||
<div v-if="bookmarks.length" class="h-full max-h-[calc(80vh-60px)] w-full relative overflow-y-auto overflow-x-hidden">
|
||||
<template v-for="bookmark in bookmarks">
|
||||
<modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" @click="clickBookmark" @update="submitUpdateBookmark" @delete="deleteBookmark" />
|
||||
<modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" :playback-rate="playbackRate" @click="clickBookmark" @delete="deleteBookmark" />
|
||||
</template>
|
||||
<div v-if="!bookmarks.length" class="flex h-32 items-center justify-center">
|
||||
<p class="text-xl">{{ $strings.MessageNoBookmarks }}</p>
|
||||
</div>
|
||||
<div v-if="!hideCreate" class="w-full h-px bg-white bg-opacity-10" />
|
||||
<form v-if="!hideCreate" @submit.prevent="submitCreateBookmark">
|
||||
<div v-show="canCreateBookmark" class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">
|
||||
</div>
|
||||
<div v-else class="flex h-32 items-center justify-center">
|
||||
<p class="text-xl">{{ $strings.MessageNoBookmarks }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="canCreateBookmark && !hideCreate" class="w-full border-t border-white/10">
|
||||
<form @submit.prevent="submitCreateBookmark">
|
||||
<div class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">
|
||||
<div class="w-16 max-w-16 text-center">
|
||||
<p class="text-sm font-mono text-gray-400">
|
||||
{{ this.$secondsToTimestamp(currentTime) }}
|
||||
{{ this.$secondsToTimestamp(currentTime / playbackRate) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-grow px-2">
|
||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full h-10" />
|
||||
</div>
|
||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-symbols text-2xl -mt-px">add</span></ui-btn>
|
||||
</div>
|
||||
@@ -45,6 +47,7 @@ export default {
|
||||
default: 0
|
||||
},
|
||||
libraryItemId: String,
|
||||
playbackRate: Number,
|
||||
hideCreate: Boolean
|
||||
},
|
||||
data() {
|
||||
@@ -57,6 +60,7 @@ export default {
|
||||
watch: {
|
||||
show(newVal) {
|
||||
if (newVal) {
|
||||
this.selectedBookmark = null
|
||||
this.showBookmarkTitleInput = false
|
||||
this.newBookmarkTitle = ''
|
||||
}
|
||||
@@ -72,7 +76,7 @@ export default {
|
||||
}
|
||||
},
|
||||
canCreateBookmark() {
|
||||
return !this.bookmarks.find((bm) => bm.time === this.currentTime)
|
||||
return !this.bookmarks.find((bm) => Math.abs(this.currentTime - bm.time) < 1)
|
||||
},
|
||||
dateFormat() {
|
||||
return this.$store.state.serverSettings.dateFormat
|
||||
@@ -102,19 +106,6 @@ export default {
|
||||
clickBookmark(bm) {
|
||||
this.$emit('select', bm)
|
||||
},
|
||||
submitUpdateBookmark(updatedBookmark) {
|
||||
var bookmark = { ...updatedBookmark }
|
||||
this.$axios
|
||||
.$patch(`/api/me/item/${this.libraryItemId}/bookmark`, bookmark)
|
||||
.then(() => {
|
||||
this.$toast.success(this.$strings.ToastBookmarkUpdateSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
console.error(error)
|
||||
})
|
||||
this.show = false
|
||||
},
|
||||
submitCreateBookmark() {
|
||||
if (!this.newBookmarkTitle) {
|
||||
this.newBookmarkTitle = this.$formatDatetime(Date.now(), this.dateFormat, this.timeFormat)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="hidden absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 rounded-lg items-center justify-center" style="z-index: 61" @click="clickClose">
|
||||
<div class="absolute top-3 right-3 md:top-5 md:right-5 h-8 w-8 md:h-12 md:w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300">
|
||||
<div ref="wrapper" role="dialog" aria-modal="true" class="hidden absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 rounded-lg items-center justify-center" style="z-index: 61" @click="clickClose">
|
||||
<button type="button" class="absolute top-3 right-3 md:top-5 md:right-5 h-8 w-8 md:h-12 md:w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" aria-label="Close modal">
|
||||
<span class="material-symbols text-2xl md:text-4xl">close</span>
|
||||
</div>
|
||||
</button>
|
||||
<div ref="content" class="text-white">
|
||||
<form v-if="selectedSeries" @submit.prevent="submitSeriesForm">
|
||||
<div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="modal modal-bg w-full h-full fixed top-0 left-0 bg-primary items-center justify-center opacity-0 hidden" :class="`z-${zIndex} bg-opacity-${bgOpacity}`">
|
||||
<div ref="wrapper" role="dialog" aria-modal="true" class="modal modal-bg w-full h-full fixed top-0 left-0 bg-primary items-center justify-center opacity-0 hidden" :class="`z-${zIndex} bg-opacity-${bgOpacity}`">
|
||||
<div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" />
|
||||
|
||||
<button class="absolute top-4 right-4 landscape:top-4 landscape:right-4 md:portrait:top-5 md:portrait:right-5 lg:top-5 lg:right-5 inline-flex text-gray-200 hover:text-white" aria-label="Close modal" @click="clickClose">
|
||||
<span class="material-symbols text-2xl landscape:text-2xl md:portrait:text-4xl lg:text-4xl">close</span>
|
||||
</button>
|
||||
<slot name="outer" />
|
||||
<div ref="content" style="min-width: 380px; min-height: 200px; max-width: 100vw" class="relative text-white" aria-modal="true" :style="{ height: modalHeight, width: modalWidth, marginTop: contentMarginTop + 'px' }" @mousedown="mousedownModal" @mouseup="mouseupModal" v-click-outside="clickBg">
|
||||
<div ref="content" tabindex="0" style="min-width: 380px; min-height: 200px; max-width: 100vw" class="relative text-white outline-none" :style="{ height: modalHeight, width: modalWidth, marginTop: contentMarginTop + 'px' }" @mousedown="mousedownModal" @mouseup="mouseupModal" v-click-outside="clickBg">
|
||||
<slot />
|
||||
<div v-if="processing" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-black bg-opacity-60 rounded-lg flex items-center justify-center">
|
||||
<ui-loading-indicator />
|
||||
@@ -126,6 +126,9 @@ export default {
|
||||
|
||||
this.$eventBus.$on('modal-hotkey', this.hotkey)
|
||||
this.$store.commit('setOpenModal', this.name)
|
||||
|
||||
// Set focus to the modal content
|
||||
this.content.focus()
|
||||
},
|
||||
setHide() {
|
||||
if (this.content) this.content.style.transform = 'scale(0)'
|
||||
|
||||
@@ -11,9 +11,12 @@
|
||||
<div class="flex items-center mb-4">
|
||||
<ui-select-input v-model="jumpForwardAmount" :label="$strings.LabelJumpForwardAmount" menuMaxHeight="250px" :items="jumpValues" @input="setJumpForwardAmount" />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center mb-4">
|
||||
<ui-select-input v-model="jumpBackwardAmount" :label="$strings.LabelJumpBackwardAmount" menuMaxHeight="250px" :items="jumpValues" @input="setJumpBackwardAmount" />
|
||||
</div>
|
||||
<div class="flex items-center mb-4">
|
||||
<ui-select-input v-model="playbackRateIncrementDecrement" :label="$strings.LabelPlaybackRateIncrementDecrement" menuMaxHeight="250px" :items="playbackRateIncrementDecrementValues" @input="setPlaybackRateIncrementDecrementAmount" />
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
@@ -35,7 +38,9 @@ export default {
|
||||
{ text: this.$getString('LabelTimeDurationXMinutes', ['5']), value: 300 }
|
||||
],
|
||||
jumpForwardAmount: 10,
|
||||
jumpBackwardAmount: 10
|
||||
jumpBackwardAmount: 10,
|
||||
playbackRateIncrementDecrementValues: [0.1, 0.05],
|
||||
playbackRateIncrementDecrement: 0.1
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -60,10 +65,15 @@ export default {
|
||||
this.jumpBackwardAmount = val
|
||||
this.$store.dispatch('user/updateUserSettings', { jumpBackwardAmount: val })
|
||||
},
|
||||
setPlaybackRateIncrementDecrementAmount(val) {
|
||||
this.playbackRateIncrementDecrement = val
|
||||
this.$store.dispatch('user/updateUserSettings', { playbackRateIncrementDecrement: val })
|
||||
},
|
||||
settingsUpdated() {
|
||||
this.useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack')
|
||||
this.jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount')
|
||||
this.jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount')
|
||||
this.playbackRateIncrementDecrement = this.$store.getters['user/getUserSetting']('playbackRateIncrementDecrement')
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -16,15 +16,16 @@
|
||||
<template v-if="currentShare">
|
||||
<div class="w-full py-2">
|
||||
<label class="px-1 text-sm font-semibold block">{{ $strings.LabelShareURL }}</label>
|
||||
<ui-text-input v-model="currentShareUrl" show-copy readonly class="text-base h-10" />
|
||||
<ui-text-input v-model="currentShareUrl" show-copy readonly />
|
||||
</div>
|
||||
<div class="w-full py-2 px-1">
|
||||
<p v-if="currentShare.expiresAt" class="text-base">{{ $getString('MessageShareExpiresIn', [currentShareTimeRemaining]) }}</p>
|
||||
<p v-if="currentShare.isDownloadable" class="text-sm mb-2">{{ $strings.LabelDownloadable }}</p>
|
||||
<p v-if="currentShare.expiresAt">{{ $getString('MessageShareExpiresIn', [currentShareTimeRemaining]) }}</p>
|
||||
<p v-else>{{ $strings.LabelPermanent }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex flex-col sm:flex-row items-center justify-between space-y-4 sm:space-y-0 sm:space-x-4 mb-4">
|
||||
<div class="flex flex-col sm:flex-row items-center justify-between space-y-4 sm:space-y-0 sm:space-x-4 mb-2">
|
||||
<div class="w-full sm:w-48">
|
||||
<label class="px-1 text-sm font-semibold block">{{ $strings.LabelSlug }}</label>
|
||||
<ui-text-input v-model="newShareSlug" class="text-base h-10" />
|
||||
@@ -46,6 +47,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center w-full md:w-1/2 mb-4">
|
||||
<p class="text-sm text-gray-300 py-1 px-1">{{ $strings.LabelDownloadable }}</p>
|
||||
<ui-toggle-switch size="sm" v-model="isDownloadable" />
|
||||
<ui-tooltip :text="$strings.LabelShareDownloadableHelp">
|
||||
<p class="pl-4 text-sm">
|
||||
<span class="material-symbols icon-text text-sm">info</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<p class="text-sm text-gray-300 py-1 px-1" v-html="$getString('MessageShareURLWillBe', [demoShareUrl])" />
|
||||
<p class="text-sm text-gray-300 py-1 px-1" v-html="$getString('MessageShareExpirationWillBe', [expirationDateString])" />
|
||||
</template>
|
||||
@@ -81,7 +91,8 @@ export default {
|
||||
text: this.$strings.LabelDays,
|
||||
value: 'days'
|
||||
}
|
||||
]
|
||||
],
|
||||
isDownloadable: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -172,7 +183,8 @@ export default {
|
||||
slug: this.newShareSlug,
|
||||
mediaItemType: 'book',
|
||||
mediaItemId: this.libraryItem.media.id,
|
||||
expiresAt: this.expireDurationSeconds ? Date.now() + this.expireDurationSeconds * 1000 : 0
|
||||
expiresAt: this.expireDurationSeconds ? Date.now() + this.expireDurationSeconds * 1000 : 0,
|
||||
isDownloadable: this.isDownloadable
|
||||
}
|
||||
this.processing = true
|
||||
this.$axios
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="flex items-center px-4 py-4 justify-start relative bg-primary hover:bg-opacity-25" :class="wrapperClass" @click.stop="click" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<div class="flex items-center px-4 py-4 justify-start relative hover:bg-primary/10" :class="wrapperClass" @click.stop="click" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<div class="w-16 max-w-16 text-center">
|
||||
<p class="text-sm font-mono text-gray-400">
|
||||
{{ this.$secondsToTimestamp(bookmark.time) }}
|
||||
{{ this.$secondsToTimestamp(bookmark.time / playbackRate) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-grow overflow-hidden px-2">
|
||||
@@ -10,7 +10,7 @@
|
||||
<form @submit.prevent="submitUpdate">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-grow pr-2">
|
||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full h-10" />
|
||||
</div>
|
||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-symbols text-2xl -mt-px">forward</span></ui-btn>
|
||||
<div class="pl-2 flex items-center">
|
||||
@@ -35,7 +35,8 @@ export default {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
highlight: Boolean
|
||||
highlight: Boolean,
|
||||
playbackRate: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -83,11 +84,19 @@ export default {
|
||||
if (this.newBookmarkTitle === this.bookmark.title) {
|
||||
return this.cancelEditing()
|
||||
}
|
||||
var bookmark = { ...this.bookmark }
|
||||
const bookmark = { ...this.bookmark }
|
||||
bookmark.title = this.newBookmarkTitle
|
||||
this.$emit('update', bookmark)
|
||||
|
||||
this.$axios
|
||||
.$patch(`/api/me/item/${bookmark.libraryItemId}/bookmark`, bookmark)
|
||||
.then(() => {
|
||||
this.isEditing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
console.error(error)
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -2,7 +2,7 @@
|
||||
<modals-modal v-model="show" name="changelog" :width="800" :height="'unset'">
|
||||
<template #outer>
|
||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||
<p class="text-3xl text-white truncate">Changelog</p>
|
||||
<h1 class="text-3xl text-white truncate">Changelog</h1>
|
||||
</div>
|
||||
</template>
|
||||
<div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh">
|
||||
@@ -13,7 +13,7 @@
|
||||
</p>
|
||||
<div class="custom-text" v-html="getChangelog(release)" />
|
||||
</div>
|
||||
<div v-if="release !== releasesToShow[releasesToShow.length - 1]" class="border-b border-black-300 my-8" />
|
||||
<div v-if="release !== releasesToShow[releasesToShow.length - 1]" :key="`${release.name}-divider`" class="border-b border-black-300 my-8" />
|
||||
</template>
|
||||
</div>
|
||||
</modals-modal>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div v-if="show" class="w-full h-full">
|
||||
<div class="py-4 px-4">
|
||||
<h1 v-if="!showBatchCollectionModal" class="text-2xl">{{ $strings.LabelAddToCollection }}</h1>
|
||||
@@ -19,9 +19,20 @@
|
||||
</template>
|
||||
</transition-group>
|
||||
</div>
|
||||
<div v-if="!collections.length" class="flex h-32 items-center justify-center">
|
||||
<p class="text-xl">{{ $strings.MessageNoCollections }}</p>
|
||||
<div v-if="!collections.length" class="flex h-32 items-center justify-center text-center px-2">
|
||||
<div>
|
||||
<p class="text-xl mb-2">{{ $strings.MessageNoCollections }}</p>
|
||||
<div class="text-sm flex items-center justify-center text-gray-200">
|
||||
<p>{{ $strings.MessageBookshelfNoCollectionsHelp }}</p>
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides/collections" target="_blank" class="inline-flex">
|
||||
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full h-px bg-white bg-opacity-10" />
|
||||
<form @submit.prevent="submitCreateCollection">
|
||||
<div class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">
|
||||
@@ -138,7 +149,6 @@ export default {
|
||||
.$post(`/api/collections/${collection.id}/batch/remove`, { books: this.selectedBookIds })
|
||||
.then((updatedCollection) => {
|
||||
console.log(`Books removed from collection`, updatedCollection)
|
||||
this.$toast.success(this.$strings.ToastCollectionItemsRemoveSuccess)
|
||||
this.processing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -152,7 +162,6 @@ export default {
|
||||
.$delete(`/api/collections/${collection.id}/book/${this.selectedLibraryItemId}`)
|
||||
.then((updatedCollection) => {
|
||||
console.log(`Book removed from collection`, updatedCollection)
|
||||
this.$toast.success(this.$strings.ToastCollectionItemsRemoveSuccess)
|
||||
this.processing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -167,12 +176,11 @@ export default {
|
||||
this.processing = true
|
||||
|
||||
if (this.showBatchCollectionModal) {
|
||||
// BATCH Remove books
|
||||
// BATCH Add books
|
||||
this.$axios
|
||||
.$post(`/api/collections/${collection.id}/batch/add`, { books: this.selectedBookIds })
|
||||
.then((updatedCollection) => {
|
||||
console.log(`Books added to collection`, updatedCollection)
|
||||
this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
|
||||
this.processing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -187,7 +195,6 @@ export default {
|
||||
.$post(`/api/collections/${collection.id}/book`, { id: this.selectedLibraryItemId })
|
||||
.then((updatedCollection) => {
|
||||
console.log(`Book added to collection`, updatedCollection)
|
||||
this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
|
||||
this.processing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -214,7 +221,6 @@ export default {
|
||||
.$post('/api/collections', newCollection)
|
||||
.then((data) => {
|
||||
console.log('New Collection Created', data)
|
||||
this.$toast.success(`Collection "${data.name}" created`)
|
||||
this.processing = false
|
||||
this.newCollectionName = ''
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-bg" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-black-400" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<div v-if="isBookIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
|
||||
<div class="w-20 max-w-20 text-center">
|
||||
<covers-collection-cover :book-items="books" :width="80" :height="40 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
|
||||
@@ -2,24 +2,24 @@
|
||||
<modals-modal v-model="show" name="edit-book" :width="800" :height="height" :processing="processing" :content-margin-top="marginTop">
|
||||
<template #outer>
|
||||
<div class="absolute top-0 left-0 p-4 landscape:px-4 landscape:py-2 md:portrait:p-5 lg:p-5 w-2/3 overflow-hidden pointer-events-none">
|
||||
<p class="text-xl md:portrait:text-3xl md:landscape:text-lg lg:text-3xl text-white truncate pointer-events-none">{{ title }}</p>
|
||||
<h1 class="text-xl md:portrait:text-3xl md:landscape:text-lg lg:text-3xl text-white truncate pointer-events-none">{{ title }}</h1>
|
||||
</div>
|
||||
</template>
|
||||
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
||||
<div role="tablist" class="absolute -top-10 left-0 z-10 w-full flex">
|
||||
<template v-for="tab in availableTabs">
|
||||
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
||||
<button :key="tab.id" role="tab" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||
<div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevBook" @mousedown.prevent>arrow_back_ios</div>
|
||||
</div>
|
||||
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||
<div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextBook" @mousedown.prevent>arrow_forward_ios</div>
|
||||
<div role="tabpanel" class="w-full h-full max-h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative">
|
||||
<component v-if="libraryItem && show" :is="tabName" :library-item="libraryItem" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
|
||||
</div>
|
||||
|
||||
<div class="w-full h-full max-h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative">
|
||||
<component v-if="libraryItem && show" :is="tabName" :library-item="libraryItem" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
|
||||
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||
<button class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" :aria-label="$strings.ButtonNext" @click.stop.prevent="goPrevBook" @mousedown.prevent>arrow_back_ios</button>
|
||||
</div>
|
||||
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||
<button class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" :aria-label="$strings.ButtonPrevious" @click.stop.prevent="goNextBook" @mousedown.prevent>arrow_forward_ios</button>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
@@ -196,6 +196,9 @@ export default {
|
||||
methods: {
|
||||
async goPrevBook() {
|
||||
if (this.currentBookshelfIndex - 1 < 0) return
|
||||
// Remove focus from active input
|
||||
document.activeElement?.blur?.()
|
||||
|
||||
var prevBookId = this.bookshelfBookIds[this.currentBookshelfIndex - 1]
|
||||
this.processing = true
|
||||
var prevBook = await this.$axios.$get(`/api/items/${prevBookId}?expanded=1`).catch((error) => {
|
||||
@@ -215,6 +218,9 @@ export default {
|
||||
},
|
||||
async goNextBook() {
|
||||
if (this.currentBookshelfIndex >= this.bookshelfBookIds.length - 1) return
|
||||
// Remove focus from active input
|
||||
document.activeElement?.blur?.()
|
||||
|
||||
this.processing = true
|
||||
var nextBookId = this.bookshelfBookIds[this.currentBookshelfIndex + 1]
|
||||
var nextBook = await this.$axios.$get(`/api/items/${nextBookId}?expanded=1`).catch((error) => {
|
||||
@@ -300,4 +306,4 @@ export default {
|
||||
.tab.tab-selected {
|
||||
height: 41px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -113,6 +113,10 @@ export default {
|
||||
return false
|
||||
})
|
||||
console.log('updateResult', updateResult)
|
||||
} else if (!lastEpisodeCheck) {
|
||||
this.$toast.error(this.$strings.ToastDateTimeInvalidOrIncomplete)
|
||||
this.checkingNewEpisodes = false
|
||||
return false
|
||||
}
|
||||
|
||||
this.$axios
|
||||
|
||||
@@ -94,9 +94,9 @@
|
||||
<div v-if="selectedMatchOrig.description" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.description" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" />
|
||||
<ui-rich-text-editor v-model="selectedMatch.description" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" />
|
||||
<p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</a>
|
||||
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.descriptionPlain.substr(0, 100) + (mediaMetadata.descriptionPlain.length > 100 ? '...' : '') }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<ui-checkbox v-model="enableAutoScan" @input="toggleEnableAutoScan" :label="$strings.LabelEnable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" />
|
||||
</div>
|
||||
<widgets-cron-expression-builder ref="cronExpressionBuilder" v-if="enableAutoScan" v-model="cronExpression" @input="updatedCron" />
|
||||
<div v-else>
|
||||
<p class="text-yellow-400 text-base">{{ $strings.MessageScheduleLibraryScanNote }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div v-if="show" class="w-full h-full">
|
||||
<div class="py-4 px-4">
|
||||
<h1 v-if="!isBatch" class="text-2xl">{{ $strings.LabelAddToPlaylist }}</h1>
|
||||
@@ -19,8 +19,18 @@
|
||||
</template>
|
||||
</transition-group>
|
||||
</div>
|
||||
<div v-if="!playlists.length" class="flex h-32 items-center justify-center">
|
||||
<p class="text-xl">{{ $strings.MessageNoUserPlaylists }}</p>
|
||||
<div v-if="!playlists.length" class="flex h-32 items-center justify-center text-center px-2">
|
||||
<div>
|
||||
<p class="text-xl mb-2">{{ $strings.MessageNoUserPlaylists }}</p>
|
||||
<div class="text-sm flex items-center justify-center text-gray-200">
|
||||
<p>{{ $strings.MessageNoUserPlaylistsHelp }}</p>
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides/collections" target="_blank" class="inline-flex">
|
||||
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full h-px bg-white bg-opacity-10" />
|
||||
<form @submit.prevent="submitCreatePlaylist">
|
||||
@@ -130,7 +140,6 @@ export default {
|
||||
.$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects })
|
||||
.then((updatedPlaylist) => {
|
||||
console.log(`Items removed from playlist`, updatedPlaylist)
|
||||
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
|
||||
this.processing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -148,7 +157,6 @@ export default {
|
||||
.$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects })
|
||||
.then((updatedPlaylist) => {
|
||||
console.log(`Items added to playlist`, updatedPlaylist)
|
||||
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
|
||||
this.processing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -174,7 +182,6 @@ export default {
|
||||
.$post('/api/playlists', newPlaylist)
|
||||
.then((data) => {
|
||||
console.log('New playlist created', data)
|
||||
this.$toast.success(this.$strings.ToastPlaylistCreateSuccess + ': ' + data.name)
|
||||
this.processing = false
|
||||
this.newPlaylistName = ''
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-bg" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-black-400" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<div v-if="isItemIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
|
||||
<div class="w-16 max-w-16 text-center">
|
||||
<covers-playlist-cover :items="items" :width="64" :height="64" />
|
||||
|
||||
@@ -117,8 +117,12 @@ export default {
|
||||
methods: {
|
||||
async goPrevEpisode() {
|
||||
if (this.currentEpisodeIndex - 1 < 0) return
|
||||
// Remove focus from active input
|
||||
document.activeElement?.blur?.()
|
||||
|
||||
const prevEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex - 1]
|
||||
this.processing = true
|
||||
|
||||
const prevEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${prevEpisodeId}`).catch((error) => {
|
||||
const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch episode'
|
||||
this.$toast.error(errorMsg)
|
||||
@@ -134,8 +138,12 @@ export default {
|
||||
},
|
||||
async goNextEpisode() {
|
||||
if (this.currentEpisodeIndex >= this.episodeTableEpisodeIds.length - 1) return
|
||||
// Remove focus from active input
|
||||
document.activeElement?.blur?.()
|
||||
|
||||
this.processing = true
|
||||
const nextEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex + 1]
|
||||
|
||||
const nextEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${nextEpisodeId}`).catch((error) => {
|
||||
const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book'
|
||||
this.$toast.error(errorMsg)
|
||||
@@ -170,6 +178,12 @@ export default {
|
||||
this.show = false
|
||||
}
|
||||
},
|
||||
libraryItemUpdated(libraryItem) {
|
||||
const episode = libraryItem.media.episodes.find((e) => e.id === this.selectedEpisodeId)
|
||||
if (episode) {
|
||||
this.episodeItem = episode
|
||||
}
|
||||
},
|
||||
hotkey(action) {
|
||||
if (action === this.$hotkeys.Modal.NEXT_PAGE) {
|
||||
this.goNextEpisode()
|
||||
@@ -178,9 +192,15 @@ export default {
|
||||
}
|
||||
},
|
||||
registerListeners() {
|
||||
if (this.libraryItem) {
|
||||
this.$eventBus.$on(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
|
||||
}
|
||||
this.$eventBus.$on('modal-hotkey', this.hotkey)
|
||||
},
|
||||
unregisterListeners() {
|
||||
if (this.libraryItem) {
|
||||
this.$eventBus.$on(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
|
||||
}
|
||||
this.$eventBus.$off('modal-hotkey', this.hotkey)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p dir="auto" class="text-lg font-semibold mb-6">{{ title }}</p>
|
||||
<div v-if="description" dir="auto" class="default-style" v-html="description" />
|
||||
<div v-if="description" dir="auto" class="default-style less-spacing" v-html="description" />
|
||||
<p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p>
|
||||
|
||||
<div class="w-full h-px bg-white/5 my-4" />
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
<div>
|
||||
<div class="flex flex-wrap">
|
||||
<div class="w-1/5 p-1">
|
||||
<ui-text-input-with-label v-model="newEpisode.season" :label="$strings.LabelSeason" />
|
||||
<ui-text-input-with-label v-model="newEpisode.season" trim-whitespace :label="$strings.LabelSeason" />
|
||||
</div>
|
||||
<div class="w-1/5 p-1">
|
||||
<ui-text-input-with-label v-model="newEpisode.episode" :label="$strings.LabelEpisode" />
|
||||
<ui-text-input-with-label v-model="newEpisode.episode" trim-whitespace :label="$strings.LabelEpisode" />
|
||||
</div>
|
||||
<div class="w-1/5 p-1">
|
||||
<ui-dropdown v-model="newEpisode.episodeType" :label="$strings.LabelEpisodeType" :items="episodeTypes" small />
|
||||
</div>
|
||||
<div class="w-2/5 p-1">
|
||||
<ui-text-input-with-label v-model="pubDateInput" @input="updatePubDate" type="datetime-local" :label="$strings.LabelPubDate" />
|
||||
<ui-text-input-with-label v-model="pubDateInput" ref="pubdate" type="datetime-local" :label="$strings.LabelPubDate" @input="updatePubDate" />
|
||||
</div>
|
||||
<div class="w-full p-1">
|
||||
<ui-text-input-with-label v-model="newEpisode.title" :label="$strings.LabelTitle" />
|
||||
<ui-text-input-with-label v-model="newEpisode.title" :label="$strings.LabelTitle" trim-whitespace />
|
||||
</div>
|
||||
<div class="w-full p-1">
|
||||
<ui-textarea-with-label v-model="newEpisode.subtitle" :label="$strings.LabelSubtitle" :rows="3" />
|
||||
<ui-textarea-with-label v-model="newEpisode.subtitle" :label="$strings.LabelSubtitle" :rows="3" trim-whitespace />
|
||||
</div>
|
||||
<div class="w-full p-1">
|
||||
<ui-rich-text-editor :label="$strings.LabelDescription" v-model="newEpisode.description" />
|
||||
@@ -145,11 +145,18 @@ export default {
|
||||
return null
|
||||
}
|
||||
|
||||
// Check pubdate is valid if it is being updated. Cannot be set to null in the web client
|
||||
if (this.newEpisode.pubDate === null && this.$refs.pubdate?.$refs?.input?.isInvalidDate) {
|
||||
this.$toast.error(this.$strings.ToastDateTimeInvalidOrIncomplete)
|
||||
return null
|
||||
}
|
||||
|
||||
const updatedDetails = this.getUpdatePayload()
|
||||
if (!Object.keys(updatedDetails).length) {
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
return false
|
||||
}
|
||||
|
||||
return this.updateDetails(updatedDetails)
|
||||
},
|
||||
async updateDetails(updatedDetails) {
|
||||
@@ -163,13 +170,10 @@ export default {
|
||||
|
||||
this.isProcessing = false
|
||||
if (updateResult) {
|
||||
if (updateResult) {
|
||||
this.$toast.success(this.$strings.ToastItemUpdateSuccess)
|
||||
return true
|
||||
} else {
|
||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||
}
|
||||
this.$toast.success(this.$strings.ToastItemUpdateSuccess)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -10,9 +10,7 @@
|
||||
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</p>
|
||||
|
||||
<div class="w-full relative">
|
||||
<ui-text-input v-model="currentFeed.feedUrl" readonly />
|
||||
|
||||
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(currentFeed.feedUrl)">content_copy</span>
|
||||
<ui-text-input :value="feedUrl" readonly show-copy />
|
||||
</div>
|
||||
|
||||
<div v-if="currentFeed.meta" class="mt-5">
|
||||
@@ -111,8 +109,11 @@ export default {
|
||||
userIsAdminOrUp() {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
feedUrl() {
|
||||
return this.currentFeed ? `${window.origin}${this.$config.routerBasePath}${this.currentFeed.feedUrl}` : ''
|
||||
},
|
||||
demoFeedUrl() {
|
||||
return `${window.origin}/feed/${this.newFeedSlug}`
|
||||
return `${window.origin}${this.$config.routerBasePath}/feed/${this.newFeedSlug}`
|
||||
},
|
||||
isHttp() {
|
||||
return window.origin.startsWith('http://')
|
||||
@@ -157,9 +158,6 @@ export default {
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
copyToClipboard(str) {
|
||||
this.$copyToClipboard(str, this)
|
||||
},
|
||||
closeFeed() {
|
||||
this.processing = true
|
||||
this.$axios
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedGeneral }}</p>
|
||||
|
||||
<div class="w-full relative">
|
||||
<ui-text-input v-model="feed.feedUrl" readonly />
|
||||
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feed.feedUrl)">content_copy</span>
|
||||
<ui-text-input :value="feedUrl" readonly show-copy />
|
||||
</div>
|
||||
|
||||
<div v-if="feed.meta" class="mt-5">
|
||||
@@ -70,14 +69,11 @@ export default {
|
||||
},
|
||||
_feed() {
|
||||
return this.feed || {}
|
||||
},
|
||||
feedUrl() {
|
||||
return this.feed ? `${window.origin}${this.$config.routerBasePath}${this.feed.feedUrl}` : ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(str) {
|
||||
this.$copyToClipboard(str, this)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<div class="w-full -mt-6">
|
||||
<div class="w-full relative mb-1">
|
||||
<div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full">
|
||||
<controls-playback-speed-control v-model="playbackRate" @input="setPlaybackRate" @change="playbackRateChanged" class="mx-2 block" />
|
||||
<controls-playback-speed-control v-model="playbackRate" @input="setPlaybackRate" @change="playbackRateChanged" :playbackRateIncrementDecrement="playbackRateIncrementDecrement" class="mx-2 block" />
|
||||
|
||||
<ui-tooltip direction="left" :text="$strings.LabelVolume">
|
||||
<ui-tooltip direction="bottom" :text="$strings.LabelVolume">
|
||||
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden sm:block" />
|
||||
</ui-tooltip>
|
||||
|
||||
@@ -180,6 +180,9 @@ export default {
|
||||
useChapterTrack() {
|
||||
const _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
|
||||
return this.chapters.length ? _useChapterTrack : false
|
||||
},
|
||||
playbackRateIncrementDecrement() {
|
||||
return this.$store.getters['user/getUserSetting']('playbackRateIncrementDecrement')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -223,12 +226,12 @@ export default {
|
||||
},
|
||||
increasePlaybackRate() {
|
||||
if (this.playbackRate >= 10) return
|
||||
this.playbackRate = Number((this.playbackRate + 0.1).toFixed(1))
|
||||
this.playbackRate = Number((this.playbackRate + this.playbackRateIncrementDecrement || 0.1).toFixed(2))
|
||||
this.setPlaybackRate(this.playbackRate)
|
||||
},
|
||||
decreasePlaybackRate() {
|
||||
if (this.playbackRate <= 0.5) return
|
||||
this.playbackRate = Number((this.playbackRate - 0.1).toFixed(1))
|
||||
this.playbackRate = Number((this.playbackRate - this.playbackRateIncrementDecrement || 0.1).toFixed(2))
|
||||
this.setPlaybackRate(this.playbackRate)
|
||||
},
|
||||
playbackRateChanged(playbackRate) {
|
||||
|
||||
@@ -97,9 +97,9 @@ export default {
|
||||
},
|
||||
ebookUrl() {
|
||||
if (this.fileId) {
|
||||
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
|
||||
return `${this.$config.routerBasePath}/api/items/${this.libraryItemId}/ebook/${this.fileId}`
|
||||
}
|
||||
return `/api/items/${this.libraryItemId}/ebook`
|
||||
return `${this.$config.routerBasePath}/api/items/${this.libraryItemId}/ebook`
|
||||
},
|
||||
themeRules() {
|
||||
const isDark = this.ereaderSettings.theme === 'dark'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div id="heatmap" class="w-full">
|
||||
<div class="mx-auto" :style="{ height: innerHeight + 160 + 'px', width: innerWidth + 52 + 'px' }" style="background-color: rgba(13, 17, 23, 0)">
|
||||
<p class="mb-2 px-1 text-sm text-gray-200">{{ $getString('MessageListeningSessionsInTheLastYear', [Object.values(daysListening).length]) }}</p>
|
||||
<p class="mb-2 px-1 text-sm text-gray-200">{{ $getString('MessageDaysListenedInTheLastYear', [daysListenedInTheLastYear]) }}</p>
|
||||
<div class="border border-white border-opacity-25 rounded py-2 w-full" style="background-color: #232323" :style="{ height: innerHeight + 80 + 'px' }">
|
||||
<div :style="{ width: innerWidth + 'px', height: innerHeight + 'px' }" class="ml-10 mt-5 absolute" @mouseover="mouseover" @mouseout="mouseout">
|
||||
<div v-for="dayLabel in dayLabels" :key="dayLabel.label" :style="dayLabel.style" class="absolute top-0 left-0 text-gray-300">{{ dayLabel.label }}</div>
|
||||
@@ -37,6 +37,7 @@ export default {
|
||||
innerHeight: 13 * 7,
|
||||
blockWidth: 13,
|
||||
data: [],
|
||||
daysListenedInTheLastYear: 0,
|
||||
monthLabels: [],
|
||||
tooltipEl: null,
|
||||
tooltipTextEl: null,
|
||||
@@ -62,9 +63,6 @@ export default {
|
||||
dayOfWeekToday() {
|
||||
return new Date().getDay()
|
||||
},
|
||||
firstWeekStart() {
|
||||
return this.$addDaysToToday(-this.daysToShow)
|
||||
},
|
||||
dayLabels() {
|
||||
return [
|
||||
{
|
||||
@@ -193,46 +191,59 @@ export default {
|
||||
buildData() {
|
||||
this.data = []
|
||||
|
||||
var maxValue = 0
|
||||
var minValue = 0
|
||||
Object.values(this.daysListening).forEach((val) => {
|
||||
if (val > maxValue) maxValue = val
|
||||
if (!minValue || val < minValue) minValue = val
|
||||
})
|
||||
let maxValue = 0
|
||||
let minValue = 0
|
||||
|
||||
const dates = []
|
||||
|
||||
const numDaysInTheLastYear = 52 * 7 + this.dayOfWeekToday
|
||||
const firstDay = this.$addDaysToToday(-numDaysInTheLastYear)
|
||||
for (let i = 0; i < numDaysInTheLastYear + 1; i++) {
|
||||
const date = i === 0 ? firstDay : this.$addDaysToDate(firstDay, i)
|
||||
const dateString = this.$formatJsDate(date, 'yyyy-MM-dd')
|
||||
|
||||
if (this.daysListening[dateString] > 0) {
|
||||
this.daysListenedInTheLastYear++
|
||||
}
|
||||
|
||||
const visibleDayIndex = i - (numDaysInTheLastYear - this.daysToShow)
|
||||
if (visibleDayIndex < 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const dateObj = {
|
||||
col: Math.floor(visibleDayIndex / 7),
|
||||
row: visibleDayIndex % 7,
|
||||
date,
|
||||
dateString,
|
||||
datePretty: this.$formatJsDate(date, 'MMM d, yyyy'),
|
||||
monthString: this.$formatJsDate(date, 'MMM'),
|
||||
dayOfMonth: Number(dateString.split('-').pop()),
|
||||
yearString: dateString.split('-').shift(),
|
||||
value: this.daysListening[dateString] || 0
|
||||
}
|
||||
dates.push(dateObj)
|
||||
|
||||
if (dateObj.value > 0) {
|
||||
if (dateObj.value > maxValue) maxValue = dateObj.value
|
||||
if (!minValue || dateObj.value < minValue) minValue = dateObj.value
|
||||
}
|
||||
}
|
||||
const range = maxValue - minValue + 0.01
|
||||
|
||||
for (let i = 0; i < this.daysToShow + 1; i++) {
|
||||
const col = Math.floor(i / 7)
|
||||
const row = i % 7
|
||||
|
||||
const date = i === 0 ? this.firstWeekStart : this.$addDaysToDate(this.firstWeekStart, i)
|
||||
const dateString = this.$formatJsDate(date, 'yyyy-MM-dd')
|
||||
const datePretty = this.$formatJsDate(date, 'MMM d, yyyy')
|
||||
const monthString = this.$formatJsDate(date, 'MMM')
|
||||
const value = this.daysListening[dateString] || 0
|
||||
const x = col * 13
|
||||
const y = row * 13
|
||||
|
||||
var bgColor = this.bgColors[0]
|
||||
var outlineColor = this.outlineColors[0]
|
||||
if (value) {
|
||||
for (const dateObj of dates) {
|
||||
let bgColor = this.bgColors[0]
|
||||
let outlineColor = this.outlineColors[0]
|
||||
if (dateObj.value) {
|
||||
outlineColor = this.outlineColors[1]
|
||||
var percentOfAvg = (value - minValue) / range
|
||||
var bgIndex = Math.floor(percentOfAvg * 4) + 1
|
||||
const percentOfAvg = (dateObj.value - minValue) / range
|
||||
const bgIndex = Math.floor(percentOfAvg * 4) + 1
|
||||
bgColor = this.bgColors[bgIndex] || 'red'
|
||||
}
|
||||
|
||||
this.data.push({
|
||||
date,
|
||||
dateString,
|
||||
datePretty,
|
||||
monthString,
|
||||
dayOfMonth: Number(dateString.split('-').pop()),
|
||||
yearString: dateString.split('-').shift(),
|
||||
value,
|
||||
col,
|
||||
row,
|
||||
style: `transform:translate(${x}px,${y}px);background-color:${bgColor};outline:1px solid ${outlineColor};outline-offset:-1px;`
|
||||
...dateObj,
|
||||
style: `transform:translate(${dateObj.col * 13}px,${dateObj.row * 13}px);background-color:${bgColor};outline:1px solid ${outlineColor};outline-offset:-1px;`
|
||||
})
|
||||
}
|
||||
|
||||
@@ -260,6 +271,7 @@ export default {
|
||||
const heatmapEl = document.getElementById('heatmap')
|
||||
this.contentWidth = heatmapEl.clientWidth
|
||||
this.maxInnerWidth = this.contentWidth - 52
|
||||
this.daysListenedInTheLastYear = 0
|
||||
this.buildData()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
|
||||
<div v-if="processing" role="img" :aria-label="$strings.MessageLoading" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
|
||||
<widgets-loading-spinner />
|
||||
</div>
|
||||
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" />
|
||||
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" :aria-label="$getString('LabelPersonalYearReview', [variant + 1])" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<p class="hidden md:block text-xl font-semibold">{{ $getString('HeaderYearReview', [yearInReviewYear]) }}</p>
|
||||
<h1 class="hidden md:block text-xl font-semibold">{{ $getString('HeaderYearReview', [yearInReviewYear]) }}</h1>
|
||||
<div class="hidden md:block flex-grow" />
|
||||
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide : $strings.LabelYearReviewShow }}</ui-btn>
|
||||
</div>
|
||||
@@ -16,17 +16,22 @@
|
||||
<div v-if="showYearInReview">
|
||||
<div class="w-full h-px bg-slate-200/10 my-4" />
|
||||
|
||||
<div class="flex items-center justify-center mb-2 max-w-[800px] mx-auto">
|
||||
<div v-if="availableYears.length > 1" class="mb-2 py-2 max-w-[800px] mx-auto">
|
||||
<!-- year selector -->
|
||||
<ui-dropdown v-model="yearInReviewYear" small :items="availableYears" :disabled="processingYearInReview" class="max-w-24" @input="yearInReviewYearChanged" />
|
||||
</div>
|
||||
|
||||
<div role="toolbar" class="flex items-center justify-center mb-2 max-w-[800px] mx-auto">
|
||||
<!-- previous button -->
|
||||
<ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--">
|
||||
<ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" :aria-label="$strings.ButtonPrevious" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--">
|
||||
<span class="material-symbols text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
||||
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
|
||||
</ui-btn>
|
||||
<!-- share button -->
|
||||
<ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview">{{ $strings.ButtonShare }} </ui-btn>
|
||||
<ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview">{{ $strings.ButtonShare }} </ui-btn>
|
||||
|
||||
<div class="flex-grow" />
|
||||
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelPersonalYearReview', [yearInReviewVariant + 1]) }}</p>
|
||||
<h2 class="hidden sm:block text-lg font-semibold">{{ $getString('LabelPersonalYearReview', [yearInReviewVariant + 1]) }}</h2>
|
||||
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p>
|
||||
<div class="flex-grow" />
|
||||
|
||||
@@ -36,7 +41,7 @@
|
||||
<span class="material-symbols sm:!hidden text-lg py-px">refresh</span>
|
||||
</ui-btn>
|
||||
<!-- next button -->
|
||||
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
|
||||
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" :aria-label="$strings.ButtonNext" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
|
||||
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
|
||||
<span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
||||
</ui-btn>
|
||||
@@ -46,23 +51,23 @@
|
||||
<!-- your year in review short -->
|
||||
<div class="w-full max-w-[800px] mx-auto my-4">
|
||||
<!-- share button -->
|
||||
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex sm:hidden items-center font-semibold mb-1" @click="shareYearInReviewShort">{{ $strings.ButtonShare }}</ui-btn>
|
||||
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex items-center font-semibold mb-1" @click="shareYearInReviewShort">{{ $strings.ButtonShare }}</ui-btn>
|
||||
<stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" />
|
||||
</div>
|
||||
|
||||
<!-- your server in review -->
|
||||
<div v-if="isAdminOrUp" class="w-full max-w-[800px] mx-auto mb-2 mt-4 border-t pt-4 border-white/10">
|
||||
<div v-if="isAdminOrUp" role="toolbar" class="w-full max-w-[800px] mx-auto mb-2 mt-4 border-t pt-4 border-white/10">
|
||||
<div class="flex items-center justify-center mb-2">
|
||||
<!-- previous button -->
|
||||
<ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--">
|
||||
<ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" :aria-label="$strings.ButtonPrevious" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--">
|
||||
<span class="material-symbols text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
||||
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
|
||||
</ui-btn>
|
||||
<!-- share button -->
|
||||
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer">{{ $strings.ButtonShare }} </ui-btn>
|
||||
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer">{{ $strings.ButtonShare }} </ui-btn>
|
||||
|
||||
<div class="flex-grow" />
|
||||
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelServerYearReview', [yearInReviewServerVariant + 1]) }}</p>
|
||||
<h2 class="hidden sm:block text-lg font-semibold">{{ $getString('LabelServerYearReview', [yearInReviewServerVariant + 1]) }}</h2>
|
||||
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p>
|
||||
<div class="flex-grow" />
|
||||
|
||||
@@ -72,7 +77,7 @@
|
||||
<span class="material-symbols sm:!hidden text-lg py-px">refresh</span>
|
||||
</ui-btn>
|
||||
<!-- next button -->
|
||||
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
|
||||
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" :aria-label="$strings.ButtonNext" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
|
||||
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
|
||||
<span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
||||
</ui-btn>
|
||||
@@ -88,6 +93,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
showYearInReview: false,
|
||||
availableYears: [],
|
||||
yearInReviewYear: 0,
|
||||
yearInReviewVariant: 0,
|
||||
yearInReviewServerVariant: 0,
|
||||
@@ -100,6 +106,9 @@ export default {
|
||||
computed: {
|
||||
isAdminOrUp() {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
user() {
|
||||
return this.$store.state.user.user
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -112,25 +121,57 @@ export default {
|
||||
shareYearInReviewShort() {
|
||||
this.$refs.yearInReviewShort.share()
|
||||
},
|
||||
yearInReviewYearChanged() {
|
||||
this.$nextTick(() => {
|
||||
this.refreshYearInReview()
|
||||
this.refreshYearInReviewServer()
|
||||
})
|
||||
},
|
||||
refreshYearInReviewServer() {
|
||||
this.$refs.yearInReviewServer.refresh()
|
||||
if (this.$refs.yearInReviewServer != null) {
|
||||
this.$refs.yearInReviewServer.refresh()
|
||||
}
|
||||
},
|
||||
refreshYearInReview() {
|
||||
this.$refs.yearInReview.refresh()
|
||||
this.$refs.yearInReviewShort.refresh()
|
||||
if (this.$refs.yearInReview != null && this.$refs.yearInReviewShort != null) {
|
||||
this.$refs.yearInReview.refresh()
|
||||
this.$refs.yearInReviewShort.refresh()
|
||||
}
|
||||
},
|
||||
clickShowYearInReview() {
|
||||
this.showYearInReview = !this.showYearInReview
|
||||
},
|
||||
getAvailableYears() {
|
||||
if (this.user) {
|
||||
const oldestDate = this.user.createdAt
|
||||
if (oldestDate) {
|
||||
const date = new Date(oldestDate)
|
||||
const oldestYear = date.getFullYear()
|
||||
const currentYear = new Date().getFullYear()
|
||||
|
||||
const years = []
|
||||
for (let year = currentYear; year >= oldestYear; year--) {
|
||||
years.push({ value: year, text: year.toString() })
|
||||
}
|
||||
|
||||
return years
|
||||
}
|
||||
}
|
||||
// Fallback on error
|
||||
return [{ value: this.yearInReviewYear, text: this.yearInReviewYear.toString() }]
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.yearInReviewYear = new Date().getFullYear()
|
||||
|
||||
// When not December show previous year
|
||||
if (new Date().getMonth() < 11) {
|
||||
this.yearInReviewYear--
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.availableYears = this.getAvailableYears()
|
||||
|
||||
if (typeof navigator.share !== 'undefined' && navigator.share) {
|
||||
this.showShareButton = true
|
||||
} else {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
|
||||
<div v-if="processing" role="img" :aria-label="$strings.MessageLoading" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
|
||||
<widgets-loading-spinner />
|
||||
</div>
|
||||
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" />
|
||||
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" :aria-label="$getString('LabelServerYearReview', [variant + 1])" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -218,7 +218,6 @@ export default {
|
||||
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
|
||||
} else {
|
||||
console.log(`Item removed from playlist`, updatedPlaylist)
|
||||
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -96,7 +96,7 @@ export default {
|
||||
return this.episode?.title || ''
|
||||
},
|
||||
episodeSubtitle() {
|
||||
return this.episode?.subtitle || ''
|
||||
return this.episode?.subtitle || this.episode?.description || ''
|
||||
},
|
||||
episodeType() {
|
||||
return this.episode?.episodeType || ''
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
|
||||
</form>
|
||||
</div>
|
||||
<div class="relative min-h-[176px]">
|
||||
<div class="relative min-h-44">
|
||||
<template v-for="episode in totalEpisodes">
|
||||
<div :key="episode" :id="`episode-${episode - 1}`" class="w-full h-44 px-2 py-3 overflow-hidden relative border-b border-white/10">
|
||||
<!-- episode is mounted here -->
|
||||
@@ -39,7 +39,7 @@
|
||||
<div v-if="isSearching" class="w-full h-full absolute inset-0 flex justify-center py-12" :class="{ 'bg-black/50': totalEpisodes }">
|
||||
<ui-loading-indicator />
|
||||
</div>
|
||||
<div v-else-if="!totalEpisodes" class="h-44 flex items-center justify-center">
|
||||
<div v-else-if="!totalEpisodes" id="no-episodes" class="h-44 flex items-center justify-center">
|
||||
<p class="text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,7 +80,8 @@ export default {
|
||||
episodeComponentRefs: {},
|
||||
windowHeight: 0,
|
||||
episodesTableOffsetTop: 0,
|
||||
episodeRowHeight: 176
|
||||
episodeRowHeight: 44 * 4, // h-44,
|
||||
currScrollTop: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -484,9 +485,8 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
scroll(evt) {
|
||||
if (!evt?.target?.scrollTop) return
|
||||
const scrollTop = Math.max(evt.target.scrollTop - this.episodesTableOffsetTop, 0)
|
||||
handleScroll() {
|
||||
const scrollTop = this.currScrollTop
|
||||
let firstEpisodeIndex = Math.floor(scrollTop / this.episodeRowHeight)
|
||||
let lastEpisodeIndex = Math.ceil((scrollTop + this.windowHeight) / this.episodeRowHeight)
|
||||
lastEpisodeIndex = Math.min(this.totalEpisodes - 1, lastEpisodeIndex)
|
||||
@@ -501,6 +501,12 @@ export default {
|
||||
})
|
||||
this.mountEpisodes(firstEpisodeIndex, lastEpisodeIndex + 1)
|
||||
},
|
||||
scroll(evt) {
|
||||
if (!evt?.target?.scrollTop) return
|
||||
const scrollTop = Math.max(evt.target.scrollTop - this.episodesTableOffsetTop, 0)
|
||||
this.currScrollTop = scrollTop
|
||||
this.handleScroll()
|
||||
},
|
||||
initListeners() {
|
||||
const itemPageWrapper = document.getElementById('item-page-wrapper')
|
||||
if (itemPageWrapper) {
|
||||
@@ -532,11 +538,24 @@ export default {
|
||||
this.episodesTableOffsetTop = (lazyEpisodesTableEl?.offsetTop || 0) + 64
|
||||
|
||||
this.windowHeight = window.innerHeight
|
||||
this.episodesPerPage = Math.ceil(this.windowHeight / this.episodeRowHeight)
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.mountEpisodes(0, Math.min(this.episodesPerPage, this.totalEpisodes))
|
||||
this.recalcEpisodeRowHeight()
|
||||
this.episodesPerPage = Math.ceil(this.windowHeight / this.episodeRowHeight)
|
||||
// Maybe update currScrollTop if items were removed
|
||||
const itemPageWrapper = document.getElementById('item-page-wrapper')
|
||||
const { scrollHeight, clientHeight } = itemPageWrapper
|
||||
const maxScrollTop = scrollHeight - clientHeight
|
||||
this.currScrollTop = Math.min(this.currScrollTop, maxScrollTop)
|
||||
this.handleScroll()
|
||||
})
|
||||
},
|
||||
recalcEpisodeRowHeight() {
|
||||
const episodeRowEl = document.getElementById('episode-0') || document.getElementById('no-episodes')
|
||||
if (episodeRowEl) {
|
||||
const height = getComputedStyle(episodeRowEl).height
|
||||
this.episodeRowHeight = parseInt(height) || this.episodeRowHeight
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
|
||||
<slot :disabled="disabled" :showMenu="showMenu" :clickShowMenu="clickShowMenu" :processing="processing">
|
||||
<button v-if="!processing" type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||
<button v-if="!processing" type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" :aria-label="$strings.LabelMore" aria-haspopup="menu" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||
<span class="material-symbols text-2xl" :class="iconClass"></span>
|
||||
</button>
|
||||
<div v-else class="h-full w-full flex items-center justify-center">
|
||||
@@ -10,12 +10,12 @@
|
||||
</slot>
|
||||
|
||||
<transition name="menu">
|
||||
<div v-show="showMenu" ref="menuWrapper" class="absolute right-0 mt-1 z-10 bg-bg border border-black-200 shadow-lg rounded-md py-1 focus:outline-none sm:text-sm" :style="{ width: menuWidth + 'px' }">
|
||||
<div v-show="showMenu" ref="menuWrapper" role="menu" class="absolute right-0 mt-1 z-10 bg-bg border border-black-200 shadow-lg rounded-md py-1 focus:outline-none sm:text-sm" :style="{ width: menuWidth + 'px' }">
|
||||
<template v-for="(item, index) in items">
|
||||
<template v-if="item.subitems">
|
||||
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-default" :class="{ 'bg-white/5': mouseoverItemIndex == index }" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop>
|
||||
<button :key="index" role="menuitem" aria-haspopup="menu" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-default w-full" :class="{ 'bg-white/5': mouseoverItemIndex == index }" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop>
|
||||
<p>{{ item.text }}</p>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
v-if="mouseoverItemIndex === index"
|
||||
:key="`subitems-${index}`"
|
||||
@@ -25,14 +25,14 @@
|
||||
:class="openSubMenuLeft ? 'rounded-l-md' : 'rounded-r-md'"
|
||||
:style="{ left: submenuLeftPos + 'px', top: index * 28 + 'px', width: submenuWidth + 'px' }"
|
||||
>
|
||||
<div v-for="(subitem, subitemindex) in item.subitems" :key="`subitem-${subitemindex}`" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer" @click.stop="clickAction(subitem.action, subitem.data)">
|
||||
<button v-for="(subitem, subitemindex) in item.subitems" :key="`subitem-${subitemindex}`" role="menuitem" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer w-full" @click.stop="clickAction(subitem.action, subitem.data)">
|
||||
<p>{{ subitem.text }}</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.action)">
|
||||
<button v-else :key="index" role="menuitem" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer w-full" @click.stop="clickAction(item.action)">
|
||||
<p class="text-left">{{ item.text }}</p>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="relative w-full" v-click-outside="clickOutsideObj">
|
||||
<p v-if="label" class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
||||
<button type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||
<button type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left sm:text-sm" :class="buttonClass" aria-haspopup="menu" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||
<span class="flex items-center">
|
||||
<span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small }">{{ selectedText }}</span>
|
||||
<span v-if="selectedSubtext">: </span>
|
||||
@@ -13,9 +13,9 @@
|
||||
</button>
|
||||
|
||||
<transition name="menu">
|
||||
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="listbox" :style="{ maxHeight: menuMaxHeight }">
|
||||
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="menu" :style="{ maxHeight: menuMaxHeight }">
|
||||
<template v-for="item in itemsToShow">
|
||||
<li :key="item.value" class="text-gray-100 relative py-2 cursor-pointer hover:bg-black-400" :id="'listbox-option-' + item.value" role="option" tabindex="0" @keyup.enter="clickedOption(item.value)" @click="clickedOption(item.value)">
|
||||
<li :key="item.value" class="text-gray-100 relative py-2 cursor-pointer hover:bg-black-400" role="menuitem" tabindex="0" @keyup.enter="clickedOption(item.value)" @click="clickedOption(item.value)">
|
||||
<div class="flex items-center">
|
||||
<span class="ml-3 block truncate font-sans text-sm" :class="{ 'font-semibold': item.subtext }">{{ item.text }}</span>
|
||||
<span v-if="item.subtext">: </span>
|
||||
@@ -119,4 +119,4 @@ export default {
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<button class="icon-btn rounded-md flex items-center justify-center relative" @mousedown.prevent :disabled="disabled || loading" :class="className" @click="clickBtn">
|
||||
<button :aria-label="ariaLabel" class="icon-btn rounded-md flex items-center justify-center relative" @mousedown.prevent :disabled="disabled || loading" :class="className" @click="clickBtn">
|
||||
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
||||
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||
@@ -28,7 +28,8 @@ export default {
|
||||
size: {
|
||||
type: Number,
|
||||
default: 9
|
||||
}
|
||||
},
|
||||
ariaLabel: String
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
type="button"
|
||||
:disabled="disabled"
|
||||
class="w-10 sm:w-full relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200"
|
||||
aria-haspopup="listbox"
|
||||
aria-haspopup="menu"
|
||||
:aria-expanded="showMenu"
|
||||
:aria-label="$strings.ButtonLibrary + ': ' + currentLibrary.name"
|
||||
@click.stop.prevent="clickShowMenu"
|
||||
@@ -16,9 +16,9 @@
|
||||
</button>
|
||||
|
||||
<transition name="menu">
|
||||
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full min-w-48 bg-primary border border-black-200 shadow-lg rounded-b-md py-1 overflow-auto focus:outline-none sm:text-sm librariesDropdownMenu" tabindex="-1" role="listbox">
|
||||
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full min-w-48 bg-primary border border-black-200 shadow-lg rounded-b-md py-1 overflow-auto focus:outline-none sm:text-sm librariesDropdownMenu" tabindex="-1" role="menu">
|
||||
<template v-for="library in librariesFiltered">
|
||||
<li :key="library.id" class="text-gray-400 hover:text-white relative py-2 cursor-pointer hover:bg-black-400" role="option" tabindex="0" @keydown.enter="selectLibrary(library)" @click="selectLibrary(library)">
|
||||
<li :key="library.id" class="text-gray-400 hover:text-white relative py-2 cursor-pointer hover:bg-black-400" role="menuitem" tabindex="0" @keydown.enter="selectLibrary(library)" @click="selectLibrary(library)">
|
||||
<div class="flex items-center px-2">
|
||||
<ui-library-icon :icon="library.icon" class="mr-1.5" />
|
||||
<span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p>
|
||||
<label :for="identifier" class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</label>
|
||||
<div ref="wrapper" class="relative">
|
||||
<form @submit.prevent="submitForm">
|
||||
<div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-1" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
||||
<div v-for="item in selected" :key="item" class="rounded-full px-2 py-1 mx-0.5 my-0.5 text-xs bg-bg flex flex-nowrap break-all items-center relative">
|
||||
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 px-1 bg-bg bg-opacity-75 flex items-center justify-end opacity-0 hover:opacity-100">
|
||||
<span v-if="showEdit" class="material-symbols text-white hover:text-warning cursor-pointer" style="font-size: 1.1rem" @click.stop="editItem(item)">edit</span>
|
||||
<span class="material-symbols text-white hover:text-error cursor-pointer" style="font-size: 1.1rem" @click.stop="removeItem(item)">close</span>
|
||||
<div ref="inputWrapper" role="list" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-1" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
||||
<div v-for="item in selected" :key="item" role="listitem" class="rounded-full px-2 py-1 mx-0.5 my-0.5 text-xs bg-bg flex flex-nowrap break-all items-center relative">
|
||||
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 px-1 bg-bg bg-opacity-75 flex items-center justify-end opacity-0 hover:opacity-100" :class="{ 'opacity-100': inputFocused }">
|
||||
<button v-if="showEdit" type="button" :aria-label="$strings.ButtonEdit" class="material-symbols text-white hover:text-warning cursor-pointer" style="font-size: 1.1rem" @click.stop="editItem(item)">edit</button>
|
||||
<button type="button" :aria-label="$strings.ButtonRemove" class="material-symbols text-white hover:text-error focus:text-error cursor-pointer" style="font-size: 1.1rem" @click.stop="removeItem(item)" @keydown.enter.stop.prevent="removeItem(item)" @focus="setInputFocused(true)" @blur="setInputFocused(false)" tabindex="0">close</button>
|
||||
</div>
|
||||
{{ item }}
|
||||
</div>
|
||||
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
|
||||
<input v-show="!readonly" v-model="textInput" ref="input" :id="identifier" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -66,7 +66,8 @@ export default {
|
||||
typingTimeout: null,
|
||||
isFocused: false,
|
||||
menu: null,
|
||||
filteredItems: null
|
||||
filteredItems: null,
|
||||
inputFocused: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -100,6 +101,9 @@ export default {
|
||||
}
|
||||
|
||||
return this.filteredItems
|
||||
},
|
||||
identifier() {
|
||||
return Math.random().toString(36).substring(2)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -129,6 +133,9 @@ export default {
|
||||
}, 100)
|
||||
this.setInputWidth()
|
||||
},
|
||||
setInputFocused(focused) {
|
||||
this.inputFocused = focused
|
||||
},
|
||||
setInputWidth() {
|
||||
setTimeout(() => {
|
||||
var value = this.$refs.input.value
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p>
|
||||
<label :for="identifier" class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</label>
|
||||
<div ref="wrapper" class="relative">
|
||||
<form @submit.prevent="submitForm">
|
||||
<div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-0.5" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
||||
<div v-for="item in selected" :key="item.id" class="rounded-full px-2 py-0.5 m-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center justify-center relative min-w-12">
|
||||
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer">
|
||||
<span v-if="showEdit" class="material-symbols text-base text-white hover:text-warning mr-1" @click.stop="editItem(item)">edit</span>
|
||||
<span class="material-symbols text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item.id)">close</span>
|
||||
<div ref="inputWrapper" role="list" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-0.5" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
||||
<div v-for="item in selected" :key="item.id" role="listitem" class="rounded-full px-2 py-0.5 m-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center justify-center relative min-w-12">
|
||||
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer" :class="{ 'opacity-100': inputFocused }">
|
||||
<button v-if="showEdit" type="button" :aria-label="$strings.ButtonEdit" class="material-symbols text-base text-white hover:text-warning focus:text-warning mr-1" @click.stop="editItem(item)" @keydown.enter.stop.prevent="editItem(item)" @focus="setInputFocused(true)" @blur="setInputFocused(false)" tabindex="0">edit</button>
|
||||
<button type="button" :aria-label="$strings.ButtonRemove" class="material-symbols text-white hover:text-error focus:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item.id)" @keydown.enter.stop="removeItem(item.id)" @focus="setInputFocused(true)" @blur="setInputFocused(false)" tabindex="0">close</button>
|
||||
</div>
|
||||
{{ item[textKey] }}
|
||||
</div>
|
||||
<div v-if="showEdit && !disabled" class="rounded-full cursor-pointer w-6 h-6 mx-0.5 bg-bg flex items-center justify-center">
|
||||
<span class="material-symbols text-white hover:text-success pt-px pr-px" style="font-size: 1.1rem" @click.stop="addItem">add</span>
|
||||
<button type="button" :aria-label="$strings.ButtonAdd" class="material-symbols text-white hover:text-success focus:text-success pt-px pr-px" style="font-size: 1.1rem" @click.stop="addItem" @keydown.enter.stop="addItem" tabindex="0">add</button>
|
||||
</div>
|
||||
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
|
||||
<input v-show="!readonly" v-model="textInput" ref="input" :id="identifier" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -65,6 +65,7 @@ export default {
|
||||
currentSearch: null,
|
||||
typingTimeout: null,
|
||||
isFocused: false,
|
||||
inputFocused: false,
|
||||
menu: null,
|
||||
items: []
|
||||
}
|
||||
@@ -102,6 +103,9 @@ export default {
|
||||
},
|
||||
filterData() {
|
||||
return this.$store.state.libraries.filterData || {}
|
||||
},
|
||||
identifier() {
|
||||
return Math.random().toString(36).substring(2)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -114,6 +118,9 @@ export default {
|
||||
getIsSelected(itemValue) {
|
||||
return !!this.selected.find((i) => i.id === itemValue)
|
||||
},
|
||||
setInputFocused(focused) {
|
||||
this.inputFocused = focused
|
||||
},
|
||||
search() {
|
||||
if (!this.textInput) return
|
||||
this.currentSearch = this.textInput
|
||||
@@ -208,6 +215,10 @@ export default {
|
||||
inputBlur() {
|
||||
if (!this.isFocused) return
|
||||
|
||||
if (typeof this.textInput === 'string') {
|
||||
this.textInput = this.textInput.trim()
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (document.activeElement === this.$refs.input) {
|
||||
return
|
||||
@@ -224,6 +235,11 @@ export default {
|
||||
},
|
||||
forceBlur() {
|
||||
this.isFocused = false
|
||||
|
||||
if (typeof this.textInput === 'string') {
|
||||
this.textInput = this.textInput.trim()
|
||||
}
|
||||
|
||||
if (this.textInput) this.submitForm()
|
||||
if (this.$refs.input) this.$refs.input.blur()
|
||||
},
|
||||
@@ -282,11 +298,12 @@ export default {
|
||||
this.selectedMenuItemIndex = null
|
||||
},
|
||||
submitForm() {
|
||||
if (!this.textInput) return
|
||||
if (!this.textInput || !this.textInput.trim?.()) return
|
||||
|
||||
this.textInput = this.textInput.trim()
|
||||
|
||||
const cleaned = this.textInput.trim()
|
||||
const matchesItem = this.items.find((i) => {
|
||||
return i.name === cleaned
|
||||
return i.name === this.textInput
|
||||
})
|
||||
|
||||
if (matchesItem) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<button class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" :class="borderless ? '' : 'bg-primary border border-gray-600'" @click="clickBtn">
|
||||
<button :aria-label="isRead ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" :class="borderless ? '' : 'bg-primary border border-gray-600'" @click="clickBtn">
|
||||
<div class="w-5 h-5 text-white relative">
|
||||
<svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
|
||||
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="default-style">
|
||||
<p v-if="label" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
|
||||
<p v-if="label" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }" style="margin-top: 0; margin-bottom: 0.125em">
|
||||
{{ label }}
|
||||
</p>
|
||||
<ui-vue-trix v-model="content" :config="config" :disabled-editor="disabled" @trix-file-accept="trixFileAccept" />
|
||||
<ui-vue-trix ref="input" v-model="content" :disabled-editor="disabled" @trix-file-accept="trixFileAccept" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -12,7 +12,10 @@ export default {
|
||||
props: {
|
||||
value: String,
|
||||
label: String,
|
||||
disabled: Boolean
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
@@ -25,49 +28,19 @@ export default {
|
||||
set(val) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
config() {
|
||||
return {
|
||||
toolbar: {
|
||||
getDefaultHTML: () => `<div class="trix-button-row">
|
||||
<span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
|
||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" title="${this.$strings.LabelFontBold}" tabindex="-1">${this.$strings.LabelFontBold}</button>
|
||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" title="${this.$strings.LabelFontItalic}" tabindex="-1">${this.$strings.LabelFontItalic}</button>
|
||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" title="${this.$strings.LabelFontStrikethrough}" tabindex="-1">${this.$strings.LabelFontStrikethrough}</button>
|
||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="${this.$strings.LabelTextEditorLink}" tabindex="-1">${this.$strings.LabelTextEditorLink}</button>
|
||||
</span>
|
||||
<span class="trix-button-group trix-button-group--block-tools" data-trix-button-group="block-tools">
|
||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" title="${this.$strings.LabelTextEditorBulletedList}" tabindex="-1">${this.$strings.LabelTextEditorBulletedList}</button>
|
||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" title="${this.$strings.LabelTextEditorNumberedList}" tabindex="-1">${this.$strings.LabelTextEditorNumberedList}</button>
|
||||
</span>
|
||||
|
||||
<span class="trix-button-group-spacer"></span>
|
||||
<span class="trix-button-group trix-button-group--history-tools" data-trix-button-group="history-tools">
|
||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" title="${this.$strings.LabelUndo}" tabindex="-1">${this.$strings.LabelUndo}</button>
|
||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" title="${this.$strings.LabelRedo}" tabindex="-1">${this.$strings.LabelRedo}</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="trix-dialogs" data-trix-dialogs>
|
||||
<div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
|
||||
<div class="trix-dialog__link-fields">
|
||||
<input type="url" name="href" class="trix-input trix-input--dialog" placeholder="" aria-label="URL" required data-trix-input>
|
||||
<div class="trix-button-group">
|
||||
<input type="button" class="trix-button trix-button--dialog" value="${this.$strings.LabelTextEditorLink}" data-trix-method="setAttribute">
|
||||
<input type="button" class="trix-button trix-button--dialog" value="${this.$strings.LabelTextEditorUnlink}" data-trix-method="removeAttribute">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
trixFileAccept(e) {
|
||||
e.preventDefault()
|
||||
},
|
||||
blur() {
|
||||
if (this.$refs.input && this.$refs.input.blur) {
|
||||
this.$refs.input.blur()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {},
|
||||
beforeDestroy() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,32 +1,14 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="relative">
|
||||
<input
|
||||
:id="inputId"
|
||||
:name="inputName"
|
||||
ref="input"
|
||||
v-model="inputValue"
|
||||
:type="actualType"
|
||||
:step="step"
|
||||
:min="min"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:placeholder="placeholder"
|
||||
dir="auto"
|
||||
class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full"
|
||||
:class="classList"
|
||||
@keyup="keyup"
|
||||
@change="change"
|
||||
@focus="focused"
|
||||
@blur="blurred"
|
||||
/>
|
||||
<input :id="inputId" :name="inputName" ref="input" v-model="inputValue" :type="actualType" :step="step" :min="min" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" dir="auto" class="rounded bg-primary text-gray-200 focus:bg-bg focus:outline-none border h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
|
||||
<div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
|
||||
<span class="material-symbols text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
|
||||
</div>
|
||||
<div v-if="type === 'password' && isHovering" class="absolute top-0 right-0 h-full px-4 flex items-center justify-center">
|
||||
<span class="material-symbols text-gray-400 cursor-pointer text-lg" @click.stop.prevent="showPassword = !showPassword">{{ !showPassword ? 'visibility' : 'visibility_off' }}</span>
|
||||
</div>
|
||||
<div v-else-if="showCopy" class="absolute top-0 right-0 h-full px-4 flex items-center justify-center">
|
||||
<span class="material-symbols text-gray-400 cursor-pointer text-lg" @click.stop.prevent="copyToClipboard">{{ !hasCopied ? 'content_copy' : 'done' }}</span>
|
||||
<div v-else-if="showCopy" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
|
||||
<span class="material-symbols cursor-pointer text-lg" :class="hasCopied ? 'text-success' : 'text-gray-400 hover:text-white'" @click.stop.prevent="copyToClipboard">{{ !hasCopied ? 'content_copy' : 'done' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -58,14 +40,16 @@ export default {
|
||||
showCopy: Boolean,
|
||||
step: [String, Number],
|
||||
min: [String, Number],
|
||||
customInputClass: String
|
||||
customInputClass: String,
|
||||
trimWhitespace: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showPassword: false,
|
||||
isHovering: false,
|
||||
isFocused: false,
|
||||
hasCopied: false
|
||||
hasCopied: null,
|
||||
isInvalidDate: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -79,11 +63,20 @@ export default {
|
||||
},
|
||||
classList() {
|
||||
var _list = []
|
||||
_list.push(`px-${this.paddingX}`)
|
||||
if (this.showCopy) {
|
||||
_list.push('pl-3', 'pr-8')
|
||||
} else {
|
||||
_list.push(`px-${this.paddingX}`)
|
||||
}
|
||||
|
||||
_list.push(`py-${this.paddingY}`)
|
||||
if (this.noSpinner) _list.push('no-spinner')
|
||||
if (this.textCenter) _list.push('text-center')
|
||||
if (this.customInputClass) _list.push(this.customInputClass)
|
||||
|
||||
if (this.isInvalidDate) _list.push('border-error')
|
||||
else _list.push('focus:border-gray-300 border-gray-600')
|
||||
|
||||
return _list.join(' ')
|
||||
},
|
||||
actualType() {
|
||||
@@ -93,11 +86,10 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard() {
|
||||
if (this.hasCopied) return
|
||||
clearTimeout(this.hasCopied)
|
||||
this.$copyToClipboard(this.inputValue).then((success) => {
|
||||
this.hasCopied = success
|
||||
setTimeout(() => {
|
||||
this.hasCopied = false
|
||||
this.hasCopied = setTimeout(() => {
|
||||
this.hasCopied = null
|
||||
}, 2000)
|
||||
})
|
||||
},
|
||||
@@ -110,14 +102,26 @@ export default {
|
||||
this.$emit('focus')
|
||||
},
|
||||
blurred() {
|
||||
if (this.trimWhitespace && typeof this.inputValue === 'string') {
|
||||
this.inputValue = this.inputValue.trim()
|
||||
}
|
||||
this.isFocused = false
|
||||
this.$emit('blur')
|
||||
},
|
||||
|
||||
change(e) {
|
||||
this.$emit('change', e.target.value)
|
||||
},
|
||||
keyup(e) {
|
||||
this.$emit('keyup', e)
|
||||
|
||||
if (this.type === 'datetime-local') {
|
||||
if (e.target.validity?.badInput) {
|
||||
this.isInvalidDate = true
|
||||
} else {
|
||||
this.isInvalidDate = false
|
||||
}
|
||||
}
|
||||
},
|
||||
blur() {
|
||||
if (this.$refs.input) this.$refs.input.blur()
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<slot>
|
||||
<label :for="identifier" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }"
|
||||
>{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em></label
|
||||
>
|
||||
<label :for="identifier" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
|
||||
{{ label }}
|
||||
<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
|
||||
</label>
|
||||
</slot>
|
||||
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" :class="inputClass" @blur="inputBlurred" />
|
||||
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :show-copy="showCopy" class="w-full" :class="inputClass" :trim-whitespace="trimWhitespace" @blur="inputBlurred" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -22,7 +23,9 @@ export default {
|
||||
},
|
||||
readonly: Boolean,
|
||||
disabled: Boolean,
|
||||
inputClass: String
|
||||
inputClass: String,
|
||||
showCopy: Boolean,
|
||||
trimWhitespace: Boolean
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
@@ -57,4 +60,4 @@ export default {
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<button :aria-labelledby="labeledBy" role="checkbox" type="button" class="border rounded-full border-black-100 flex items-center cursor-pointer justify-start" :style="{ width: buttonWidth + 'px' }" :aria-checked="toggleValue" :class="className" @click="clickToggle">
|
||||
<button :aria-labelledby="labeledBy" :aria-label="label" role="checkbox" type="button" class="border rounded-full border-black-100 flex items-center cursor-pointer justify-start" :style="{ width: buttonWidth + 'px' }" :aria-checked="toggleValue" :class="className" @click="clickToggle">
|
||||
<span class="rounded-full border border-black-50 shadow transform transition-transform duration-100" :style="{ width: cursorHeightWidth + 'px', height: cursorHeightWidth + 'px' }" :class="switchClassName"></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -20,6 +20,7 @@ export default {
|
||||
},
|
||||
disabled: Boolean,
|
||||
labeledBy: String,
|
||||
label: String,
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md'
|
||||
|
||||
@@ -1,6 +1,37 @@
|
||||
<template>
|
||||
<div>
|
||||
<trix-editor :contenteditable="!disabledEditor" :class="['trix-content']" ref="trix" :input="computedId" :placeholder="placeholder" @trix-change="handleContentChange" @trix-initialize="handleInitialize" @trix-focus="processTrixFocus" @trix-blur="processTrixBlur" />
|
||||
<trix-toolbar :id="toolbarId">
|
||||
<div v-show="!disabledEditor" class="trix-button-row">
|
||||
<span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
|
||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" :title="$strings.LabelFontBold" tabindex="-1">{{ $strings.LabelFontBold }}</button>
|
||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" :title="$strings.LabelFontItalic" tabindex="-1">{{ $strings.LabelFontItalic }}</button>
|
||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" :title="$strings.LabelFontStrikethrough" tabindex="-1">{{ $strings.LabelFontStrikethrough }}</button>
|
||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" :title="$strings.LabelTextEditorLink" tabindex="-1">{{ $strings.LabelTextEditorLink }}</button>
|
||||
</span>
|
||||
<span class="trix-button-group trix-button-group--block-tools" data-trix-button-group="block-tools">
|
||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" :title="$strings.LabelTextEditorBulletedList" tabindex="-1">{{ $strings.LabelTextEditorBulletedList }}</button>
|
||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" :title="$strings.LabelTextEditorNumberedList" tabindex="-1">{{ $strings.LabelTextEditorNumberedList }}</button>
|
||||
</span>
|
||||
|
||||
<span class="trix-button-group-spacer"></span>
|
||||
<span class="trix-button-group trix-button-group--history-tools" data-trix-button-group="history-tools">
|
||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" :title="$strings.LabelUndo" tabindex="-1">{{ $strings.LabelUndo }}</button>
|
||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" :title="$strings.LabelRedo" tabindex="-1">{{ $strings.LabelRedo }}</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="trix-dialogs" data-trix-dialogs>
|
||||
<div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
|
||||
<div class="trix-dialog__link-fields">
|
||||
<input type="url" name="href" class="trix-input trix-input--dialog" placeholder="" aria-label="URL" required data-trix-input />
|
||||
<div class="trix-button-group">
|
||||
<input type="button" class="trix-button trix-button--dialog" :value="$strings.LabelTextEditorLink" data-trix-method="setAttribute" />
|
||||
<input type="button" class="trix-button trix-button--dialog" :value="$strings.LabelTextEditorUnlink" data-trix-method="removeAttribute" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</trix-toolbar>
|
||||
<trix-editor :toolbar="toolbarId" :contenteditable="!disabledEditor" :class="['trix-content']" ref="trix" :input="computedId" :placeholder="placeholder" @trix-change="handleContentChange" @trix-initialize="handleInitialize" @trix-focus="processTrixFocus" @trix-blur="processTrixBlur" />
|
||||
<input type="hidden" :name="inputName" :id="computedId" :value="editorContent" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -14,6 +45,30 @@
|
||||
import Trix from 'trix'
|
||||
import '@/assets/trix.css'
|
||||
|
||||
function enableBreakParagraphOnReturn() {
|
||||
// Trix works with divs by default, we want paragraphs instead
|
||||
Trix.config.blockAttributes.default.tagName = 'p'
|
||||
// Enable break paragraph on Enter (Shift + Enter will still create a line break)
|
||||
Trix.config.blockAttributes.default.breakOnReturn = true
|
||||
|
||||
// Hack to fix buggy paragraph breaks
|
||||
// Copied from https://github.com/basecamp/trix/issues/680#issuecomment-735742942
|
||||
Trix.Block.prototype.breaksOnReturn = function () {
|
||||
const attr = this.getLastAttribute()
|
||||
const config = Trix.getBlockConfig(attr ? attr : 'default')
|
||||
return config ? config.breakOnReturn : false
|
||||
}
|
||||
Trix.LineBreakInsertion.prototype.shouldInsertBlockBreak = function () {
|
||||
if (this.block.hasAttributes() && this.block.isListItem() && !this.block.isEmpty()) {
|
||||
return this.startLocation.offset > 0
|
||||
} else {
|
||||
return !this.shouldBreakFormattedBlock() ? this.breaksOnReturn : false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enableBreakParagraphOnReturn()
|
||||
|
||||
export default {
|
||||
name: 'vue-trix',
|
||||
model: {
|
||||
@@ -134,6 +189,9 @@ export default {
|
||||
* Compute a random id of hidden input
|
||||
* when it haven't been specified.
|
||||
*/
|
||||
toolbarId() {
|
||||
return `trix-toolbar-${this.generateId}`
|
||||
},
|
||||
generateId() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
var r = (Math.random() * 16) | 0
|
||||
@@ -223,13 +281,17 @@ export default {
|
||||
decorateDisabledEditor(editorState) {
|
||||
/** Disable toolbar and editor by pointer events styling */
|
||||
if (editorState) {
|
||||
this.$refs.trix.toolbarElement.style['pointer-events'] = 'none'
|
||||
this.$refs.trix.disabled = true
|
||||
this.$refs.trix.contentEditable = false
|
||||
this.$refs.trix.style['background'] = '#e9ecef'
|
||||
this.$refs.trix.style['pointer-events'] = 'none'
|
||||
this.$refs.trix.style['background-color'] = '#444'
|
||||
this.$refs.trix.style['color'] = '#bbb'
|
||||
} else {
|
||||
this.$refs.trix.toolbarElement.style['pointer-events'] = 'unset'
|
||||
this.$refs.trix.disabled = false
|
||||
this.$refs.trix.contentEditable = true
|
||||
this.$refs.trix.style['pointer-events'] = 'unset'
|
||||
this.$refs.trix.style['background'] = 'transparent'
|
||||
this.$refs.trix.style['background-color'] = ''
|
||||
this.$refs.trix.style['color'] = ''
|
||||
}
|
||||
},
|
||||
overrideConfig(config) {
|
||||
@@ -249,6 +311,11 @@ export default {
|
||||
}
|
||||
}
|
||||
return target
|
||||
},
|
||||
blur() {
|
||||
if (this.$refs.trix && this.$refs.trix.blur) {
|
||||
this.$refs.trix.blur()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -283,4 +350,14 @@ export default {
|
||||
.trix_container .trix-content {
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
trix-editor {
|
||||
height: calc(4 * 1lh);
|
||||
min-height: calc(4 * 1lh);
|
||||
overflow-y: auto;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
trix-editor * {
|
||||
pointer-events: inherit;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
<form class="w-full h-full px-2 md:px-4 py-6" @submit.prevent="submitForm">
|
||||
<div class="flex flex-wrap -mx-1">
|
||||
<div class="w-full md:w-1/2 px-1">
|
||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" :label="$strings.LabelSubtitle" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" :label="$strings.LabelSubtitle" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" />
|
||||
<ui-rich-text-editor ref="descriptionInput" v-model="details.description" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" />
|
||||
|
||||
<div class="flex flex-wrap mt-2 -mx-1">
|
||||
<div class="w-full md:w-1/2 px-1">
|
||||
@@ -42,19 +42,19 @@
|
||||
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" :label="$strings.LabelNarrators" :items="narrators" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap mt-2 -mx-1">
|
||||
<div class="w-full md:w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
|
||||
<div class="flex justify-center">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent>
|
||||
<div aria-hidden="true" class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent>
|
||||
<span class="material-symbols" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize" aria-label="Decrease Cover Size" role="button"></span>
|
||||
<p class="px-2 font-mono" style="font-size: 1rem">{{ bookCoverWidth }}</p>
|
||||
<span class="material-symbols" :class="selectedSizeIndex === availableSizes.length - 1 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="increaseSize" aria-label="Increase Cover Size" role="button"></span>
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
<div class="flex items-center py-3e">
|
||||
<slot />
|
||||
<div class="flex-grow" />
|
||||
<button cy-id="leftScrollButton" v-if="isScrollable" class="w-8e h-8e mx-1e flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
|
||||
<button cy-id="leftScrollButton" v-if="isScrollable" :aria-label="$strings.ButtonScrollLeft" class="w-8e h-8e mx-1e flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
|
||||
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">chevron_left</span>
|
||||
</button>
|
||||
<button cy-id="rightScrollButton" v-if="isScrollable" class="w-8e h-8e mx-1e flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
|
||||
<button cy-id="rightScrollButton" v-if="isScrollable" :aria-label="$strings.ButtonScrollRight" class="w-8e h-8e mx-1e flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
|
||||
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -124,6 +124,7 @@ export default {
|
||||
this.updateSelectionMode(false)
|
||||
},
|
||||
editEpisode({ libraryItem, episode }) {
|
||||
this.$store.commit('setEpisodeTableEpisodeIds', [episode.id])
|
||||
this.$store.commit('setSelectedLibraryItem', libraryItem)
|
||||
this.$store.commit('globals/setSelectedEpisode', episode)
|
||||
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
<form class="w-full h-full px-4 py-6" @submit.prevent="submitForm">
|
||||
<div class="flex -mx-1">
|
||||
<div class="w-1/2 px-1">
|
||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1">
|
||||
<ui-text-input-with-label ref="authorInput" v-model="details.author" :label="$strings.LabelAuthor" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="authorInput" v-model="details.author" :label="$strings.LabelAuthor" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" :label="$strings.LabelRSSFeedURL" class="mt-2" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" :label="$strings.LabelRSSFeedURL" trim-whitespace class="mt-2" @input="handleInputChange" />
|
||||
|
||||
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" />
|
||||
|
||||
@@ -25,13 +25,13 @@
|
||||
|
||||
<div class="flex mt-2 -mx-1">
|
||||
<div class="w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" :label="$strings.LabelReleaseDate" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" :label="$strings.LabelReleaseDate" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" @input="handleInputChange" />
|
||||
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" trim-whitespace @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1 pt-6">
|
||||
<div class="flex justify-center">
|
||||
|
||||
188
client/cypress/tests/utils/ElapsedPrettyExtended.cy.js
Normal file
188
client/cypress/tests/utils/ElapsedPrettyExtended.cy.js
Normal file
@@ -0,0 +1,188 @@
|
||||
import Vue from 'vue'
|
||||
import '@/plugins/utils'
|
||||
|
||||
// This is the actual function that is being tested
|
||||
const elapsedPrettyExtended = Vue.prototype.$elapsedPrettyExtended
|
||||
|
||||
// Helper function to convert days, hours, minutes, seconds to total seconds
|
||||
function DHMStoSeconds(days, hours, minutes, seconds) {
|
||||
return seconds + minutes * 60 + hours * 3600 + days * 86400
|
||||
}
|
||||
|
||||
describe('$elapsedPrettyExtended', () => {
|
||||
describe('function is on the Vue Prototype', () => {
|
||||
it('exists as a function on Vue.prototype', () => {
|
||||
expect(Vue.prototype.$elapsedPrettyExtended).to.exist
|
||||
expect(Vue.prototype.$elapsedPrettyExtended).to.be.a('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('param default values', () => {
|
||||
const testSeconds = DHMStoSeconds(0, 25, 1, 5) // 25h 1m 5s = 90065 seconds
|
||||
|
||||
it('uses useDays=true showSeconds=true by default', () => {
|
||||
expect(elapsedPrettyExtended(testSeconds)).to.equal('1d 1h 1m 5s')
|
||||
})
|
||||
|
||||
it('only useDays=false overrides useDays but keeps showSeconds=true', () => {
|
||||
expect(elapsedPrettyExtended(testSeconds, false)).to.equal('25h 1m 5s')
|
||||
})
|
||||
|
||||
it('explicit useDays=false showSeconds=false overrides both', () => {
|
||||
expect(elapsedPrettyExtended(testSeconds, false, false)).to.equal('25h 1m')
|
||||
})
|
||||
})
|
||||
|
||||
describe('useDays=false showSeconds=true', () => {
|
||||
const useDaysFalse = false
|
||||
const showSecondsTrue = true
|
||||
const testCases = [
|
||||
[[0, 0, 0, 0], '', '0s -> ""'],
|
||||
[[0, 1, 0, 1], '1h 1s', '1h 1s -> 1h 1s'],
|
||||
[[0, 25, 0, 1], '25h 1s', '25h 1s -> 25h 1s']
|
||||
]
|
||||
|
||||
testCases.forEach(([dhms, expected, description]) => {
|
||||
it(description, () => {
|
||||
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysFalse, showSecondsTrue)).to.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('useDays=true showSeconds=true', () => {
|
||||
const useDaysTrue = true
|
||||
const showSecondsTrue = true
|
||||
const testCases = [
|
||||
[[0, 0, 0, 0], '', '0s -> ""'],
|
||||
[[0, 1, 0, 1], '1h 1s', '1h 1s -> 1h 1s'],
|
||||
[[0, 25, 0, 1], '1d 1h 1s', '25h 1s -> 1d 1h 1s']
|
||||
]
|
||||
|
||||
testCases.forEach(([dhms, expected, description]) => {
|
||||
it(description, () => {
|
||||
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysTrue, showSecondsTrue)).to.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('useDays=true showSeconds=false', () => {
|
||||
const useDaysTrue = true
|
||||
const showSecondsFalse = false
|
||||
const testCases = [
|
||||
[[0, 0, 0, 0], '', '0s -> ""'],
|
||||
[[0, 1, 0, 0], '1h', '1h -> 1h'],
|
||||
[[0, 1, 0, 1], '1h', '1h 1s -> 1h'],
|
||||
[[0, 1, 1, 0], '1h 1m', '1h 1m -> 1h 1m'],
|
||||
[[0, 25, 0, 0], '1d 1h', '25h -> 1d 1h'],
|
||||
[[0, 25, 0, 1], '1d 1h', '25h 1s -> 1d 1h'],
|
||||
[[2, 0, 0, 0], '2d', '2d -> 2d']
|
||||
]
|
||||
|
||||
testCases.forEach(([dhms, expected, description]) => {
|
||||
it(description, () => {
|
||||
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysTrue, showSecondsFalse)).to.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('rounding useDays=true showSeconds=true', () => {
|
||||
const useDaysTrue = true
|
||||
const showSecondsTrue = true
|
||||
const testCases = [
|
||||
// Seconds rounding
|
||||
[[0, 0, 0, 1], '1s', '1s -> 1s'],
|
||||
[[0, 0, 0, 29.9], '30s', '29.9s -> 30s'],
|
||||
[[0, 0, 0, 30], '30s', '30s -> 30s'],
|
||||
[[0, 0, 0, 30.1], '30s', '30.1s -> 30s'],
|
||||
[[0, 0, 0, 59.4], '59s', '59.4s -> 59s'],
|
||||
[[0, 0, 0, 59.5], '1m', '59.5s -> 1m'],
|
||||
|
||||
// Minutes rounding
|
||||
[[0, 0, 59, 29], '59m 29s', '59m 29s -> 59m 29s'],
|
||||
[[0, 0, 59, 30], '59m 30s', '59m 30s -> 59m 30s'],
|
||||
[[0, 0, 59, 59.5], '1h', '59m 59.5s -> 1h'],
|
||||
|
||||
// Hours rounding
|
||||
[[0, 23, 59, 29], '23h 59m 29s', '23h 59m 29s -> 23h 59m 29s'],
|
||||
[[0, 23, 59, 30], '23h 59m 30s', '23h 59m 30s -> 23h 59m 30s'],
|
||||
[[0, 23, 59, 59.5], '1d', '23h 59m 59.5s -> 1d'],
|
||||
|
||||
// The actual bug case
|
||||
[[44, 23, 59, 30], '44d 23h 59m 30s', '44d 23h 59m 30s -> 44d 23h 59m 30s']
|
||||
]
|
||||
|
||||
testCases.forEach(([dhms, expected, description]) => {
|
||||
it(description, () => {
|
||||
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysTrue, showSecondsTrue)).to.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('rounding useDays=true showSeconds=false', () => {
|
||||
const useDaysTrue = true
|
||||
const showSecondsFalse = false
|
||||
const testCases = [
|
||||
// Seconds rounding - these cases changed behavior from original
|
||||
[[0, 0, 0, 1], '', '1s -> ""'],
|
||||
[[0, 0, 0, 29.9], '', '29.9s -> ""'],
|
||||
[[0, 0, 0, 30], '', '30s -> ""'],
|
||||
[[0, 0, 0, 30.1], '', '30.1s -> ""'],
|
||||
[[0, 0, 0, 59.4], '', '59.4s -> ""'],
|
||||
[[0, 0, 0, 59.5], '1m', '59.5s -> 1m'],
|
||||
// This is unexpected behavior, but it's consistent with the original behavior
|
||||
// We preserved the test case, to document the current behavior
|
||||
// - with showSeconds=false,
|
||||
// one might expect: 1m 29.5s --round(1.4901m)-> 1m
|
||||
// actual implementation: 1h 29.5s --roundSeconds-> 1h 30s --roundMinutes-> 2m
|
||||
// So because of the separate rounding of seconds, and then minutes, it returns 2m
|
||||
[[0, 0, 1, 29.5], '2m', '1m 29.5s -> 2m'],
|
||||
|
||||
// Minutes carry - actual bug fixes below
|
||||
[[0, 0, 59, 29], '59m', '59m 29s -> 59m'],
|
||||
[[0, 0, 59, 30], '1h', '59m 30s -> 1h'], // This was an actual bug, used to return 60m
|
||||
[[0, 0, 59, 59.5], '1h', '59m 59.5s -> 1h'],
|
||||
|
||||
// Hours carry
|
||||
[[0, 23, 59, 29], '23h 59m', '23h 59m 29s -> 23h 59m'],
|
||||
[[0, 23, 59, 30], '1d', '23h 59m 30s -> 1d'], // This was an actual bug, used to return 23h 60m
|
||||
[[0, 23, 59, 59.5], '1d', '23h 59m 59.5s -> 1d'],
|
||||
|
||||
// The actual bug case
|
||||
[[44, 23, 59, 30], '45d', '44d 23h 59m 30s -> 45d'] // This was an actual bug, used to return 44d 23h 60m
|
||||
]
|
||||
|
||||
testCases.forEach(([dhms, expected, description]) => {
|
||||
it(description, () => {
|
||||
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysTrue, showSecondsFalse)).to.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('empty values', () => {
|
||||
const paramCombos = [
|
||||
// useDays, showSeconds, description
|
||||
[true, true, 'with days and seconds'],
|
||||
[true, false, 'with days, no seconds'],
|
||||
[false, true, 'no days, with seconds'],
|
||||
[false, false, 'no days, no seconds']
|
||||
]
|
||||
|
||||
const emptyInputs = [
|
||||
// input, description
|
||||
[null, 'null input'],
|
||||
[undefined, 'undefined input'],
|
||||
[0, 'zero'],
|
||||
[0.49, 'rounds to zero'] // Just under rounding threshold
|
||||
]
|
||||
|
||||
paramCombos.forEach(([useDays, showSeconds, paramDesc]) => {
|
||||
describe(paramDesc, () => {
|
||||
emptyInputs.forEach(([input, desc]) => {
|
||||
it(desc, () => {
|
||||
expect(elapsedPrettyExtended(input, useDays, showSeconds)).to.equal('')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -57,9 +57,10 @@ export default {
|
||||
for (let entry of entries) {
|
||||
this.cardWidth = entry.borderBoxSize[0].inlineSize
|
||||
this.cardHeight = entry.borderBoxSize[0].blockSize
|
||||
this.resizeObserver.disconnect()
|
||||
this.$refs.bookshelf.removeChild(instance.$el)
|
||||
}
|
||||
this.coverHeight = instance.coverHeight
|
||||
this.resizeObserver.disconnect()
|
||||
this.$refs.bookshelf.removeChild(instance.$el)
|
||||
})
|
||||
instance.$el.style.visibility = 'hidden'
|
||||
instance.$el.style.position = 'absolute'
|
||||
@@ -131,10 +132,7 @@ export default {
|
||||
this.entityComponentRefs[index] = instance
|
||||
|
||||
instance.$mount()
|
||||
const shelfOffsetY = this.shelfPaddingHeight * this.sizeMultiplier
|
||||
const row = index % this.entitiesPerShelf
|
||||
const shelfOffsetX = row * this.totalEntityCardWidth + this.bookshelfMarginLeft
|
||||
instance.$el.style.transform = `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)`
|
||||
instance.$el.style.transform = this.entityTransform((index % this.entitiesPerShelf) + 1)
|
||||
instance.$el.classList.add('absolute', 'top-0', 'left-0')
|
||||
shelfEl.appendChild(instance.$el)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const pkg = require('./package.json')
|
||||
|
||||
const routerBasePath = process.env.ROUTER_BASE_PATH || ''
|
||||
const routerBasePath = process.env.ROUTER_BASE_PATH ?? '/audiobookshelf'
|
||||
const serverHostUrl = process.env.NODE_ENV === 'production' ? '' : 'http://localhost:3333'
|
||||
const serverPaths = ['api/', 'public/', 'hls/', 'auth/', 'feed/', 'status', 'login', 'logout', 'init']
|
||||
const proxy = Object.fromEntries(serverPaths.map((path) => [`${routerBasePath}/${path}`, { target: process.env.NODE_ENV !== 'production' ? serverHostUrl : '/' }]))
|
||||
|
||||
4
client/package-lock.json
generated
4
client/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.17.4",
|
||||
"version": "2.19.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.17.4",
|
||||
"version": "2.19.2",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.17.4",
|
||||
"version": "2.19.2",
|
||||
"buildNumber": 1,
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
|
||||
@@ -414,11 +414,8 @@ export default {
|
||||
|
||||
const audioEl = this.audioEl || document.createElement('audio')
|
||||
var src = audioTrack.contentUrl + `?token=${this.userToken}`
|
||||
if (this.$isDev) {
|
||||
src = `${process.env.serverUrl}${src}`
|
||||
}
|
||||
|
||||
audioEl.src = src
|
||||
audioEl.src = `${process.env.serverUrl}${src}`
|
||||
audioEl.id = 'chapter-audio'
|
||||
document.body.appendChild(audioEl)
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<div v-if="openMapOptions" class="flex flex-wrap">
|
||||
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.subtitle" />
|
||||
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" class="mb-5 ml-4" />
|
||||
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" trim-whitespace class="mb-5 ml-4" />
|
||||
</div>
|
||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 h-18 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.authors" />
|
||||
@@ -31,7 +31,7 @@
|
||||
</div>
|
||||
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.publishedYear" />
|
||||
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" class="mb-5 ml-4" />
|
||||
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" trim-whitespace class="mb-5 ml-4" />
|
||||
</div>
|
||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 h-18 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.series" />
|
||||
@@ -51,11 +51,11 @@
|
||||
</div>
|
||||
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.publisher" />
|
||||
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" class="mb-5 ml-4" />
|
||||
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" trim-whitespace class="mb-5 ml-4" />
|
||||
</div>
|
||||
<div v-if="!isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.language" />
|
||||
<ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" class="mb-5 ml-4" />
|
||||
<ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" trim-whitespace class="mb-5 ml-4" />
|
||||
</div>
|
||||
<div v-if="!isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.explicit" />
|
||||
@@ -86,7 +86,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex items-center justify-end p-4">
|
||||
<div class="w-full flex items-center p-4 space-x-2">
|
||||
<ui-btn small @click.stop="resetMapDetails">{{ $strings.ButtonReset }}</ui-btn>
|
||||
<ui-tooltip direction="bottom" :text="$strings.MessageBatchEditPopulateMapDetailsAllHelp">
|
||||
<ui-btn small :disabled="!hasSelectedBatchUsage" @click.stop="populateFromExisting()">{{ $strings.ButtonBatchEditPopulateFromExisting }}</ui-btn>
|
||||
</ui-tooltip>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" :disabled="!hasSelectedBatchUsage" :padding-x="8" small class="text-base" :loading="isProcessing" @click="mapBatchDetails">{{ $strings.ButtonApply }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,6 +102,11 @@
|
||||
<div class="flex justify-center flex-wrap">
|
||||
<template v-for="libraryItem in libraryItemCopies">
|
||||
<div :key="libraryItem.id" class="w-full max-w-3xl border border-black-300 p-6 -ml-px -mt-px">
|
||||
<div class="flex items-center justify-end">
|
||||
<ui-tooltip direction="bottom" :text="$strings.MessageBatchEditPopulateMapDetailsItemHelp">
|
||||
<ui-btn small :disabled="!hasSelectedBatchUsage" @click="populateFromExisting(libraryItem.id)">{{ $strings.ButtonBatchEditPopulateMapDetails }}</ui-btn>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<widgets-book-details-edit v-if="libraryItem.mediaType === 'book'" :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" @change="handleItemChange" />
|
||||
<widgets-podcast-details-edit v-else :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" @change="handleItemChange" />
|
||||
</div>
|
||||
@@ -228,6 +238,88 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetMapDetails() {
|
||||
this.blurBatchForm()
|
||||
this.batchDetails = {
|
||||
subtitle: null,
|
||||
authors: null,
|
||||
publishedYear: null,
|
||||
series: [],
|
||||
genres: [],
|
||||
tags: [],
|
||||
narrators: [],
|
||||
publisher: null,
|
||||
language: null,
|
||||
explicit: false,
|
||||
abridged: false
|
||||
}
|
||||
this.selectedBatchUsage = {
|
||||
subtitle: false,
|
||||
authors: false,
|
||||
publishedYear: false,
|
||||
series: false,
|
||||
genres: false,
|
||||
tags: false,
|
||||
narrators: false,
|
||||
publisher: false,
|
||||
language: false,
|
||||
explicit: false,
|
||||
abridged: false
|
||||
}
|
||||
},
|
||||
populateFromExisting(libraryItemId) {
|
||||
this.blurBatchForm()
|
||||
|
||||
let libraryItemsToMap = this.libraryItemCopies
|
||||
if (libraryItemId) {
|
||||
libraryItemsToMap = this.libraryItemCopies.filter((li) => li.id === libraryItemId)
|
||||
}
|
||||
|
||||
for (const key in this.selectedBatchUsage) {
|
||||
if (!this.selectedBatchUsage[key]) continue
|
||||
if (this.isMapAppend && !this.appendableKeys.includes(key)) continue
|
||||
|
||||
let existingValues = undefined
|
||||
libraryItemsToMap.forEach((li) => {
|
||||
if (key === 'tags') {
|
||||
if (!existingValues) existingValues = []
|
||||
li.media.tags.forEach((tag) => {
|
||||
if (!existingValues.includes(tag)) {
|
||||
existingValues.push(tag)
|
||||
}
|
||||
})
|
||||
} else if (key === 'authors') {
|
||||
if (!existingValues) existingValues = []
|
||||
li.media.metadata[key].forEach((entity) => {
|
||||
if (!existingValues.some((au) => au.id === entity.id)) {
|
||||
existingValues.push({
|
||||
id: entity.id,
|
||||
name: entity.name
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (key === 'series') {
|
||||
if (!existingValues) existingValues = []
|
||||
li.media.metadata[key].forEach((entity) => {
|
||||
if (!existingValues.includes(entity.name)) {
|
||||
existingValues.push(entity.name)
|
||||
}
|
||||
})
|
||||
} else if (key === 'genres' || key === 'narrators') {
|
||||
if (!existingValues) existingValues = []
|
||||
li.media.metadata[key].forEach((item) => {
|
||||
if (!existingValues.includes(item)) {
|
||||
existingValues.push(item)
|
||||
}
|
||||
})
|
||||
} else if (existingValues === undefined) {
|
||||
existingValues = li.media.metadata[key]
|
||||
}
|
||||
})
|
||||
|
||||
this.batchDetails[key] = existingValues
|
||||
}
|
||||
},
|
||||
handleItemChange(itemChange) {
|
||||
if (!itemChange.hasChanges) {
|
||||
this.itemsWithChanges = this.itemsWithChanges.filter((id) => id !== itemChange.libraryItemId)
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<div class="pt-4">
|
||||
<h2 class="font-semibold">{{ $strings.HeaderSettingsGeneral }}</h2>
|
||||
</div>
|
||||
<div class="flex items-end py-2">
|
||||
<ui-toggle-switch labeledBy="settings-store-cover-with-items" v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsStoreCoversWithItemHelp">
|
||||
<div role="article" :aria-label="$strings.LabelSettingsStoreCoversWithItemHelp" class="flex items-end py-2">
|
||||
<ui-toggle-switch :label="$strings.LabelSettingsStoreCoversWithItem" v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
|
||||
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsStoreCoversWithItemHelp">
|
||||
<p class="pl-4">
|
||||
<span id="settings-store-cover-with-items">{{ $strings.LabelSettingsStoreCoversWithItem }}</span>
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
@@ -16,9 +16,9 @@
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch labeledBy="settings-store-metadata-with-items" v-model="newServerSettings.storeMetadataWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithItem', val)" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsStoreMetadataWithItemHelp">
|
||||
<div role="article" :aria-label="$strings.LabelSettingsStoreMetadataWithItemHelp" class="flex items-center py-2">
|
||||
<ui-toggle-switch :label="$strings.LabelSettingsStoreMetadataWithItem" v-model="newServerSettings.storeMetadataWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithItem', val)" />
|
||||
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsStoreMetadataWithItemHelp">
|
||||
<p class="pl-4">
|
||||
<span id="settings-store-metadata-with-items">{{ $strings.LabelSettingsStoreMetadataWithItem }}</span>
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
@@ -26,9 +26,9 @@
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch labeledBy="settings-sorting-ignore-prefixes" v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsSortingIgnorePrefixesHelp">
|
||||
<div role="article" :aria-label="$strings.LabelSettingsSortingIgnorePrefixesHelp" class="flex items-center py-2">
|
||||
<ui-toggle-switch :label="$strings.LabelSettingsSortingIgnorePrefixes" v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
|
||||
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsSortingIgnorePrefixesHelp">
|
||||
<p class="pl-4">
|
||||
<span id="settings-sorting-ignore-prefixes">{{ $strings.LabelSettingsSortingIgnorePrefixes }}</span>
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
@@ -42,18 +42,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2 mb-2">
|
||||
<ui-toggle-switch labeledBy="settings-chromecast-support" v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
|
||||
<p class="pl-4" id="settings-chromecast-support">{{ $strings.LabelSettingsChromecastSupport }}</p>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<h2 class="font-semibold">{{ $strings.HeaderSettingsScanner }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch labeledBy="settings-parse-subtitles" v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsParseSubtitlesHelp">
|
||||
<div role="article" :aria-label="$strings.LabelSettingsParseSubtitlesHelp" class="flex items-center py-2">
|
||||
<ui-toggle-switch :label="$strings.LabelSettingsParseSubtitles" v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" />
|
||||
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsParseSubtitlesHelp">
|
||||
<p class="pl-4">
|
||||
<span id="settings-parse-subtitles">{{ $strings.LabelSettingsParseSubtitles }}</span>
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
@@ -61,9 +56,9 @@
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch labeledBy="settings-find-covers" v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsFindCoversHelp">
|
||||
<div role="article" :aria-label="$strings.LabelSettingsFindCoversHelp" class="flex items-center py-2">
|
||||
<ui-toggle-switch :label="$strings.LabelSettingsFindCovers" v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" />
|
||||
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsFindCoversHelp">
|
||||
<p class="pl-4">
|
||||
<span id="settings-find-covers">{{ $strings.LabelSettingsFindCovers }}</span>
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
@@ -75,9 +70,9 @@
|
||||
<ui-dropdown v-model="newServerSettings.scannerCoverProvider" small :items="providers" label="Cover Provider" @input="updateScannerCoverProvider" :disabled="updatingServerSettings" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch labeledBy="settings-prefer-matched-metadata" v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
|
||||
<div role="article" :aria-label="$strings.LabelSettingsPreferMatchedMetadataHelp" class="flex items-center py-2">
|
||||
<ui-toggle-switch :label="$strings.LabelSettingsPreferMatchedMetadata" v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
|
||||
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
|
||||
<p class="pl-4">
|
||||
<span id="settings-prefer-matched-metadata">{{ $strings.LabelSettingsPreferMatchedMetadata }}</span>
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
@@ -85,15 +80,29 @@
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch labeledBy="settings-disable-watcher" v-model="scannerEnableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', !val)" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsEnableWatcherHelp">
|
||||
<div role="article" :aria-label="$strings.LabelSettingsEnableWatcherHelp" class="flex items-center py-2">
|
||||
<ui-toggle-switch :label="$strings.LabelSettingsEnableWatcher" v-model="scannerEnableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', !val)" />
|
||||
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsEnableWatcherHelp">
|
||||
<p class="pl-4">
|
||||
<span id="settings-disable-watcher">{{ $strings.LabelSettingsEnableWatcher }}</span>
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<h2 class="font-semibold">{{ $strings.HeaderSettingsWebClient }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch v-model="newServerSettings.chromecastEnabled" :label="$strings.LabelSettingsChromecastSupport" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
|
||||
<p aria-hidden="true" class="pl-4">{{ $strings.LabelSettingsChromecastSupport }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2 mb-2">
|
||||
<ui-toggle-switch v-model="newServerSettings.allowIframe" :label="$strings.LabelSettingsAllowIframe" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('allowIframe', val)" />
|
||||
<p aria-hidden="true" class="pl-4">{{ $strings.LabelSettingsAllowIframe }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
@@ -324,21 +333,21 @@ export default {
|
||||
},
|
||||
updateServerSettings(payload) {
|
||||
this.updatingServerSettings = true
|
||||
this.$store
|
||||
.dispatch('updateServerSettings', payload)
|
||||
.then(() => {
|
||||
this.updatingServerSettings = false
|
||||
this.$store.dispatch('updateServerSettings', payload).then((response) => {
|
||||
this.updatingServerSettings = false
|
||||
|
||||
if (payload.language) {
|
||||
// Updating language after save allows for re-rendering
|
||||
this.$setLanguageCode(payload.language)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update server settings', error)
|
||||
this.updatingServerSettings = false
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
if (response.error) {
|
||||
console.error('Failed to update server settins', response.error)
|
||||
this.$toast.error(response.error)
|
||||
this.initServerSettings()
|
||||
return
|
||||
}
|
||||
|
||||
if (payload.language) {
|
||||
// Updating language after save allows for re-rendering
|
||||
this.$setLanguageCode(payload.language)
|
||||
}
|
||||
})
|
||||
},
|
||||
initServerSettings() {
|
||||
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<tr v-for="feed in feeds" :key="feed.id" class="cursor-pointer h-12" @click="showFeed(feed)">
|
||||
<!-- -->
|
||||
<td>
|
||||
<img :src="coverUrl(feed)" class="h-full w-full" />
|
||||
<img :src="coverUrl(feed)" class="h-auto w-full" />
|
||||
</td>
|
||||
<!-- -->
|
||||
<td class="w-48 max-w-64 min-w-24 text-left truncate">
|
||||
@@ -126,7 +126,7 @@ export default {
|
||||
},
|
||||
coverUrl(feed) {
|
||||
if (!feed.coverPath) return `${this.$config.routerBasePath}/Logo.png`
|
||||
return `${feed.feedUrl}/cover`
|
||||
return `${this.$config.routerBasePath}${feed.feedUrl}/cover`
|
||||
},
|
||||
async loadFeeds() {
|
||||
const data = await this.$axios.$get(`/api/feeds`).catch((err) => {
|
||||
@@ -137,7 +137,16 @@ export default {
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
return
|
||||
}
|
||||
this.feeds = data.feeds
|
||||
this.feeds = data.feeds.map((feed) => ({
|
||||
...feed,
|
||||
episodes: [...feed.episodes].sort((a, b) => {
|
||||
if (!a.pubDate) return 1 // null dates sort to end
|
||||
if (!b.pubDate) return -1
|
||||
const dateA = new Date(a.pubDate)
|
||||
const dateB = new Date(b.pubDate)
|
||||
return dateA - dateB
|
||||
})
|
||||
}))
|
||||
},
|
||||
init() {
|
||||
this.loadFeeds()
|
||||
|
||||
@@ -14,11 +14,7 @@
|
||||
<h1 class="text-xl pl-2">{{ username }}</h1>
|
||||
</div>
|
||||
<div v-if="userToken" class="flex text-xs mt-4">
|
||||
<ui-text-input-with-label :label="$strings.LabelApiToken" :value="userToken" readonly />
|
||||
|
||||
<div class="px-1 mt-8 cursor-pointer" @click="copyToClipboard(userToken)">
|
||||
<span class="material-symbols pl-2 text-base">content_copy</span>
|
||||
</div>
|
||||
<ui-text-input-with-label :label="$strings.LabelApiToken" :value="userToken" readonly show-copy />
|
||||
</div>
|
||||
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
|
||||
<div class="py-2">
|
||||
@@ -140,9 +136,6 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(str) {
|
||||
this.$copyToClipboard(str, this)
|
||||
},
|
||||
async init() {
|
||||
this.listeningSessions = await this.$axios
|
||||
.$get(`/api/users/${this.user.id}/listening-sessions?page=0&itemsPerPage=10`)
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
<!-- Item Cover Overlay -->
|
||||
<div class="absolute top-0 left-0 w-full h-full z-10 opacity-0 group-hover:opacity-100 pointer-events-none">
|
||||
<div v-show="showPlayButton && !isStreaming" class="h-full flex items-center justify-center pointer-events-none">
|
||||
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto cursor-pointer" @click.stop.prevent="playItem">
|
||||
<button class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto cursor-pointer" :aria-label="$strings.ButtonPlay" @click.stop.prevent="playItem">
|
||||
<span class="material-symbols fill text-4xl">play_arrow</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span class="absolute bottom-2.5 right-2.5 z-10 material-symbols text-lg cursor-pointer text-white text-opacity-75 hover:text-opacity-100 hover:scale-110 transform duration-200 pointer-events-auto" @click="showEditCover">edit</span>
|
||||
<button class="absolute bottom-2.5 right-2.5 z-10 material-symbols text-lg cursor-pointer text-white text-opacity-75 hover:text-opacity-100 hover:scale-110 transform duration-200 pointer-events-auto" :aria-label="$strings.ButtonEdit" @click="showEditCover">edit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,7 +87,7 @@
|
||||
</ui-btn>
|
||||
|
||||
<ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
|
||||
<span v-show="!isStreaming" class="material-symbols text-2xl -ml-2 pr-1 text-white">error</span>
|
||||
<span class="material-symbols text-2xl -ml-2 pr-1 text-white">error</span>
|
||||
{{ isMissing ? $strings.LabelMissing : $strings.LabelIncomplete }}
|
||||
</ui-btn>
|
||||
|
||||
@@ -96,12 +96,12 @@
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-btn v-if="showReadButton" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
|
||||
<span class="material-symbols text-2xl -ml-2 pr-2 text-white">auto_stories</span>
|
||||
<span class="material-symbols text-2xl -ml-2 pr-2 text-white" aria-hidden="true">auto_stories</span>
|
||||
{{ $strings.ButtonRead }}
|
||||
</ui-btn>
|
||||
|
||||
<ui-tooltip v-if="userCanUpdate" :text="$strings.LabelEdit" direction="top">
|
||||
<ui-icon-btn icon="" outlined class="mx-0.5" @click="editClick" />
|
||||
<ui-icon-btn icon="" outlined class="mx-0.5" :aria-label="$strings.LabelEdit" @click="editClick" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip v-if="!isPodcast" :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
|
||||
@@ -110,12 +110,12 @@
|
||||
|
||||
<!-- Only admin or root user can download new episodes -->
|
||||
<ui-tooltip v-if="isPodcast && userIsAdminOrUp" :text="$strings.LabelFindEpisodes" direction="top">
|
||||
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
||||
<ui-icon-btn icon="search" class="mx-0.5" :aria-label="$strings.LabelFindEpisodes" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="148" @action="contextMenuAction">
|
||||
<template #default="{ showMenu, clickShowMenu, disabled }">
|
||||
<button type="button" :disabled="disabled" class="mx-0.5 icon-btn bg-primary border border-gray-600 w-9 h-9 rounded-md flex items-center justify-center relative" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||
<button type="button" :disabled="disabled" class="mx-0.5 icon-btn bg-primary border border-gray-600 w-9 h-9 rounded-md flex items-center justify-center relative" aria-haspopup="listbox" :aria-expanded="showMenu" :aria-label="$strings.LabelMore" @click.stop.prevent="clickShowMenu">
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
</button>
|
||||
</template>
|
||||
@@ -123,7 +123,8 @@
|
||||
</div>
|
||||
|
||||
<div class="my-4 w-full">
|
||||
<p ref="description" id="item-description" dir="auto" class="text-base text-gray-100 whitespace-pre-line mb-1" :class="{ 'show-full': showFullDescription }">{{ description }}</p>
|
||||
<div ref="description" id="item-description" dir="auto" class="default-style less-spacing text-base text-gray-100 whitespace-pre-line mb-1" :class="{ 'show-full': showFullDescription }" v-html="description" />
|
||||
|
||||
<button v-if="isDescriptionClamped" class="py-0.5 flex items-center text-slate-300 hover:text-white" @click="showFullDescription = !showFullDescription">{{ showFullDescription ? $strings.ButtonReadLess : $strings.ButtonReadMore }} <span class="material-symbols text-xl pl-1" v-html="showFullDescription ? 'expand_less' : ''" /></button>
|
||||
</div>
|
||||
|
||||
@@ -141,7 +142,7 @@
|
||||
</div>
|
||||
|
||||
<modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" />
|
||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :library-item-id="libraryItemId" hide-create @select="selectBookmark" />
|
||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :playback-rate="1" :library-item-id="libraryItemId" hide-create @select="selectBookmark" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -804,8 +805,7 @@ export default {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 4;
|
||||
max-height: 6.25rem;
|
||||
transition: all 0.3s ease-in-out;
|
||||
max-height: calc(6 * 1lh);
|
||||
}
|
||||
#item-description.show-full {
|
||||
-webkit-line-clamp: unset;
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
<div class="w-full pt-16">
|
||||
<player-ui ref="audioPlayer" :chapters="chapters" :current-chapter="currentChapter" :paused="isPaused" :loading="!hasLoaded" :is-podcast="false" hide-bookmarks hide-sleep-timer @playPause="playPause" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setVolume="setVolume" @setPlaybackRate="setPlaybackRate" @seek="seek" />
|
||||
</div>
|
||||
|
||||
<ui-tooltip v-if="mediaItemShare.isDownloadable" direction="bottom" :text="$strings.LabelDownload" class="absolute top-0 left-0 m-4">
|
||||
<button aria-label="Download" class="text-gray-300 hover:text-white" @click="downloadShareItem"><span class="material-symbols text-2xl sm:text-3xl">download</span></button>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,6 +67,9 @@ export default {
|
||||
if (!this.playbackSession.coverPath) return `${this.$config.routerBasePath}/book_placeholder.jpg`
|
||||
return `${this.$config.routerBasePath}/public/share/${this.mediaItemShare.slug}/cover`
|
||||
},
|
||||
downloadUrl() {
|
||||
return `${process.env.serverUrl}/public/share/${this.mediaItemShare.slug}/download`
|
||||
},
|
||||
audioTracks() {
|
||||
return (this.playbackSession.audioTracks || []).map((track) => {
|
||||
track.relativeContentUrl = track.contentUrl
|
||||
@@ -103,6 +110,84 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
mediaSessionPlay() {
|
||||
console.log('Media session play')
|
||||
this.play()
|
||||
},
|
||||
mediaSessionPause() {
|
||||
console.log('Media session pause')
|
||||
this.pause()
|
||||
},
|
||||
mediaSessionStop() {
|
||||
console.log('Media session stop')
|
||||
this.pause()
|
||||
},
|
||||
mediaSessionSeekBackward() {
|
||||
console.log('Media session seek backward')
|
||||
this.jumpBackward()
|
||||
},
|
||||
mediaSessionSeekForward() {
|
||||
console.log('Media session seek forward')
|
||||
this.jumpForward()
|
||||
},
|
||||
mediaSessionSeekTo(e) {
|
||||
console.log('Media session seek to', e)
|
||||
if (e.seekTime !== null && !isNaN(e.seekTime)) {
|
||||
this.seek(e.seekTime)
|
||||
}
|
||||
},
|
||||
mediaSessionPreviousTrack() {
|
||||
if (this.$refs.audioPlayer) {
|
||||
this.$refs.audioPlayer.prevChapter()
|
||||
}
|
||||
},
|
||||
mediaSessionNextTrack() {
|
||||
if (this.$refs.audioPlayer) {
|
||||
this.$refs.audioPlayer.nextChapter()
|
||||
}
|
||||
},
|
||||
updateMediaSessionPlaybackState() {
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.playbackState = this.isPlaying ? 'playing' : 'paused'
|
||||
}
|
||||
},
|
||||
setMediaSession() {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API
|
||||
if ('mediaSession' in navigator) {
|
||||
const chapterInfo = []
|
||||
if (this.chapters.length > 0) {
|
||||
this.chapters.forEach((chapter) => {
|
||||
chapterInfo.push({
|
||||
title: chapter.title,
|
||||
startTime: chapter.start
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: this.mediaItemShare.playbackSession.displayTitle || 'No title',
|
||||
artist: this.mediaItemShare.playbackSession.displayAuthor || 'Unknown',
|
||||
artwork: [
|
||||
{
|
||||
src: this.coverUrl
|
||||
}
|
||||
],
|
||||
chapterInfo
|
||||
})
|
||||
console.log('Set media session metadata', navigator.mediaSession.metadata)
|
||||
|
||||
navigator.mediaSession.setActionHandler('play', this.mediaSessionPlay)
|
||||
navigator.mediaSession.setActionHandler('pause', this.mediaSessionPause)
|
||||
navigator.mediaSession.setActionHandler('stop', this.mediaSessionStop)
|
||||
navigator.mediaSession.setActionHandler('seekbackward', this.mediaSessionSeekBackward)
|
||||
navigator.mediaSession.setActionHandler('seekforward', this.mediaSessionSeekForward)
|
||||
navigator.mediaSession.setActionHandler('seekto', this.mediaSessionSeekTo)
|
||||
navigator.mediaSession.setActionHandler('previoustrack', this.mediaSessionSeekBackward)
|
||||
navigator.mediaSession.setActionHandler('nexttrack', this.mediaSessionSeekForward)
|
||||
} else {
|
||||
console.warn('Media session not available')
|
||||
}
|
||||
},
|
||||
async coverImageLoaded(e) {
|
||||
if (!this.playbackSession.coverPath) return
|
||||
const fac = new FastAverageColor()
|
||||
@@ -119,8 +204,19 @@ export default {
|
||||
})
|
||||
},
|
||||
playPause() {
|
||||
if (this.isPlaying) {
|
||||
this.pause()
|
||||
} else {
|
||||
this.play()
|
||||
}
|
||||
},
|
||||
play() {
|
||||
if (!this.localAudioPlayer || !this.hasLoaded) return
|
||||
this.localAudioPlayer.playPause()
|
||||
this.localAudioPlayer.play()
|
||||
},
|
||||
pause() {
|
||||
if (!this.localAudioPlayer || !this.hasLoaded) return
|
||||
this.localAudioPlayer.pause()
|
||||
},
|
||||
jumpForward() {
|
||||
if (!this.localAudioPlayer || !this.hasLoaded) return
|
||||
@@ -206,6 +302,7 @@ export default {
|
||||
} else {
|
||||
this.stopPlayInterval()
|
||||
}
|
||||
this.updateMediaSessionPlaybackState()
|
||||
},
|
||||
playerTimeUpdate(time) {
|
||||
this.setCurrentTime(time)
|
||||
@@ -247,6 +344,9 @@ export default {
|
||||
},
|
||||
playerFinished() {
|
||||
console.log('Player finished')
|
||||
},
|
||||
downloadShareItem() {
|
||||
this.$downloadFile(this.downloadUrl)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -266,6 +366,8 @@ export default {
|
||||
this.localAudioPlayer.on('timeupdate', this.playerTimeUpdate.bind(this))
|
||||
this.localAudioPlayer.on('error', this.playerError.bind(this))
|
||||
this.localAudioPlayer.on('finished', this.playerFinished.bind(this))
|
||||
|
||||
this.setMediaSession()
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.resize)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default class AudioTrack {
|
||||
constructor(track, userToken) {
|
||||
constructor(track, userToken, routerBasePath) {
|
||||
this.index = track.index || 0
|
||||
this.startOffset = track.startOffset || 0 // Total time of all previous tracks
|
||||
this.duration = track.duration || 0
|
||||
@@ -9,20 +9,27 @@ export default class AudioTrack {
|
||||
this.metadata = track.metadata || {}
|
||||
|
||||
this.userToken = userToken
|
||||
this.routerBasePath = routerBasePath || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for CastPlayer
|
||||
*/
|
||||
get fullContentUrl() {
|
||||
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
|
||||
}
|
||||
return `${window.location.origin}${this.contentUrl}?token=${this.userToken}`
|
||||
return `${window.location.origin}${this.routerBasePath}${this.contentUrl}?token=${this.userToken}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for LocalPlayer
|
||||
*/
|
||||
get relativeContentUrl() {
|
||||
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
|
||||
|
||||
return this.contentUrl + `?token=${this.userToken}`
|
||||
return `${this.routerBasePath}${this.contentUrl}?token=${this.userToken}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ export default class PlayerHandler {
|
||||
|
||||
console.log('[PlayerHandler] Preparing Session', session)
|
||||
|
||||
var audioTracks = session.audioTracks.map((at) => new AudioTrack(at, this.userToken))
|
||||
var audioTracks = session.audioTracks.map((at) => new AudioTrack(at, this.userToken, this.ctx.$config.routerBasePath))
|
||||
|
||||
this.ctx.playerLoading = true
|
||||
this.isHlsTranscode = true
|
||||
|
||||
@@ -7,6 +7,7 @@ const defaultCode = 'en-us'
|
||||
const languageCodeMap = {
|
||||
bg: { label: 'Български', dateFnsLocale: 'bg' },
|
||||
bn: { label: 'বাংলা', dateFnsLocale: 'bn' },
|
||||
ca: { label: 'Català', dateFnsLocale: 'ca' },
|
||||
cs: { label: 'Čeština', dateFnsLocale: 'cs' },
|
||||
da: { label: 'Dansk', dateFnsLocale: 'da' },
|
||||
de: { label: 'Deutsch', dateFnsLocale: 'de' },
|
||||
@@ -41,6 +42,7 @@ Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map((code) =>
|
||||
|
||||
// iTunes search API uses ISO 3166 country codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
||||
const podcastSearchRegionMap = {
|
||||
au: { label: 'Australia' },
|
||||
br: { label: 'Brasil' },
|
||||
be: { label: 'België / Belgique / Belgien' },
|
||||
cz: { label: 'Česko' },
|
||||
@@ -56,6 +58,7 @@ const podcastSearchRegionMap = {
|
||||
hu: { label: 'Magyarország' },
|
||||
nl: { label: 'Nederland' },
|
||||
no: { label: 'Norge' },
|
||||
nz: { label: 'New Zealand' },
|
||||
at: { label: 'Österreich' },
|
||||
pl: { label: 'Polska' },
|
||||
pt: { label: 'Portugal' },
|
||||
|
||||
@@ -128,12 +128,11 @@ Vue.prototype.$sanitizeSlug = (str) => {
|
||||
return str
|
||||
}
|
||||
|
||||
Vue.prototype.$copyToClipboard = (str, ctx) => {
|
||||
Vue.prototype.$copyToClipboard = (str) => {
|
||||
return new Promise((resolve) => {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(str).then(
|
||||
() => {
|
||||
if (ctx) ctx.$toast.success('Copied to clipboard')
|
||||
resolve(true)
|
||||
},
|
||||
(err) => {
|
||||
@@ -152,7 +151,6 @@ Vue.prototype.$copyToClipboard = (str, ctx) => {
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(el)
|
||||
|
||||
if (ctx) ctx.$toast.success('Copied to clipboard')
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -69,17 +69,22 @@ Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true, showSeconds = t
|
||||
let hours = Math.floor(minutes / 60)
|
||||
minutes -= hours * 60
|
||||
|
||||
// Handle rollovers before days calculation
|
||||
if (minutes && seconds && !showSeconds) {
|
||||
if (seconds >= 30) minutes++
|
||||
if (minutes >= 60) {
|
||||
hours++ // Increment hours if minutes roll over
|
||||
minutes -= 60 // adjust minutes
|
||||
}
|
||||
}
|
||||
|
||||
// Now calculate days with the final hours value
|
||||
let days = 0
|
||||
if (useDays || Math.floor(hours / 24) >= 100) {
|
||||
days = Math.floor(hours / 24)
|
||||
hours -= days * 24
|
||||
}
|
||||
|
||||
// If not showing seconds then round minutes up
|
||||
if (minutes && seconds && !showSeconds) {
|
||||
if (seconds >= 30) minutes++
|
||||
}
|
||||
|
||||
const strs = []
|
||||
if (days) strs.push(`${days}d`)
|
||||
if (hours) strs.push(`${hours}h`)
|
||||
|
||||
@@ -72,16 +72,17 @@ export const actions = {
|
||||
return this.$axios
|
||||
.$patch('/api/settings', updatePayload)
|
||||
.then((result) => {
|
||||
if (result.success) {
|
||||
if (result.serverSettings) {
|
||||
commit('setServerSettings', result.serverSettings)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return result
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update server settings', error)
|
||||
return false
|
||||
const errorMsg = error.response?.data || 'Unknown error'
|
||||
return {
|
||||
error: errorMsg
|
||||
}
|
||||
})
|
||||
},
|
||||
checkForUpdate({ commit }) {
|
||||
|
||||
@@ -5,6 +5,7 @@ export const state = () => ({
|
||||
orderDesc: false,
|
||||
filterBy: 'all',
|
||||
playbackRate: 1,
|
||||
playbackRateIncrementDecrement: 0.1,
|
||||
bookshelfCoverSize: 120,
|
||||
collapseSeries: false,
|
||||
collapseBookSeries: false,
|
||||
|
||||
215
client/strings/be.json
Normal file
215
client/strings/be.json
Normal file
@@ -0,0 +1,215 @@
|
||||
{
|
||||
"ButtonAdd": "Дадаць",
|
||||
"ButtonAddChapters": "Дадаць раздзелы",
|
||||
"ButtonAddDevice": "Дадаць прыладу",
|
||||
"ButtonAddLibrary": "Дадаць бібліятэку",
|
||||
"ButtonAddPodcasts": "Дадаць падкасты",
|
||||
"ButtonAddUser": "Дадаць карыстальніка",
|
||||
"ButtonAddYourFirstLibrary": "Дадайце сваю першую бібліятэку",
|
||||
"ButtonApply": "Ужыць",
|
||||
"ButtonApplyChapters": "Ужыць раздзелы",
|
||||
"ButtonAuthors": "Аўтары",
|
||||
"ButtonBack": "Назад",
|
||||
"ButtonBatchEditPopulateFromExisting": "Запоўніць з існуючага",
|
||||
"ButtonBrowseForFolder": "Знайсці тэчку",
|
||||
"ButtonCancel": "Адмяніць",
|
||||
"ButtonCancelEncode": "Адмяніць кадзіраванне",
|
||||
"ButtonChangeRootPassword": "Зменіце Root пароль",
|
||||
"ButtonCheckAndDownloadNewEpisodes": "Праверыць і спампаваць новыя эпізоды",
|
||||
"ButtonChooseAFolder": "Выбраць тэчку",
|
||||
"ButtonChooseFiles": "Выбраць файлы",
|
||||
"ButtonClearFilter": "Ачысціць фільтр",
|
||||
"ButtonCloseFeed": "Закрыць стужку",
|
||||
"ButtonCloseSession": "Закрыць адкрыты сеанс",
|
||||
"ButtonCollections": "Калекцыі",
|
||||
"ButtonConfigureScanner": "Наладзіць сканер",
|
||||
"ButtonCreate": "Ствараць",
|
||||
"ButtonCreateBackup": "Стварыць рэзервовую копію",
|
||||
"ButtonDelete": "Выдаліць",
|
||||
"ButtonDownloadQueue": "Чарга",
|
||||
"ButtonEdit": "Рэдагаваць",
|
||||
"ButtonEditChapters": "Рэдагаваць раздзелы",
|
||||
"ButtonEditPodcast": "Рэдагаваць падкаст",
|
||||
"ButtonEnable": "Уключыць",
|
||||
"ButtonFireAndFail": "Агонь і няўдача",
|
||||
"ButtonFireOnTest": "Тэст на вогнеўстойлівасць",
|
||||
"ButtonForceReScan": "Прымусовае паўторнае сканаванне",
|
||||
"ButtonFullPath": "Поўны шлях",
|
||||
"ButtonHide": "Схаваць",
|
||||
"ButtonHome": "Галоўная",
|
||||
"ButtonIssues": "Праблемы",
|
||||
"ButtonJumpBackward": "Перайсці назад",
|
||||
"ButtonJumpForward": "Перайсці наперад",
|
||||
"ButtonLatest": "Апошняе",
|
||||
"ButtonLibrary": "Бібліятэка",
|
||||
"ButtonLogout": "Выйсці",
|
||||
"ButtonLookup": "",
|
||||
"ButtonManageTracks": "Кіраванне дарожкамі",
|
||||
"ButtonMapChapterTitles": "Супаставіць назвы раздзелаў",
|
||||
"ButtonMatchAllAuthors": "Супадзенне ўсіх аўтараў",
|
||||
"ButtonMatchBooks": "Падбор кніг",
|
||||
"ButtonNevermind": "Няважна",
|
||||
"ButtonNext": "Далей",
|
||||
"ButtonNextChapter": "Наступны раздзел",
|
||||
"ButtonNextItemInQueue": "Наступны элемент у чарзе",
|
||||
"ButtonOk": "Добра",
|
||||
"ButtonOpenFeed": "Адкрыць стужку",
|
||||
"ButtonOpenManager": "Адкрыць менеджар",
|
||||
"ButtonPause": "Паўза",
|
||||
"ButtonPlay": "Прайграць",
|
||||
"ButtonPlayAll": "Прайграць усё",
|
||||
"ButtonPlaying": "Прайграваецца",
|
||||
"ButtonPlaylists": "Плэйлісты",
|
||||
"ButtonPrevious": "Папярэдні",
|
||||
"ButtonPreviousChapter": "Папярэдні раздзел",
|
||||
"ButtonProbeAudioFile": "Праверыць аўдыяфайл",
|
||||
"ButtonPurgeAllCache": "Ачысціць увесь кэш",
|
||||
"ButtonPurgeItemsCache": "Ачысціць кэш элементаў",
|
||||
"ButtonQueueAddItem": "Дадаць у чаргу",
|
||||
"ButtonQueueRemoveItem": "Выдаліць з чаргі",
|
||||
"ButtonQuickEmbed": "Хуткае ўбудаванне",
|
||||
"ButtonQuickEmbedMetadata": "Хуткае ўбудаванне метаданых",
|
||||
"ButtonQuickMatch": "Хуткі пошук",
|
||||
"ButtonReScan": "Паўторнае сканаванне",
|
||||
"ButtonRead": "Чытаць",
|
||||
"ButtonRefresh": "Абнавіць",
|
||||
"ButtonRemove": "Выдаліць",
|
||||
"ButtonRemoveAll": "Выдаліць усе",
|
||||
"ButtonRemoveAllLibraryItems": "Выдаліць усе элементы бібліятэкі",
|
||||
"ButtonRemoveFromContinueListening": "Выдаліць з Працягваць слухаць",
|
||||
"ButtonRemoveFromContinueReading": "Выдаліць з Працягваць чытанне",
|
||||
"ButtonRemoveSeriesFromContinueSeries": "Выдаліць серыю з Працягваць серыю",
|
||||
"ButtonReset": "Скінуць",
|
||||
"ButtonResetToDefault": "Скінуць па змаўчанні",
|
||||
"ButtonRestore": "Аднавіць",
|
||||
"ButtonSave": "Захаваць",
|
||||
"ButtonSaveAndClose": "Захаваць і зачыніць",
|
||||
"ButtonSaveTracklist": "Захаваць спіс трэкаў",
|
||||
"ButtonScan": "Сканаваць",
|
||||
"ButtonScanLibrary": "Сканіраваць бібліятэку",
|
||||
"ButtonScrollLeft": "Пракруціць улева",
|
||||
"ButtonScrollRight": "Пракруціць направа",
|
||||
"ButtonSearch": "Пошук",
|
||||
"ButtonSelectFolderPath": "Выбраць шлях да тэчкі",
|
||||
"ButtonSeries": "Серыі",
|
||||
"ButtonSetChaptersFromTracks": "Усталяваць раздзелы з трэкаў",
|
||||
"ButtonShare": "Падзяліцца",
|
||||
"ButtonStartM4BEncode": "Пачаць кадзіраванне ў M4B",
|
||||
"ButtonStartMetadataEmbed": "Пачаць убудаванне метаданых",
|
||||
"ButtonStats": "Статыстыка",
|
||||
"ButtonSubmit": "Адправіць",
|
||||
"ButtonTest": "Тэст",
|
||||
"ButtonUnlinkOpenId": "Адвязаць OpenID",
|
||||
"ButtonUpload": "Загрузіць",
|
||||
"ButtonUploadBackup": "Загрузіць рэзервовую копію",
|
||||
"ButtonUploadCover": "Загрузіць вокладку",
|
||||
"ButtonUploadOPMLFile": "Загрузіць OPML файл",
|
||||
"ButtonUserDelete": "Выдаліць карыстальніка {0}",
|
||||
"ButtonUserEdit": "Рэдагаваць карыстальніка {0}",
|
||||
"ButtonViewAll": "Прагледзець усе",
|
||||
"ButtonYes": "Так",
|
||||
"ErrorUploadFetchMetadataAPI": "Памылка пры атрыманні метададзеных",
|
||||
"ErrorUploadFetchMetadataNoResults": "Не ўдалося атрымаць метададзеныя – паспрабуйце абнавіць назву і/або аўтара",
|
||||
"ErrorUploadLacksTitle": "Павінна быць назва",
|
||||
"HeaderAccount": "Уліковы запіс",
|
||||
"HeaderAddCustomMetadataProvider": "Дадаць карыстальніцкага пастаўшчыка метаданных",
|
||||
"HeaderAdvanced": "Дадаткова",
|
||||
"HeaderAppriseNotificationSettings": "Налады апавяшчэнняў Apprise",
|
||||
"HeaderAudioTracks": "Аўдыядарожкі",
|
||||
"HeaderAudiobookTools": "Сродкі кіравання файламі аўдыякніг",
|
||||
"HeaderAuthentication": "Аўтэнтыфікацыя",
|
||||
"HeaderBackups": "Рэзервовыя копіі",
|
||||
"HeaderChangePassword": "Змяніць пароль",
|
||||
"HeaderChapters": "Раздзелы",
|
||||
"HeaderChooseAFolder": "Выбраць тэчку",
|
||||
"HeaderCollection": "Калекцыя",
|
||||
"HeaderCollectionItems": "Элементы калекцыі",
|
||||
"HeaderCover": "Вокладка",
|
||||
"HeaderCurrentDownloads": "Бягучыя спампоўкі",
|
||||
"HeaderCustomMessageOnLogin": "Карыстальніцкае паведамленне пры ўваходзе",
|
||||
"HeaderCustomMetadataProviders": "Карыстальніцкія крыніцы метададзеных",
|
||||
"HeaderDetails": "Падрабязнасці",
|
||||
"HeaderDownloadQueue": "Чарга спамповак",
|
||||
"HeaderEbookFiles": "Файлы электронных кніг",
|
||||
"HeaderEmail": "Электронная пошта",
|
||||
"HeaderEmailSettings": "Налады электроннай пошты",
|
||||
"HeaderEpisodes": "Эпізоды",
|
||||
"HeaderEreaderDevices": "Прылады для чытання",
|
||||
"HeaderEreaderSettings": "Налады прылады для чытання",
|
||||
"HeaderFiles": "Файлы",
|
||||
"HeaderFindChapters": "Знайсці раздзелы",
|
||||
"HeaderIgnoredFiles": "Ігнараваныя файлы",
|
||||
"HeaderItemFiles": "Файлы элементаў",
|
||||
"HeaderItemMetadataUtils": "Утыліты для метададзеных элементаў",
|
||||
"HeaderLastListeningSession": "Апошні сеанс праслухоўвання",
|
||||
"HeaderLatestEpisodes": "Апошнія эпізоды",
|
||||
"HeaderLibraries": "Бібліятэкі",
|
||||
"HeaderLibraryFiles": "Файлы бібліятэкі",
|
||||
"HeaderLibraryStats": "Статыстыка бібліятэкі",
|
||||
"HeaderListeningSessions": "Сеансы праслухоўвання",
|
||||
"HeaderListeningStats": "Статыстыка праслухоўвання",
|
||||
"HeaderLogin": "Уваход",
|
||||
"HeaderLogs": "Журналы",
|
||||
"HeaderManageGenres": "Кіраванне жанрамі",
|
||||
"HeaderManageTags": "Кіраванне тэгамі",
|
||||
"HeaderMapDetails": "Падрабязнасці адлюстравання",
|
||||
"HeaderNewAccount": "Новы ўліковы запіс",
|
||||
"HeaderNewLibrary": "Новая бібліятэка",
|
||||
"HeaderNotificationCreate": "Стварыць апавяшчэнне",
|
||||
"HeaderNotificationUpdate": "Абнавіць апавяшчэнне",
|
||||
"HeaderNotifications": "Апавяшчэнні",
|
||||
"HeaderOpenListeningSessions": "Адкрыць сеансы праслухоўвання",
|
||||
"HeaderScheduleEpisodeDownloads": "Расклад аўтаматычных спамповак эпізодаў",
|
||||
"HeaderSettings": "Налады",
|
||||
"HeaderSettingsDisplay": "Дысплей",
|
||||
"HeaderSettingsExperimental": "Эксперыментальныя функцыі",
|
||||
"HeaderSettingsGeneral": "Агульныя",
|
||||
"HeaderSettingsScanner": "Сканер",
|
||||
"HeaderSettingsWebClient": "Вэб-кліент",
|
||||
"HeaderStatsTop10Authors": "10 лепшых аўтараў",
|
||||
"HeaderStatsTop5Genres": "5 лепшых жанраў",
|
||||
"HeaderTableOfContents": "Змест",
|
||||
"HeaderTools": "Інструменты",
|
||||
"HeaderUpdateAccount": "Абнавіць уліковы запіс",
|
||||
"LabelAccountType": "Тып уліковага запіса",
|
||||
"LabelAccountTypeAdmin": "Адміністратар",
|
||||
"LabelAccountTypeGuest": "Госць",
|
||||
"LabelAccountTypeUser": "Карыстальнік",
|
||||
"LabelAudioBitrate": "Бітрэйт аўдыё (напрыклад, 128к)",
|
||||
"LabelAudioChannels": "Аўдыёканалы (1 або 2)",
|
||||
"LabelAudioCodec": "Аўдыёкодэк",
|
||||
"LabelAutoDownloadEpisodes": "Аўтаматычнае спампаванне эпізодаў",
|
||||
"LabelBackupAudioFiles": "Рэзервовае капіраванне аўдыёфайлаў",
|
||||
"LabelContinueListening": "Працягваць слухаць",
|
||||
"LabelDownload": "Спампаваць",
|
||||
"LabelDownloadNEpisodes": "Спампована {0} эпізодаў",
|
||||
"LabelDownloadable": "Спампоўваецца",
|
||||
"LabelEncodingBackupLocation": "Рэзервовая копія вашых арыгінальных аўдыёфайлаў будзе захавана ў:",
|
||||
"LabelEncodingChaptersNotEmbedded": "Раздзелы не ўбудаваны ў шматдарожкавыя аўдыякнігі.",
|
||||
"LabelEncodingFinishedM4B": "Гатовы файл M4B будзе змешчаны ў вашу тэчку з аўдыякнігамі па адрасе:",
|
||||
"LabelEncodingInfoEmbedded": "Метаданыя будуць убудаваны ў аўдыядарожкі ўнутры вашай тэчкі з аўдыякнігамі.",
|
||||
"LabelMaxEpisodesToDownload": "Максімальная колькасць эпізодаў для спампоўкі. Выкарыстоўвайце 0 для неабмежаванай колькасці.",
|
||||
"LabelMaxEpisodesToDownloadPerCheck": "Максімальная колькасць новых эпізодаў для спампоўкі за праверку",
|
||||
"LabelMaxEpisodesToKeepHelp": "Значэнне 0 не ўстанаўлівае максімальнага абмежавання. Пасля аўтаматычнай спампоўкі новага эпізоду будзе выдалены самы стары эпізод, калі ў вас больш за X эпізодаў. Пры кожнай новай спампоўцы будзе выдаляцца толькі 1 эпізод.",
|
||||
"LabelPermissionsDownload": "Можна спампаваць",
|
||||
"LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працягваць слухаць",
|
||||
"LabelShareDownloadableHelp": "Дазваляе карыстальнікам, якія маюць спасылку на доступ, спампаваць ZIP-файл элемента бібліятэкі.",
|
||||
"LabelStatsAudioTracks": "Аўдыядарожкі",
|
||||
"LabelTracks": "Дарожкі",
|
||||
"MessageConfirmRemoveListeningSessions": "Вы ўпэўнены, што жадаеце выдаліць {0} сеансаў праслухоўвання?",
|
||||
"MessageDownloadingEpisode": "Спампоўка эпізоду",
|
||||
"MessageEpisodesQueuedForDownload": "{0} эпізод(аў) у чарзе для спампоўкі",
|
||||
"MessageNoDownloadsInProgress": "Зараз няма актыўных спамповак",
|
||||
"MessageNoDownloadsQueued": "Няма спамповак у чарзе",
|
||||
"MessageNoListeningSessions": "Няма сеансаў праслухоўвання",
|
||||
"MessageTaskDownloadingEpisodeDescription": "Спампоўка эпізоду \"{0}\"",
|
||||
"NotificationOnEpisodeDownloadedDescription": "Выклікаецца, калі эпізод падкаста аўтаматычна спампоўваецца",
|
||||
"ToastAccountUpdateSuccess": "Уліковы запіс абноўлены",
|
||||
"ToastEpisodeDownloadQueueClearFailed": "Не ўдалося ачысціць чаргу",
|
||||
"ToastEpisodeDownloadQueueClearSuccess": "Чарга спампоўкі эпізодаў ачышчана",
|
||||
"ToastInvalidMaxEpisodesToDownload": "Няправільная максімальная колькасць эпізодаў для спампоўкі",
|
||||
"ToastNewUserCreatedFailed": "Не ўдалося стварыць уліковы запіс: \"{0}\"",
|
||||
"ToastNewUserCreatedSuccess": "Новы ўліковы запіс створаны",
|
||||
"ToastUserPasswordMustChange": "Новы пароль не можа супадаць са старым",
|
||||
"ToastUserRootRequireName": "Неабходна ўвесці імя карыстальніка адміністратара"
|
||||
}
|
||||
@@ -629,7 +629,6 @@
|
||||
"MessageItemsSelected": "{0} избрани",
|
||||
"MessageItemsUpdated": "{0} елемента обновени",
|
||||
"MessageJoinUsOn": "Присъединете се към нас",
|
||||
"MessageListeningSessionsInTheLastYear": "{0} слушателски сесии през последната година",
|
||||
"MessageLoading": "Зареждане...",
|
||||
"MessageLoadingFolders": "Зареждане на Папки...",
|
||||
"MessageM4BFailed": "M4B Провалено!",
|
||||
@@ -726,10 +725,8 @@
|
||||
"ToastBookmarkCreateFailed": "Неуспешно създаване на отметка",
|
||||
"ToastBookmarkCreateSuccess": "Отметката е създадена",
|
||||
"ToastBookmarkRemoveSuccess": "Отметката е премахната",
|
||||
"ToastBookmarkUpdateSuccess": "Отметката е обновена",
|
||||
"ToastChaptersHaveErrors": "Главите имат грешки",
|
||||
"ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия",
|
||||
"ToastCollectionItemsRemoveSuccess": "Елемент(и) премахнати от колекция",
|
||||
"ToastCollectionRemoveSuccess": "Колекцията е премахната",
|
||||
"ToastCollectionUpdateSuccess": "Колекцията е обновена",
|
||||
"ToastItemCoverUpdateSuccess": "Корицата на елемента е обновена",
|
||||
|
||||
@@ -88,6 +88,8 @@
|
||||
"ButtonSaveTracklist": "ট্র্যাকলিস্ট সংরক্ষণ করুন",
|
||||
"ButtonScan": "স্ক্যান",
|
||||
"ButtonScanLibrary": "স্ক্যান লাইব্রেরি",
|
||||
"ButtonScrollLeft": "বাম দিকে স্ক্রল করুন",
|
||||
"ButtonScrollRight": "ডানদিকে স্ক্রল করুন",
|
||||
"ButtonSearch": "অনুসন্ধান",
|
||||
"ButtonSelectFolderPath": "ফোল্ডারের পথ নির্বাচন করুন",
|
||||
"ButtonSeries": "সিরিজ",
|
||||
@@ -190,6 +192,7 @@
|
||||
"HeaderSettingsExperimental": "পরীক্ষামূলক ফিচার",
|
||||
"HeaderSettingsGeneral": "সাধারণ",
|
||||
"HeaderSettingsScanner": "স্ক্যানার",
|
||||
"HeaderSettingsWebClient": "ওয়েব ক্লায়েন্ট",
|
||||
"HeaderSleepTimer": "স্লিপ টাইমার",
|
||||
"HeaderStatsLargestItems": "সবচেয়ে বড় আইটেম",
|
||||
"HeaderStatsLongestItems": "দীর্ঘতম আইটেম (ঘন্টা)",
|
||||
@@ -297,6 +300,7 @@
|
||||
"LabelDiscover": "আবিষ্কার",
|
||||
"LabelDownload": "ডাউনলোড করুন",
|
||||
"LabelDownloadNEpisodes": "{0}টি পর্ব ডাউনলোড করুন",
|
||||
"LabelDownloadable": "ডাউনলোডযোগ্য",
|
||||
"LabelDuration": "সময়কাল",
|
||||
"LabelDurationComparisonExactMatch": "(সঠিক মিল)",
|
||||
"LabelDurationComparisonLonger": "({0} দীর্ঘ)",
|
||||
@@ -542,6 +546,7 @@
|
||||
"LabelServerYearReview": "সার্ভারের বাৎসরিক পর্যালোচনা ({0})",
|
||||
"LabelSetEbookAsPrimary": "প্রাথমিক হিসাবে সেট করুন",
|
||||
"LabelSetEbookAsSupplementary": "পরিপূরক হিসেবে সেট করুন",
|
||||
"LabelSettingsAllowIframe": "আইফ্রেমে এম্বেড করার অনুমতি দিন",
|
||||
"LabelSettingsAudiobooksOnly": "শুধুমাত্র অডিও বই",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "এই সেটিংটি সক্ষম করা ই-বই ফাইলগুলিকে উপেক্ষা করবে যদি না সেগুলি একটি অডিওবই ফোল্ডারের মধ্যে থাকে যে ক্ষেত্রে সেগুলিকে সম্পূরক ই-বই হিসাবে সেট করা হবে",
|
||||
"LabelSettingsBookshelfViewHelp": "কাঠের তাক সহ স্কুমরফিক ডিজাইন",
|
||||
@@ -584,6 +589,7 @@
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "ডিফল্টরূপে মেটাডেটা ফাইলগুলি /মেটাডাটা/আইটেমগুলি -এ সংরক্ষণ করা হয়, এই সেটিংটি সক্ষম করলে মেটাডেটা ফাইলগুলি আপনার লাইব্রেরি আইটেম ফোল্ডারে সংরক্ষণ করা হবে",
|
||||
"LabelSettingsTimeFormat": "সময় বিন্যাস",
|
||||
"LabelShare": "শেয়ার করুন",
|
||||
"LabelShareDownloadableHelp": "শেয়ার লিঙ্ক সহ ব্যবহারকারীদের লাইব্রেরি আইটেমের একটি জিপ ফাইল ডাউনলোড করার অনুমতি দিন।",
|
||||
"LabelShareOpen": "শেয়ার খোলা",
|
||||
"LabelShareURL": "শেয়ার ইউআরএল",
|
||||
"LabelShowAll": "সব দেখান",
|
||||
@@ -592,6 +598,8 @@
|
||||
"LabelSize": "আকার",
|
||||
"LabelSleepTimer": "স্লিপ টাইমার",
|
||||
"LabelSlug": "স্লাগ",
|
||||
"LabelSortAscending": "আরোহী",
|
||||
"LabelSortDescending": "অবরোহী",
|
||||
"LabelStart": "শুরু",
|
||||
"LabelStartTime": "শুরুর সময়",
|
||||
"LabelStarted": "শুরু হয়েছে",
|
||||
@@ -679,6 +687,8 @@
|
||||
"LabelViewPlayerSettings": "প্লেয়ার সেটিংস দেখুন",
|
||||
"LabelViewQueue": "প্লেয়ার সারি দেখুন",
|
||||
"LabelVolume": "ভলিউম",
|
||||
"LabelWebRedirectURLsDescription": "লগইন করার পরে ওয়েব অ্যাপে পুনঃনির্দেশের অনুমতি দেওয়ার জন্য আপনার OAuth প্রদানকারীতে এই URLগুলোকে অনুমোদন করুন:",
|
||||
"LabelWebRedirectURLsSubfolder": "রিডাইরেক্ট URL এর জন্য সাবফোল্ডার",
|
||||
"LabelWeekdaysToRun": "চলতে হবে সপ্তাহের দিন",
|
||||
"LabelXBooks": "{0}টি বই",
|
||||
"LabelXItems": "{0}টি আইটেম",
|
||||
@@ -763,7 +773,6 @@
|
||||
"MessageItemsSelected": "{0}টি আইটেম নির্বাচিত",
|
||||
"MessageItemsUpdated": "{0}টি আইটেম আপডেট করা হয়েছে",
|
||||
"MessageJoinUsOn": "আমাদের সাথে যোগ দিন",
|
||||
"MessageListeningSessionsInTheLastYear": "গত বছরে {0}টি শোনার সেশন",
|
||||
"MessageLoading": "লোড হচ্ছে.।",
|
||||
"MessageLoadingFolders": "ফোল্ডার লোড হচ্ছে...",
|
||||
"MessageLogsDescription": "লগগুলি JSON ফাইল হিসাবে <code>/metadata/logs</code>-এ সংরক্ষণ করা হয়। ক্র্যাশ লগগুলি <code>/metadata/logs/crash_logs.txt</code>-এ সংরক্ষণ করা হয়।",
|
||||
@@ -943,7 +952,6 @@
|
||||
"ToastBookmarkCreateFailed": "বুকমার্ক তৈরি করতে ব্যর্থ",
|
||||
"ToastBookmarkCreateSuccess": "বুকমার্ক যোগ করা হয়েছে",
|
||||
"ToastBookmarkRemoveSuccess": "বুকমার্ক সরানো হয়েছে",
|
||||
"ToastBookmarkUpdateSuccess": "বুকমার্ক আপডেট করা হয়েছে",
|
||||
"ToastCachePurgeFailed": "ক্যাশে পরিষ্কার করতে ব্যর্থ হয়েছে",
|
||||
"ToastCachePurgeSuccess": "ক্যাশে সফলভাবে পরিষ্কার করা হয়েছে",
|
||||
"ToastChaptersHaveErrors": "অধ্যায়ে ত্রুটি আছে",
|
||||
@@ -951,8 +959,6 @@
|
||||
"ToastChaptersRemoved": "অধ্যায়গুলো মুছে ফেলা হয়েছে",
|
||||
"ToastChaptersUpdated": "অধ্যায় আপডেট করা হয়েছে",
|
||||
"ToastCollectionItemsAddFailed": "আইটেম(গুলি) সংগ্রহে যোগ করা ব্যর্থ হয়েছে",
|
||||
"ToastCollectionItemsAddSuccess": "আইটেম(গুলি) সংগ্রহে যোগ করা সফল হয়েছে",
|
||||
"ToastCollectionItemsRemoveSuccess": "আইটেম(গুলি) সংগ্রহ থেকে সরানো হয়েছে",
|
||||
"ToastCollectionRemoveSuccess": "সংগ্রহ সরানো হয়েছে",
|
||||
"ToastCollectionUpdateSuccess": "সংগ্রহ আপডেট করা হয়েছে",
|
||||
"ToastCoverUpdateFailed": "কভার আপডেট ব্যর্থ হয়েছে",
|
||||
|
||||
1026
client/strings/ca.json
Normal file
1026
client/strings/ca.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -88,6 +88,8 @@
|
||||
"ButtonSaveTracklist": "Uložit seznam skladeb",
|
||||
"ButtonScan": "Prohledat",
|
||||
"ButtonScanLibrary": "Prohledat Knihovnu",
|
||||
"ButtonScrollLeft": "Posunout vlevo",
|
||||
"ButtonScrollRight": "Posunout vpravo",
|
||||
"ButtonSearch": "Hledat",
|
||||
"ButtonSelectFolderPath": "Vybrat cestu ke složce",
|
||||
"ButtonSeries": "Série",
|
||||
@@ -190,6 +192,7 @@
|
||||
"HeaderSettingsExperimental": "Experimentální funkce",
|
||||
"HeaderSettingsGeneral": "Obecné",
|
||||
"HeaderSettingsScanner": "Skener",
|
||||
"HeaderSettingsWebClient": "Webový klient",
|
||||
"HeaderSleepTimer": "Časovač vypnutí",
|
||||
"HeaderStatsLargestItems": "Největší položky",
|
||||
"HeaderStatsLongestItems": "Nejdelší položky (hod.)",
|
||||
@@ -231,7 +234,7 @@
|
||||
"LabelAppend": "Připojit",
|
||||
"LabelAudioBitrate": "Bitový tok zvuku (např. 128k)",
|
||||
"LabelAudioChannels": "Zvukové kanály (1 nebo 2)",
|
||||
"LabelAudioCodec": "Kodek audia",
|
||||
"LabelAudioCodec": "Audio Kodek",
|
||||
"LabelAuthor": "Autor",
|
||||
"LabelAuthorFirstLast": "Autor (jméno a příjmení)",
|
||||
"LabelAuthorLastFirst": "Autor (příjmení a jméno)",
|
||||
@@ -264,6 +267,7 @@
|
||||
"LabelChapters": "Kapitoly",
|
||||
"LabelChaptersFound": "Kapitoly nalezeny",
|
||||
"LabelClickForMoreInfo": "Klikněte pro více informací",
|
||||
"LabelClickToUseCurrentValue": "Klikni pro použití aktuální hodnoty",
|
||||
"LabelClosePlayer": "Zavřít přehrávač",
|
||||
"LabelCodec": "Kodek",
|
||||
"LabelCollapseSeries": "Sbalit sérii",
|
||||
@@ -296,6 +300,7 @@
|
||||
"LabelDiscover": "Objevit",
|
||||
"LabelDownload": "Stáhnout",
|
||||
"LabelDownloadNEpisodes": "Stáhnout {0} epizody",
|
||||
"LabelDownloadable": "Ke stažení",
|
||||
"LabelDuration": "Délka trvání",
|
||||
"LabelDurationComparisonExactMatch": "(přesná shoda)",
|
||||
"LabelDurationComparisonLonger": "({0} delší)",
|
||||
@@ -313,12 +318,25 @@
|
||||
"LabelEmailSettingsTestAddress": "Testovací adresa",
|
||||
"LabelEmbeddedCover": "Vložená obálka",
|
||||
"LabelEnable": "Povolit",
|
||||
"LabelEncodingBackupLocation": "Záloha původních audio souborů bude uložena v:",
|
||||
"LabelEncodingChaptersNotEmbedded": "Kapitoly nejsou vloženy ve vícestopých audioknihách.",
|
||||
"LabelEncodingClearItemCache": "Nezapomeňte pravidelně promazávat mezipaměť položek.",
|
||||
"LabelEncodingFinishedM4B": "Výsledné M4B bude uloženo do složky s audioknihou v:",
|
||||
"LabelEncodingInfoEmbedded": "Metadata budou vložena do audio stop ve složce s audioknihou.",
|
||||
"LabelEncodingStartedNavigation": "Po spuštění úlohy můžete opustit tuto stránku.",
|
||||
"LabelEncodingTimeWarning": "Encoding může zabrat až 30 minut.",
|
||||
"LabelEncodingWarningAdvancedSettings": "Varování: Neměňte toto nastavení pokud neznáte možnosti encodingu ffmpeg.",
|
||||
"LabelEncodingWatcherDisabled": "Pokud máte zakázaný watcher, budete po skončení muset znovu naskenovat tuto audioknihu.",
|
||||
"LabelEnd": "Konec",
|
||||
"LabelEndOfChapter": "Konec kapitoly",
|
||||
"LabelEpisode": "Epizoda",
|
||||
"LabelEpisodeNotLinkedToRssFeed": "Epizoda není propojená s RSS feed",
|
||||
"LabelEpisodeNumber": "Epizoda #{0}",
|
||||
"LabelEpisodeTitle": "Název epizody",
|
||||
"LabelEpisodeType": "Typ epizody",
|
||||
"LabelEpisodeUrlFromRssFeed": "URL epizody z RSS feed",
|
||||
"LabelEpisodes": "Epizody",
|
||||
"LabelEpisodic": "Epizodické",
|
||||
"LabelExample": "Příklad",
|
||||
"LabelExpandSeries": "Rozbalit série",
|
||||
"LabelExpandSubSeries": "Rozbalit podsérie",
|
||||
@@ -346,6 +364,7 @@
|
||||
"LabelFontScale": "Měřítko písma",
|
||||
"LabelFontStrikethrough": "Přeškrtnutí",
|
||||
"LabelFormat": "Formát",
|
||||
"LabelFull": "Plné",
|
||||
"LabelGenre": "Žánr",
|
||||
"LabelGenres": "Žánry",
|
||||
"LabelHardDeleteFile": "Trvale smazat soubor",
|
||||
@@ -388,6 +407,7 @@
|
||||
"LabelLess": "Méně",
|
||||
"LabelLibrariesAccessibleToUser": "Knihovny přístupné uživateli",
|
||||
"LabelLibrary": "Knihovna",
|
||||
"LabelLibraryFilterSublistEmpty": "Žádné {0}",
|
||||
"LabelLibraryItem": "Položka knihovny",
|
||||
"LabelLibraryName": "Název knihovny",
|
||||
"LabelLimit": "Omezit",
|
||||
@@ -400,6 +420,10 @@
|
||||
"LabelLowestPriority": "Nejnižší priorita",
|
||||
"LabelMatchExistingUsersBy": "Přiřadit stávající uživatele podle",
|
||||
"LabelMatchExistingUsersByDescription": "Slouží k propojení stávajících uživatelů. Po propojení budou uživatelé přiřazeni k jedinečnému ID od poskytovatele SSO.",
|
||||
"LabelMaxEpisodesToDownload": "Maximální # epizod pro stažení. Použijte 0 pro bez omezení.",
|
||||
"LabelMaxEpisodesToDownloadPerCheck": "Maximální počet nových epizod ke stažení při jedné kontrole",
|
||||
"LabelMaxEpisodesToKeep": "Maximální počet epizod k zachování",
|
||||
"LabelMaxEpisodesToKeepHelp": "Hodnotou 0 není nastaven žádný maximální limit. Po automatickém stažení nové epizody se odstraní nejstarší epizoda, pokud máte více než X epizod. Při každém novém stažení se odstraní pouze 1 epizoda.",
|
||||
"LabelMediaPlayer": "Přehrávač médií",
|
||||
"LabelMediaType": "Typ média",
|
||||
"LabelMetaTag": "Metaznačka",
|
||||
@@ -445,12 +469,14 @@
|
||||
"LabelOpenIDGroupClaimDescription": "Název požadavku OpenID, který obsahuje seznam uživatelských skupin. Běžně se označuje jako <code>groups</code>. <b>Je-li nakonfigurováno</b>, plikace automaticky přiřadí role na základě členství uživatele ve skupinách, pokud jsou tyto skupiny v požadavku pojmenovány case-insensitive 'admin', 'user' nebo 'guest'. Požadavek by měl obsahovat seznam, a pokud uživatel patří do více skupin, aplikace přiřadí roli odpovídající nejvyšší úrovni práva přístupu. Pokud žádná skupina není shodná, bude přístup odepřen.",
|
||||
"LabelOpenRSSFeed": "Otevřít RSS kanál",
|
||||
"LabelOverwrite": "Přepsat",
|
||||
"LabelPaginationPageXOfY": "Strana {0} z {1}",
|
||||
"LabelPassword": "Heslo",
|
||||
"LabelPath": "Cesta",
|
||||
"LabelPermanent": "Trvalé",
|
||||
"LabelPermissionsAccessAllLibraries": "Má přístup ke všem knihovnám",
|
||||
"LabelPermissionsAccessAllTags": "Má přístup ke všem značkám",
|
||||
"LabelPermissionsAccessExplicitContent": "Má přístup k explicitnímu obsahu",
|
||||
"LabelPermissionsCreateEreader": "Může vytvořit Ereader",
|
||||
"LabelPermissionsDelete": "Může mazat",
|
||||
"LabelPermissionsDownload": "Může stahovat",
|
||||
"LabelPermissionsUpdate": "Může aktualizovat",
|
||||
@@ -474,6 +500,8 @@
|
||||
"LabelPubDate": "Datum vydání",
|
||||
"LabelPublishYear": "Rok vydání",
|
||||
"LabelPublishedDate": "Vydáno {0}",
|
||||
"LabelPublishedDecade": "Publikováno (dekáda)",
|
||||
"LabelPublishedDecades": "Publikováno (dekády)",
|
||||
"LabelPublisher": "Vydavatel",
|
||||
"LabelPublishers": "Vydavatelé",
|
||||
"LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka",
|
||||
@@ -493,24 +521,32 @@
|
||||
"LabelRedo": "Přepracovat",
|
||||
"LabelRegion": "Region",
|
||||
"LabelReleaseDate": "Datum vydání",
|
||||
"LabelRemoveAllMetadataAbs": "Odebrat všechny soubory metadata.abs",
|
||||
"LabelRemoveAllMetadataJson": "Smazat všechny soubory metadata.json",
|
||||
"LabelRemoveCover": "Odstranit obálku",
|
||||
"LabelRemoveMetadataFile": "Odstranit soubory metadat ve složkách položek knihovny",
|
||||
"LabelRemoveMetadataFileHelp": "Odstraníte všechny soubory metadata.json a metadata.abs ve svých složkách {0}.",
|
||||
"LabelRowsPerPage": "Řádky na stránku",
|
||||
"LabelSearchTerm": "Vyhledat termín",
|
||||
"LabelSearchTitle": "Vyhledat název",
|
||||
"LabelSearchTitleOrASIN": "Vyhledat název nebo ASIN",
|
||||
"LabelSeason": "Sezóna",
|
||||
"LabelSeasonNumber": "Sezóna č.{0}",
|
||||
"LabelSelectAll": "Vybrat vše",
|
||||
"LabelSelectAllEpisodes": "Vybrat všechny epizody",
|
||||
"LabelSelectEpisodesShowing": "Vyberte {0} epizody, které se zobrazují",
|
||||
"LabelSelectUsers": "Vybrat uživatele",
|
||||
"LabelSendEbookToDevice": "Odeslat e-knihu do...",
|
||||
"LabelSequence": "Sekvence",
|
||||
"LabelSerial": "Sériové",
|
||||
"LabelSeries": "Série",
|
||||
"LabelSeriesName": "Název série",
|
||||
"LabelSeriesProgress": "Průběh série",
|
||||
"LabelServerLogLevel": "Úroveň protokolu serveru",
|
||||
"LabelServerYearReview": "Přehled roku na serveru ({0})",
|
||||
"LabelSetEbookAsPrimary": "Nastavit jako primární",
|
||||
"LabelSetEbookAsSupplementary": "Nastavit jako doplňkové",
|
||||
"LabelSettingsAllowIframe": "Povolit vložení do rámce iframe",
|
||||
"LabelSettingsAudiobooksOnly": "Pouze audioknihy",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "Povolením tohoto nastavení budou soubory e-knih ignorovány, pokud nejsou ve složce audioknih, v takovém případě budou nastaveny jako doplňkové e-knihy",
|
||||
"LabelSettingsBookshelfViewHelp": "Skeumorfní design s dřevěnými policemi",
|
||||
@@ -532,9 +568,12 @@
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Série, které mají jedinou knihu, budou skryty na stránce série a na domovské stránce.",
|
||||
"LabelSettingsHomePageBookshelfView": "Domovská stránka používá zobrazení police s knihami",
|
||||
"LabelSettingsLibraryBookshelfView": "Knihovna používá zobrazení police s knihami",
|
||||
"LabelSettingsLibraryMarkAsFinishedPercentComplete": "Procento dokončení je vyšší než",
|
||||
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Zbývající čas je kratší než (sekund)",
|
||||
"LabelSettingsLibraryMarkAsFinishedWhen": "Označit položku médií jako dokončenou, když",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Přeskočit předchozí knihy v pokračování série",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Polička Pokračovat v sérii na domovské stránce zobrazuje první nezačatou knihu v sériích, které mají alespoň jednu knihu dokončenou a žádnou rozečtenou. Povolením tohoto nastavení budou série pokračovat od poslední dokončené knihy namísto první nezačaté knihy.",
|
||||
"LabelSettingsParseSubtitles": "Analzyovat podtitul",
|
||||
"LabelSettingsParseSubtitles": "Analyzovat podtitul",
|
||||
"LabelSettingsParseSubtitlesHelp": "Rozparsovat podtitul z názvů složek audioknih.<br>Podtiul musí být oddělen znakem \" - \"<br>tj. \"Název knihy - Zde Podtitul\" má podtitul \"Zde podtitul\"",
|
||||
"LabelSettingsPreferMatchedMetadata": "Preferovat spárovaná metadata",
|
||||
"LabelSettingsPreferMatchedMetadataHelp": "Spárovaná data budou mít při použití funkce Rychlé párování přednost před údaji o položce. Ve výchozím nastavení funkce Rychlé párování pouze doplní chybějící údaje.",
|
||||
@@ -550,12 +589,17 @@
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Ve výchozím nastavení jsou soubory metadat uloženy v adresáři /metadata/items, povolením tohoto nastavení budou soubory metadat uloženy ve složkách položek knihovny",
|
||||
"LabelSettingsTimeFormat": "Formát času",
|
||||
"LabelShare": "Sdílet",
|
||||
"LabelShareDownloadableHelp": "Umožňuje uživatelům s odkazem na sdílení stáhnout soubor zip.",
|
||||
"LabelShareOpen": "Otevřít sdílení",
|
||||
"LabelShareURL": "Sdílet URL",
|
||||
"LabelShowAll": "Zobrazit vše",
|
||||
"LabelShowSeconds": "Zobrazit sekundy",
|
||||
"LabelShowSubtitles": "Zobrazit titulky",
|
||||
"LabelSize": "Velikost",
|
||||
"LabelSleepTimer": "Časovač vypnutí",
|
||||
"LabelSlug": "URL název",
|
||||
"LabelSortAscending": "Vzestupně",
|
||||
"LabelSortDescending": "Sestupně",
|
||||
"LabelStart": "Spustit",
|
||||
"LabelStartTime": "Čas Spuštění",
|
||||
"LabelStarted": "Spuštěno",
|
||||
@@ -594,6 +638,7 @@
|
||||
"LabelTimeDurationXMinutes": "{0} minut",
|
||||
"LabelTimeDurationXSeconds": "{0} sekund",
|
||||
"LabelTimeInMinutes": "Čas v minutách",
|
||||
"LabelTimeLeft": "{0} zbývá",
|
||||
"LabelTimeListened": "Čas poslechu",
|
||||
"LabelTimeListenedToday": "Čas poslechu dnes",
|
||||
"LabelTimeRemaining": "{0} zbývá",
|
||||
@@ -601,6 +646,7 @@
|
||||
"LabelTitle": "Název",
|
||||
"LabelToolsEmbedMetadata": "Vložit metadata",
|
||||
"LabelToolsEmbedMetadataDescription": "Vložit metadata do zvukových souborů včetně obálky a kapitol.",
|
||||
"LabelToolsM4bEncoder": "Enkodér M4B",
|
||||
"LabelToolsMakeM4b": "Vytvořit soubor audioknihy M4B",
|
||||
"LabelToolsMakeM4bDescription": "Vygenerovat soubor audioknihy M4B s vloženými metadaty, obálkou a kapitolami.",
|
||||
"LabelToolsSplitM4b": "Rozdělit M4B na MP3",
|
||||
@@ -613,6 +659,7 @@
|
||||
"LabelTracksMultiTrack": "Více stop",
|
||||
"LabelTracksNone": "Žádné stopy",
|
||||
"LabelTracksSingleTrack": "Jedna stopa",
|
||||
"LabelTrailer": "Upoutávka",
|
||||
"LabelType": "Typ",
|
||||
"LabelUnabridged": "Nezkráceno",
|
||||
"LabelUndo": "Zpět",
|
||||
@@ -624,10 +671,13 @@
|
||||
"LabelUpdateDetailsHelp": "Povolit přepsání existujících údajů o vybraných knihách, když je nalezena shoda",
|
||||
"LabelUpdatedAt": "Aktualizováno v",
|
||||
"LabelUploaderDragAndDrop": "Přetáhnout soubory nebo složky",
|
||||
"LabelUploaderDragAndDropFilesOnly": "Přetáhnout a upustit soubory",
|
||||
"LabelUploaderDropFiles": "Odstranit soubory",
|
||||
"LabelUploaderItemFetchMetadataHelp": "Automaticky načíst název, autora a sérii",
|
||||
"LabelUseAdvancedOptions": "Použít pokročilé možnosti",
|
||||
"LabelUseChapterTrack": "Použít stopu kapitoly",
|
||||
"LabelUseFullTrack": "Použít celou stopu",
|
||||
"LabelUseZeroForUnlimited": "Použijte 0 pro neomezené",
|
||||
"LabelUser": "Uživatel",
|
||||
"LabelUsername": "Uživatelské jméno",
|
||||
"LabelValue": "Hodnota",
|
||||
@@ -637,6 +687,8 @@
|
||||
"LabelViewPlayerSettings": "Zobrazit nastavení přehrávače",
|
||||
"LabelViewQueue": "Zobrazit frontu přehrávače",
|
||||
"LabelVolume": "Hlasitost",
|
||||
"LabelWebRedirectURLsDescription": "Autorizujte tyto adresy URL ve zprostředkovateli OAuth, abyste po přihlášení umožnili přesměrování zpět do webové aplikace:",
|
||||
"LabelWebRedirectURLsSubfolder": "Podsložka pro přesměrování adres URL",
|
||||
"LabelWeekdaysToRun": "Dny v týdnu ke spuštění",
|
||||
"LabelXBooks": "{0} knih",
|
||||
"LabelXItems": "{0} položky",
|
||||
@@ -674,6 +726,7 @@
|
||||
"MessageConfirmDeleteMetadataProvider": "Opravdu chcete vymazat vlastního poskytovatele metadat \"{0}\"?",
|
||||
"MessageConfirmDeleteNotification": "Opravdu chcete vymazat tuto notifikaci?",
|
||||
"MessageConfirmDeleteSession": "Opravdu chcete smazat tuto relaci?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Jste si jisti, že chcete vložit metadata do {0} zvukových souborů?",
|
||||
"MessageConfirmForceReScan": "Opravdu chcete vynutit opětovné prohledání?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Opravdu chcete označit všechny epizody jako dokončené?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Opravdu chcete označit všechny epizody jako nedokončené?",
|
||||
@@ -681,9 +734,11 @@
|
||||
"MessageConfirmMarkItemNotFinished": "Opravdu chcete označit \"{0}\" jako nedokončené?",
|
||||
"MessageConfirmMarkSeriesFinished": "Opravdu chcete označit všechny knihy z této série jako dokončené?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Opravdu chcete označit všechny knihy z této série jako nedokončené?",
|
||||
"MessageConfirmNotificationTestTrigger": "Spustit toto oznámení s testovacími daty?",
|
||||
"MessageConfirmPurgeCache": "Vyčistit mezipaměť odstraní celý adresář na adrese <code>/metadata/cache</code>. <br /><br />Určitě chcete odstranit adresář mezipaměti?",
|
||||
"MessageConfirmPurgeItemsCache": "Vyčištění mezipaměti položek odstraní celý adresář <code>/metadata/cache/items</code>.<br />Jste si jistí?",
|
||||
"MessageConfirmQuickEmbed": "Varování! Rychlé vložení nezálohuje vaše zvukové soubory. Ujistěte se, že máte zálohu zvukových souborů. <br><br>Chcete pokračovat?",
|
||||
"MessageConfirmQuickMatchEpisodes": "Pokud je nalezena shoda při rychlém párování epizod, dojde k přepsání podrobností. Aktualizovány budou pouze nespárované epizody. Jste si jisti?",
|
||||
"MessageConfirmReScanLibraryItems": "Opravdu chcete znovu prohledat {0} položky?",
|
||||
"MessageConfirmRemoveAllChapters": "Opravdu chcete odstranit všechny kapitoly?",
|
||||
"MessageConfirmRemoveAuthor": "Opravdu chcete odstranit autora \"{0}\"?",
|
||||
@@ -691,6 +746,7 @@
|
||||
"MessageConfirmRemoveEpisode": "Opravdu chcete odstranit epizodu \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Opravdu chcete odstranit {0} epizody?",
|
||||
"MessageConfirmRemoveListeningSessions": "Opravdu chcete odebrat {0} poslechových relací?",
|
||||
"MessageConfirmRemoveMetadataFiles": "Jste si jisti, že chcete odstranit všechny metadata.{0} soubory ve složkách s položkami ve vaší knihovně?",
|
||||
"MessageConfirmRemoveNarrator": "Opravdu chcete odebrat předčítání \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "Opravdu chcete odstranit svůj playlist \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "Opravdu chcete přejmenovat žánr \"{0}\" na \"{1}\" pro všechny položky?",
|
||||
@@ -702,10 +758,12 @@
|
||||
"MessageConfirmResetProgress": "Opravdu chcete zahodit svůj pokrok?",
|
||||
"MessageConfirmSendEbookToDevice": "Opravdu chcete odeslat e-knihu {0} {1}\" do zařízení \"{2}\"?",
|
||||
"MessageConfirmUnlinkOpenId": "Opravdu chcete odpojit tohoto uživatele z OpenID?",
|
||||
"MessageDaysListenedInTheLastYear": "{0} poslechových dní v minulém roce",
|
||||
"MessageDownloadingEpisode": "Stahuji epizodu",
|
||||
"MessageDragFilesIntoTrackOrder": "Přetáhněte soubory do správného pořadí stop",
|
||||
"MessageEmbedFailed": "Vložení selhalo!",
|
||||
"MessageEmbedFinished": "Vložení dokončeno!",
|
||||
"MessageEmbedQueue": "Zařazeno do fronty pro vložení metadat ({0} ve frontě)",
|
||||
"MessageEpisodesQueuedForDownload": "{0} Epizody zařazené do fronty ke stažení",
|
||||
"MessageEreaderDevices": "Aby bylo zajištěno doručení elektronických knih, může být nutné přidat výše uvedenou e-mailovou adresu jako platného odesílatele pro každé zařízení uvedené níže.",
|
||||
"MessageFeedURLWillBe": "URL zdroje bude {0}",
|
||||
@@ -716,7 +774,6 @@
|
||||
"MessageItemsSelected": "{0} vybraných položek",
|
||||
"MessageItemsUpdated": "{0} položky byly aktualizovány",
|
||||
"MessageJoinUsOn": "Přidejte se k nám",
|
||||
"MessageListeningSessionsInTheLastYear": "{0} poslechových relací za poslední rok",
|
||||
"MessageLoading": "Načítá se...",
|
||||
"MessageLoadingFolders": "Načítám složky...",
|
||||
"MessageLogsDescription": "Protokoly se ukládají do souborů JSON v <code>/metadata/logs</code>. Protokoly o pádech jsou uloženy v <code>/metadata/logs/crash_logs.txt</code>.",
|
||||
@@ -750,6 +807,7 @@
|
||||
"MessageNoLogs": "Žádné protokoly",
|
||||
"MessageNoMediaProgress": "Žádný průběh médií",
|
||||
"MessageNoNotifications": "Žádná oznámení",
|
||||
"MessageNoPodcastFeed": "Neplatný podcast: Žádný kanál",
|
||||
"MessageNoPodcastsFound": "Nebyly nalezeny žádné podcasty",
|
||||
"MessageNoResults": "Žádné výsledky",
|
||||
"MessageNoSearchResultsFor": "Nebyly nalezeny žádné výsledky hledání pro \"{0}\"",
|
||||
@@ -766,7 +824,11 @@
|
||||
"MessagePlaylistCreateFromCollection": "Vytvořit seznam skladeb z kolekce",
|
||||
"MessagePleaseWait": "Čekejte prosím...",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nemá žádnou adresu URL kanálu RSS, kterou by mohl použít pro porovnávání",
|
||||
"MessageQuickMatchDescription": "Vyplňte prázdné detaily položky a obálku prvním výsledkem shody z '{0}'. Nepřepisuje podrobnosti, pokud není povoleno nastavení serveru \"Preferovat párování metadata\".",
|
||||
"MessagePodcastSearchField": "Zadejte hledaný pojem pro RSS feed URL",
|
||||
"MessageQuickEmbedInProgress": "Probíhá rychlé vkládání",
|
||||
"MessageQuickEmbedQueue": "Zařazeno do fronty pro rychlé vložení ({0} ve frontě)",
|
||||
"MessageQuickMatchAllEpisodes": "Rychlá shoda všech epizod",
|
||||
"MessageQuickMatchDescription": "Vyplnit prázdné detaily položky a obálky prvním výsledkem shody z '{0}'. Nepřepisuje detaily, pokud není povoleno nastavení serveru 'Preferovat shodná metadata'.",
|
||||
"MessageRemoveChapter": "Odstranit kapitolu",
|
||||
"MessageRemoveEpisodes": "Odstranit {0} epizodu",
|
||||
"MessageRemoveFromPlayerQueue": "Odstranit z fronty přehrávače",
|
||||
@@ -775,6 +837,7 @@
|
||||
"MessageResetChaptersConfirm": "Opravdu chcete resetovat kapitoly a vrátit zpět provedené změny?",
|
||||
"MessageRestoreBackupConfirm": "Opravdu chcete obnovit zálohu vytvořenou dne",
|
||||
"MessageRestoreBackupWarning": "Obnovení zálohy přepíše celou databázi umístěnou v /config a obálku obrázků v /metadata/items & /metadata/authors.<br /><br />Backups nezmění žádné soubory ve složkách knihovny. Pokud jste povolili nastavení serveru pro ukládání obrázků obalu a metadat do složek knihovny, nebudou zálohovány ani přepsány.<br /><br />Všichni klienti používající váš server budou automaticky obnoveni.",
|
||||
"MessageScheduleLibraryScanNote": "Většině uživatelů se doporučuje ponechat tuto funkci vypnutou a ponechat zapnuté nastavení sledování složek. Sledování složek automaticky zjistí změny ve složkách vaší knihovny. Sledování složek nefunguje pro každý souborový systém (jako je NFS), takže místo toho lze použít plánované skenování knihoven.",
|
||||
"MessageSearchResultsFor": "Výsledky hledání pro",
|
||||
"MessageSelected": "{0} vybráno",
|
||||
"MessageServerCouldNotBeReached": "Server je nedostupný",
|
||||
@@ -784,7 +847,7 @@
|
||||
"MessageShareURLWillBe": "Sdílené URL bude <strong>{0}</strong>",
|
||||
"MessageStartPlaybackAtTime": "Spustit přehrávání pro \"{0}\" v {1}?",
|
||||
"MessageTaskAudioFileNotWritable": "Nelze zapisovat do audio souboru \"{0}\"",
|
||||
"MessageTaskCanceledByUser": "Task zrušen uživatelem",
|
||||
"MessageTaskCanceledByUser": "Příkaz zrušen uživatelem",
|
||||
"MessageTaskDownloadingEpisodeDescription": "Stahování epizody \"{0}\"",
|
||||
"MessageTaskEmbeddingMetadata": "Vkládání metadat",
|
||||
"MessageTaskEmbeddingMetadataDescription": "Vkládání metadat do audioknihy \"{0}\"",
|
||||
@@ -797,14 +860,20 @@
|
||||
"MessageTaskFailedToMergeAudioFiles": "Spojení audio souborů selhalo",
|
||||
"MessageTaskFailedToMoveM4bFile": "Přesunutí m4b souboru selhalo",
|
||||
"MessageTaskFailedToWriteMetadataFile": "Zápis souboru metadat selhal",
|
||||
"MessageTaskNoFilesToScan": "Žádné soubory ke skenování",
|
||||
"MessageTaskMatchingBooksInLibrary": "Párování knih v knihovně „{0}“",
|
||||
"MessageTaskNoFilesToScan": "Žádné soubory k prohledání",
|
||||
"MessageTaskOpmlImport": "Import OPML",
|
||||
"MessageTaskOpmlImportDescription": "Vytváření podcastů z {0} RSS feedů",
|
||||
"MessageTaskOpmlImportFeed": "Importní zdroj OPML",
|
||||
"MessageTaskOpmlImportFeedDescription": "Importování RSS feedu \"{0}\"",
|
||||
"MessageTaskOpmlImportFeedFailed": "Nepodařilo se získat kanál podcastu",
|
||||
"MessageTaskOpmlImportFeedPodcastDescription": "Vytváření podcastu \"{0}\"",
|
||||
"MessageTaskOpmlImportFeedPodcastExists": "Podcast se stejnou cestou již existuje",
|
||||
"MessageTaskOpmlImportFeedPodcastFailed": "Vytváření podcastu selhalo",
|
||||
"MessageTaskOpmlImportFinished": "Přidáno {0} podcastů",
|
||||
"MessageTaskOpmlParseFailed": "Selhalo parsování OPML souboru",
|
||||
"MessageTaskOpmlParseFastFail": "Neplatný OPML soubor <opml> tag nenalezen NEBO <outline> tag nenalezen",
|
||||
"MessageTaskOpmlParseNoneFound": "Feed nebyl nalezen v OPML souboru",
|
||||
"MessageTaskScanItemsAdded": "{0} přidáno",
|
||||
"MessageTaskScanItemsMissing": "{0} chybí",
|
||||
"MessageTaskScanItemsUpdated": "{0} aktualizováno",
|
||||
@@ -812,7 +881,7 @@
|
||||
"MessageTaskScanningFileChanges": "Skenování změn souborů v \"{0}\"",
|
||||
"MessageTaskScanningLibrary": "Skenování \"{0}\" knihovny",
|
||||
"MessageTaskTargetDirectoryNotWritable": "Do cílové složky nelze zapisovat",
|
||||
"MessageThinking": "Přemýšlení...",
|
||||
"MessageThinking": "Přemýšlím...",
|
||||
"MessageUploaderItemFailed": "Nahrávání selhalo",
|
||||
"MessageUploaderItemSuccess": "Úspěšně nahráno!",
|
||||
"MessageUploading": "Nahrávám...",
|
||||
@@ -828,7 +897,11 @@
|
||||
"NoteRSSFeedPodcastAppsPubDate": "Upozornění: 1 nebo více epizod nemá datum vydání. Některé podcastové aplikace to vyžadují.",
|
||||
"NoteUploaderFoldersWithMediaFiles": "Se složkami s multimediálními soubory bude zacházeno jako se samostatnými položkami knihovny.",
|
||||
"NoteUploaderOnlyAudioFiles": "Pokud nahráváte pouze zvukové soubory, bude s každým zvukovým souborem zacházeno jako se samostatnou audioknihou.",
|
||||
"NoteUploaderUnsupportedFiles": "Nepodporované soubory jsou ignorovány. Při výběru nebo přetažení složky jsou ostatní soubory, které nejsou ve složce položek, ignorovány.",
|
||||
"NoteUploaderUnsupportedFiles": "Nepodporované soubory jsou ignorovány. Při výběru nebo přetažení složky jsou ostatní soubory, které nejsou ve složce, ignorovány.",
|
||||
"NotificationOnBackupCompletedDescription": "Spuštěno po dokončení zálohování",
|
||||
"NotificationOnBackupFailedDescription": "Spuštěno pokud zálohování selže",
|
||||
"NotificationOnEpisodeDownloadedDescription": "Spuštěno při automatickém stažení epizody podcastu",
|
||||
"NotificationOnTestDescription": "Akce pro otestování upozorňovacího systému",
|
||||
"PlaceholderNewCollection": "Nový název kolekce",
|
||||
"PlaceholderNewFolderPath": "Nová cesta ke složce",
|
||||
"PlaceholderNewPlaylist": "Nový název seznamu přehrávání",
|
||||
@@ -839,18 +912,22 @@
|
||||
"StatsBooksAdditional": "Některé další zahrnují…",
|
||||
"StatsBooksFinished": "dokončené knihy",
|
||||
"StatsBooksFinishedThisYear": "Některé knihy dokončené tento rok…",
|
||||
"StatsBooksListenedTo": "knih poslechnuto",
|
||||
"StatsCollectionGrewTo": "Vaše kolekce knih se rozrostla na…",
|
||||
"StatsSessions": "sezení",
|
||||
"StatsSessions": "sezóna",
|
||||
"StatsSpentListening": "stráveno posloucháním",
|
||||
"StatsTopAuthor": "TOP AUTOR",
|
||||
"StatsTopAuthors": "TOP AUTOŘI",
|
||||
"StatsTopGenre": "TOP ŽÁNR",
|
||||
"StatsTopGenres": "TOP ŽÁNRY",
|
||||
"StatsTopMonth": "TOP MĚSÍC",
|
||||
"StatsTopNarrator": "NEJLEPŠÍ VYPRAVĚČ",
|
||||
"StatsTopNarrators": "NEJLEPŠÍ VYPRAVĚČI",
|
||||
"StatsTotalDuration": "S celkovou dobou…",
|
||||
"StatsYearInReview": "ROK V PŘEHLEDU",
|
||||
"ToastAccountUpdateSuccess": "Účet aktualizován",
|
||||
"ToastAppriseUrlRequired": "Je nutné zadat Apprise URL",
|
||||
"ToastAsinRequired": "ASIN vyžadován",
|
||||
"ToastAuthorImageRemoveSuccess": "Obrázek autora odstraněn",
|
||||
"ToastAuthorNotFound": "Author \"{0}\" nenalezen",
|
||||
"ToastAuthorRemoveSuccess": "Autor odstraněn",
|
||||
@@ -870,21 +947,24 @@
|
||||
"ToastBackupUploadSuccess": "Záloha nahrána",
|
||||
"ToastBatchDeleteFailed": "Hromadné smazání selhalo",
|
||||
"ToastBatchDeleteSuccess": "Hromadné smazání proběhlo úspěšně",
|
||||
"ToastBatchQuickMatchFailed": "Rychlá schoda dávky se nezdařila!",
|
||||
"ToastBatchQuickMatchStarted": "Začala rychlá shoda {0} knih!",
|
||||
"ToastBatchUpdateFailed": "Dávková aktualizace se nezdařila",
|
||||
"ToastBatchUpdateSuccess": "Dávková aktualizace proběhla úspěšně",
|
||||
"ToastBookmarkCreateFailed": "Vytvoření záložky se nezdařilo",
|
||||
"ToastBookmarkCreateSuccess": "Přidána záložka",
|
||||
"ToastBookmarkRemoveSuccess": "Záložka odstraněna",
|
||||
"ToastBookmarkUpdateSuccess": "Záložka aktualizována",
|
||||
"ToastCachePurgeFailed": "Nepodařilo se vyčistit mezipaměť",
|
||||
"ToastCachePurgeSuccess": "Vyrovnávací paměť úspěšně vyčištěna",
|
||||
"ToastChaptersHaveErrors": "Kapitoly obsahují chyby",
|
||||
"ToastChaptersMustHaveTitles": "Kapitoly musí mít názvy",
|
||||
"ToastChaptersRemoved": "Kapitoly odstraněny",
|
||||
"ToastCollectionItemsRemoveSuccess": "Položky odstraněny z kolekce",
|
||||
"ToastChaptersUpdated": "Kapitola aktualizována",
|
||||
"ToastCollectionItemsAddFailed": "Přidávání položek do kolekce selhalo",
|
||||
"ToastCollectionRemoveSuccess": "Kolekce odstraněna",
|
||||
"ToastCollectionUpdateSuccess": "Kolekce aktualizována",
|
||||
"ToastCoverUpdateFailed": "Aktualizace obálky selhala",
|
||||
"ToastDateTimeInvalidOrIncomplete": "Datum a čas jsou chybné nebo nekompletní",
|
||||
"ToastDeleteFileFailed": "Nepodařilo se smazat soubor",
|
||||
"ToastDeleteFileSuccess": "Soubor smazán",
|
||||
"ToastDeviceAddFailed": "Přidání zařízení selhalo",
|
||||
@@ -892,12 +972,18 @@
|
||||
"ToastDeviceTestEmailFailed": "Odeslání testovacího emailu selhalo",
|
||||
"ToastDeviceTestEmailSuccess": "Testovací email byl odeslán",
|
||||
"ToastEmailSettingsUpdateSuccess": "Nastavení emailu aktualizována",
|
||||
"ToastEncodeCancelFailed": "Chyba zrušení kódování",
|
||||
"ToastEncodeCancelSucces": "Kódování zrušeno",
|
||||
"ToastEpisodeDownloadQueueClearFailed": "Vyčištění fronty selhalo",
|
||||
"ToastEpisodeDownloadQueueClearSuccess": "Fronta stahování epizod je prázdná",
|
||||
"ToastEpisodeUpdateSuccess": "{0} epizod aktualizováno",
|
||||
"ToastErrorCannotShare": "Na tomto zařízení nelze nativně sdílet",
|
||||
"ToastFailedToLoadData": "Nepodařilo se načíst data",
|
||||
"ToastFailedToMatch": "Nepodařilo se spárovat",
|
||||
"ToastFailedToShare": "Sdílení selhalo",
|
||||
"ToastFailedToUpdate": "Aktualizace selhala",
|
||||
"ToastInvalidImageUrl": "Neplatná URL obrázku",
|
||||
"ToastInvalidMaxEpisodesToDownload": "Neplatný maximální počet epizod ke stažení",
|
||||
"ToastInvalidUrl": "Neplatná URL",
|
||||
"ToastItemCoverUpdateSuccess": "Obálka předmětu byl aktualizována",
|
||||
"ToastItemDeletedFailed": "Smazání položky selhalo",
|
||||
@@ -915,28 +1001,84 @@
|
||||
"ToastLibraryScanFailedToStart": "Nepodařilo se spustit kontrolu",
|
||||
"ToastLibraryScanStarted": "Kontrola knihovny spuštěna",
|
||||
"ToastLibraryUpdateSuccess": "Knihovna \"{0}\" aktualizována",
|
||||
"ToastMatchAllAuthorsFailed": "Nepodařilo se přiřadit všechny autory",
|
||||
"ToastMetadataFilesRemovedError": "Při odstraňování souborů metadat.{0} došlo k chybě",
|
||||
"ToastMetadataFilesRemovedNoneFound": "Žádná metadata.{0} nebyla nalezena v knihovně",
|
||||
"ToastMetadataFilesRemovedNoneRemoved": "Žádná metadata.{0} počet odstraněných souborů",
|
||||
"ToastMetadataFilesRemovedSuccess": "{0} metadata.{1} soubor odstraněn",
|
||||
"ToastMustHaveAtLeastOnePath": "Musí mít minimálně jednu cestu",
|
||||
"ToastNameEmailRequired": "Jméno a email jsou vyžadovány",
|
||||
"ToastNameRequired": "Jméno je vyžadováno",
|
||||
"ToastNewEpisodesFound": "{0} nových epizod bylo nalezeno",
|
||||
"ToastNewUserCreatedFailed": "Chyba při vytváření účtu: \"{0}\"",
|
||||
"ToastNewUserCreatedSuccess": "Vytvořen nový účet",
|
||||
"ToastNewUserLibraryError": "Musíte vybrat alespoň jednu knihovnu",
|
||||
"ToastNewUserPasswordError": "Musí mít heslo, pouze uživatel root může mít prázdné heslo",
|
||||
"ToastNewUserTagError": "Musíte vybrat alespoň jeden tag",
|
||||
"ToastNewUserUsernameError": "Zadej uživatelské jméno",
|
||||
"ToastNoNewEpisodesFound": "Nebyla nalezena žádná nová epizoda",
|
||||
"ToastNoRSSFeed": "Podcast nemá RSS Feed",
|
||||
"ToastNoUpdatesNecessary": "Nejsou potřeba žádné aktualizace",
|
||||
"ToastNotificationCreateFailed": "Chyba při vytváření upozornění",
|
||||
"ToastNotificationDeleteFailed": "Chyba při odstranění upozornění",
|
||||
"ToastNotificationFailedMaximum": "Maximální počet chybných pokusů >= 0",
|
||||
"ToastNotificationQueueMaximum": "Maximální počet upozornění ve frontě musí být >= 0",
|
||||
"ToastNotificationSettingsUpdateSuccess": "Nastavení upozornění aktualizováno",
|
||||
"ToastNotificationTestTriggerFailed": "Chyba při spuštění testovacího upozornění",
|
||||
"ToastNotificationTestTriggerSuccess": "Spuštěno testovací upozornění",
|
||||
"ToastNotificationUpdateSuccess": "Upozornění aktualizováno",
|
||||
"ToastPlaylistCreateFailed": "Vytvoření seznamu přehrávání se nezdařilo",
|
||||
"ToastPlaylistCreateSuccess": "Seznam přehrávání vytvořen",
|
||||
"ToastPlaylistRemoveSuccess": "Seznam přehrávání odstraněn",
|
||||
"ToastPlaylistUpdateSuccess": "Seznam přehrávání aktualizován",
|
||||
"ToastPodcastCreateFailed": "Vytvoření podcastu se nezdařilo",
|
||||
"ToastPodcastCreateSuccess": "Podcast byl úspěšně vytvořen",
|
||||
"ToastPodcastGetFeedFailed": "Chyba při získání podcastového feedu",
|
||||
"ToastPodcastNoEpisodesInFeed": "Žádné epizody nenalezeny v RSS feedu",
|
||||
"ToastPodcastNoRssFeed": "Podcast nemá RSS feed",
|
||||
"ToastProgressIsNotBeingSynced": "Progres není synchronizován, restartujte přehrávání",
|
||||
"ToastProviderCreatedFailed": "Chyba při zadání poskytovatele",
|
||||
"ToastProviderCreatedSuccess": "Nový poskytovatel přidán",
|
||||
"ToastProviderNameAndUrlRequired": "Jméno a Url jsou vyžadovány",
|
||||
"ToastProviderRemoveSuccess": "Poskytovatel odstraněn",
|
||||
"ToastRSSFeedCloseFailed": "Nepodařilo se zavřít RSS kanál",
|
||||
"ToastRSSFeedCloseSuccess": "RSS kanál uzavřen",
|
||||
"ToastRemoveFailed": "Chyba při odstranění",
|
||||
"ToastRemoveItemFromCollectionFailed": "Nepodařilo se odebrat položku z kolekce",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Položka odstraněna z kolekce",
|
||||
"ToastRemoveItemsWithIssuesFailed": "Chyba při odstranění položek v knihovně s chybami",
|
||||
"ToastRemoveItemsWithIssuesSuccess": "Odstraněny položky knihovny s chybami",
|
||||
"ToastRenameFailed": "Chyba při přejmenování",
|
||||
"ToastRescanFailed": "Znovu prohledání selhalo z důvodu {0}",
|
||||
"ToastRescanRemoved": "Znova skenování komplení - položka byla odsraněna",
|
||||
"ToastRescanUpToDate": "Znovu prohledání kompletní - položka aktualizována",
|
||||
"ToastRescanUpdated": "Znovu skenování komplení - položka byla aktualizována",
|
||||
"ToastScanFailed": "Prohledání položek knihovny selhalo",
|
||||
"ToastSelectAtLeastOneUser": "Vyberte alespoň jednoho uživatele",
|
||||
"ToastSendEbookToDeviceFailed": "Odeslání e-knihy do zařízení se nezdařilo",
|
||||
"ToastSendEbookToDeviceSuccess": "E-kniha odeslána do zařízení \"{0}\"",
|
||||
"ToastSeriesUpdateFailed": "Aktualizace série se nezdařila",
|
||||
"ToastSeriesUpdateSuccess": "Aktualizace série byla úspěšná",
|
||||
"ToastServerSettingsUpdateSuccess": "Nastavení serveru aktualizováno",
|
||||
"ToastSessionCloseFailed": "Chyba při ukončení",
|
||||
"ToastSessionDeleteFailed": "Nepodařilo se smazat relaci",
|
||||
"ToastSessionDeleteSuccess": "Relace smazána",
|
||||
"ToastSleepTimerDone": "Uspání knížky ... zZzzZz",
|
||||
"ToastSlugMustChange": "Slug (URL) obsahuje chybné znaky",
|
||||
"ToastSlugRequired": "Slug (URL) je vyžadována",
|
||||
"ToastSocketConnected": "Socket připojen",
|
||||
"ToastSocketDisconnected": "Socket odpojen",
|
||||
"ToastSocketFailedToConnect": "Socket se nepodařilo připojit",
|
||||
"ToastSortingPrefixesEmptyError": "Musí mít alespoň 1 třídicí předponu",
|
||||
"ToastSortingPrefixesUpdateSuccess": "Aktualizovány předpony třídění ({0} položek)",
|
||||
"ToastTitleRequired": "Titul je vyžadován",
|
||||
"ToastUnknownError": "Neznámý error",
|
||||
"ToastUnlinkOpenIdFailed": "Chyba při odpárování uživatele z OpenID",
|
||||
"ToastUnlinkOpenIdSuccess": "Uživatel odpárován z uživatele z OpenID",
|
||||
"ToastUserDeleteFailed": "Nepodařilo se smazat uživatele",
|
||||
"ToastUserDeleteSuccess": "Uživatel smazán"
|
||||
"ToastUserDeleteSuccess": "Uživatel smazán",
|
||||
"ToastUserPasswordChangeSuccess": "Heslo bylo změněno úspěšně",
|
||||
"ToastUserPasswordMismatch": "Hesla se neschodují",
|
||||
"ToastUserPasswordMustChange": "Nové heslo se musí lišit od předchozího",
|
||||
"ToastUserRootRequireName": "Musíte zadat uživatelské jméno root"
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
"ButtonAddLibrary": "Tilføj Bibliotek",
|
||||
"ButtonAddPodcasts": "Tilføj podcasts",
|
||||
"ButtonAddUser": "Tilføj bruger",
|
||||
"ButtonAddYourFirstLibrary": "Tilføj din første bibliotek",
|
||||
"ButtonAddYourFirstLibrary": "Tilføj dit første bibliotek",
|
||||
"ButtonApply": "Anvend",
|
||||
"ButtonApplyChapters": "Anvend kapitler",
|
||||
"ButtonAuthors": "Forfattere",
|
||||
"ButtonBack": "Tilbage",
|
||||
"ButtonBatchEditPopulateFromExisting": "Opret fra eksisterende",
|
||||
"ButtonBatchEditPopulateMapDetails": "Opret fra kortlægnings detaljer",
|
||||
"ButtonBrowseForFolder": "Gennemse mappe",
|
||||
"ButtonCancel": "Annuller",
|
||||
"ButtonCancelEncode": "Annuller kodning",
|
||||
@@ -19,6 +21,7 @@
|
||||
"ButtonChooseFiles": "Vælg filer",
|
||||
"ButtonClearFilter": "Ryd filter",
|
||||
"ButtonCloseFeed": "Luk feed",
|
||||
"ButtonCloseSession": "Luk Åben Session",
|
||||
"ButtonCollections": "Samlinger",
|
||||
"ButtonConfigureScanner": "Konfigurer scanner",
|
||||
"ButtonCreate": "Opret",
|
||||
@@ -29,11 +32,15 @@
|
||||
"ButtonEditChapters": "Rediger kapitler",
|
||||
"ButtonEditPodcast": "Rediger podcast",
|
||||
"ButtonEnable": "Aktiver",
|
||||
"ButtonForceReScan": "Tvungen genindlæsning",
|
||||
"ButtonFireAndFail": "Affyring Og Fejl",
|
||||
"ButtonFireOnTest": "Affyring vedTest begivenhed",
|
||||
"ButtonForceReScan": "Tving genindlæsning",
|
||||
"ButtonFullPath": "Fuld sti",
|
||||
"ButtonHide": "Skjul",
|
||||
"ButtonHome": "Hjem",
|
||||
"ButtonIssues": "Problemer",
|
||||
"ButtonJumpBackward": "Hop Tilbage",
|
||||
"ButtonJumpForward": "Hop Fremad",
|
||||
"ButtonLatest": "Seneste",
|
||||
"ButtonLibrary": "Bibliotek",
|
||||
"ButtonLogout": "Log ud",
|
||||
@@ -43,20 +50,32 @@
|
||||
"ButtonMatchAllAuthors": "Match alle forfattere",
|
||||
"ButtonMatchBooks": "Match bøger",
|
||||
"ButtonNevermind": "Glem det",
|
||||
"ButtonOk": "OK",
|
||||
"ButtonNext": "Næste",
|
||||
"ButtonNextChapter": "Næste Kapitel",
|
||||
"ButtonNextItemInQueue": "Næste Element i Køen",
|
||||
"ButtonOk": "Ok",
|
||||
"ButtonOpenFeed": "Åbn feed",
|
||||
"ButtonOpenManager": "Åbn manager",
|
||||
"ButtonPause": "Pause",
|
||||
"ButtonPlay": "Afspil",
|
||||
"ButtonPlayAll": "Afspil Alle",
|
||||
"ButtonPlaying": "Afspiller",
|
||||
"ButtonPlaylists": "Afspilningslister",
|
||||
"ButtonPrevious": "Sidste",
|
||||
"ButtonPreviousChapter": "Sidste Kapitel",
|
||||
"ButtonProbeAudioFile": "Undersøg Lydfil",
|
||||
"ButtonPurgeAllCache": "Ryd al cache",
|
||||
"ButtonPurgeItemsCache": "Ryd elementcache",
|
||||
"ButtonQueueAddItem": "Tilføj til kø",
|
||||
"ButtonQueueRemoveItem": "Fjern fra kø",
|
||||
"ButtonQuickEmbed": "Hurtig Indlejring",
|
||||
"ButtonQuickEmbedMetadata": "Hurtig Indlejring af Metadata",
|
||||
"ButtonQuickMatch": "Hurtig Match",
|
||||
"ButtonReScan": "Gen-scan",
|
||||
"ButtonRead": "Læs",
|
||||
"ButtonReadLess": "Se mindre",
|
||||
"ButtonReadMore": "Se mere",
|
||||
"ButtonRefresh": "Genindlæs",
|
||||
"ButtonRemove": "Fjern",
|
||||
"ButtonRemoveAll": "Fjern Alle",
|
||||
"ButtonRemoveAllLibraryItems": "Fjern Alle Bibliotekselementer",
|
||||
@@ -64,31 +83,46 @@
|
||||
"ButtonRemoveFromContinueReading": "Fjern fra Fortsæt Læsning",
|
||||
"ButtonRemoveSeriesFromContinueSeries": "Fjern Serie fra Fortsæt Serie",
|
||||
"ButtonReset": "Nulstil",
|
||||
"ButtonResetToDefault": "Nulstil til standard",
|
||||
"ButtonRestore": "Gendan",
|
||||
"ButtonSave": "Gem",
|
||||
"ButtonSaveAndClose": "Gem & Luk",
|
||||
"ButtonSaveTracklist": "Gem Sporliste",
|
||||
"ButtonScan": "Scan",
|
||||
"ButtonScanLibrary": "Scan Bibliotek",
|
||||
"ButtonScrollLeft": "Rul til Venstre",
|
||||
"ButtonScrollRight": "Rul til Højre",
|
||||
"ButtonSearch": "Søg",
|
||||
"ButtonSelectFolderPath": "Vælg Mappen Sti",
|
||||
"ButtonSelectFolderPath": "Vælg Mappe Sti",
|
||||
"ButtonSeries": "Serier",
|
||||
"ButtonSetChaptersFromTracks": "Sæt kapitler fra spor",
|
||||
"ButtonShare": "Del",
|
||||
"ButtonShiftTimes": "Skift Tider",
|
||||
"ButtonShow": "Vis",
|
||||
"ButtonStartM4BEncode": "Start M4B Kode",
|
||||
"ButtonStartMetadataEmbed": "Start Metadata Indlejring",
|
||||
"ButtonStats": "Statistik",
|
||||
"ButtonSubmit": "Send",
|
||||
"ButtonTest": "Test",
|
||||
"ButtonUnlinkOpenId": "Afkobl OpenID",
|
||||
"ButtonUpload": "Upload",
|
||||
"ButtonUploadBackup": "Upload Backup",
|
||||
"ButtonUploadCover": "Upload Omslag",
|
||||
"ButtonUploadOPMLFile": "Upload OPML Fil",
|
||||
"ButtonUserDelete": "Slet bruger {0}",
|
||||
"ButtonUserEdit": "Rediger bruger {0}",
|
||||
"ButtonViewAll": "Vis Alle",
|
||||
"ButtonYes": "Ja",
|
||||
"ErrorUploadFetchMetadataAPI": "Fejl henter metadata",
|
||||
"ErrorUploadFetchMetadataNoResults": "Kunne ikke hente metadata - prøv at uploade title og/eller forfatter",
|
||||
"ErrorUploadLacksTitle": "Skal have en title",
|
||||
"HeaderAccount": "Konto",
|
||||
"HeaderAddCustomMetadataProvider": "Tilføj Brugerdefineret Metadataudbyder",
|
||||
"HeaderAdvanced": "Avanceret",
|
||||
"HeaderAppriseNotificationSettings": "Apprise Notifikationsindstillinger",
|
||||
"HeaderAudioTracks": "Lydspor",
|
||||
"HeaderAudiobookTools": "Audiobog Filhåndteringsværktøjer",
|
||||
"HeaderAuthentication": "Autentificering",
|
||||
"HeaderBackups": "Sikkerhedskopier",
|
||||
"HeaderChangePassword": "Skift Adgangskode",
|
||||
"HeaderChapters": "Kapitler",
|
||||
@@ -97,9 +131,12 @@
|
||||
"HeaderCollectionItems": "Samlingselementer",
|
||||
"HeaderCover": "Omslag",
|
||||
"HeaderCurrentDownloads": "Nuværende Downloads",
|
||||
"HeaderCustomMessageOnLogin": "Brugerdefineret Besked ved Login",
|
||||
"HeaderCustomMetadataProviders": "Brugerdefineret Metadataudbyder",
|
||||
"HeaderDetails": "Detaljer",
|
||||
"HeaderDownloadQueue": "Download Kø",
|
||||
"HeaderEbookFiles": "E-bogsfiler",
|
||||
"HeaderEmail": "Email",
|
||||
"HeaderEmailSettings": "Email Indstillinger",
|
||||
"HeaderEpisodes": "Episoder",
|
||||
"HeaderEreaderDevices": "E-læser Enheder",
|
||||
@@ -117,33 +154,47 @@
|
||||
"HeaderListeningSessions": "Lyttesessioner",
|
||||
"HeaderListeningStats": "Lyttestatistik",
|
||||
"HeaderLogin": "Log ind",
|
||||
"HeaderLogs": "Logs",
|
||||
"HeaderManageGenres": "Administrer Genrer",
|
||||
"HeaderManageTags": "Administrer Tags",
|
||||
"HeaderMapDetails": "Kort Detaljer",
|
||||
"HeaderMatch": "Match",
|
||||
"HeaderMetadataOrderOfPrecedence": "Metadata-prioritet",
|
||||
"HeaderMetadataToEmbed": "Metadata til indlejring",
|
||||
"HeaderNewAccount": "Ny Konto",
|
||||
"HeaderNewLibrary": "Nyt Bibliotek",
|
||||
"HeaderNotificationCreate": "Opret Notifikation",
|
||||
"HeaderNotificationUpdate": "Updater Notifikation",
|
||||
"HeaderNotifications": "Meddelelser",
|
||||
"HeaderOpenIDConnectAuthentication": "OpenID Connect-autentificering",
|
||||
"HeaderOpenListeningSessions": "Åbne lyttesessioner",
|
||||
"HeaderOpenRSSFeed": "Åbn RSS Feed",
|
||||
"HeaderOtherFiles": "Andre Filer",
|
||||
"HeaderPasswordAuthentication": "Adgangskodeautentificering",
|
||||
"HeaderPermissions": "Tilladelser",
|
||||
"HeaderPlayerQueue": "Afspilningskø",
|
||||
"HeaderPlayerSettings": "Afspiller Indstillinger",
|
||||
"HeaderPlaylist": "Afspilningsliste",
|
||||
"HeaderPlaylistItems": "Afspilningsliste Elementer",
|
||||
"HeaderPodcastsToAdd": "Podcasts til Tilføjelse",
|
||||
"HeaderPreviewCover": "Forhåndsvis Omslag",
|
||||
"HeaderRSSFeedGeneral": "RSS Detaljer",
|
||||
"HeaderRSSFeedIsOpen": "RSS Feed er Åben",
|
||||
"HeaderRSSFeeds": "RSS-Feeds",
|
||||
"HeaderRemoveEpisode": "Fjern Episode",
|
||||
"HeaderRemoveEpisodes": "Fjern {0} Episoder",
|
||||
"HeaderSavedMediaProgress": "Gemt Medieforløb",
|
||||
"HeaderSchedule": "Planlæg",
|
||||
"HeaderScheduleEpisodeDownloads": "Planlæg Automatisk Episode-Download",
|
||||
"HeaderScheduleLibraryScans": "Planlæg Automatiske Biblioteksscanninger",
|
||||
"HeaderSession": "Session",
|
||||
"HeaderSetBackupSchedule": "Indstil Sikkerhedskopieringsplan",
|
||||
"HeaderSettings": "Indstillinger",
|
||||
"HeaderSettingsDisplay": "Skærm",
|
||||
"HeaderSettingsExperimental": "Eksperimentelle Funktioner",
|
||||
"HeaderSettingsGeneral": "Generelt",
|
||||
"HeaderSettingsScanner": "Scanner",
|
||||
"HeaderSettingsWebClient": "Webklient",
|
||||
"HeaderSleepTimer": "Søvntimer",
|
||||
"HeaderStatsLargestItems": "Største Elementer",
|
||||
"HeaderStatsLongestItems": "Længste Elementer (timer)",
|
||||
@@ -158,8 +209,13 @@
|
||||
"HeaderUpdateDetails": "Opdater Detaljer",
|
||||
"HeaderUpdateLibrary": "Opdater Bibliotek",
|
||||
"HeaderUsers": "Brugere",
|
||||
"HeaderYearReview": "Gennemgang af År {0}",
|
||||
"HeaderYourStats": "Dine Statistikker",
|
||||
"LabelAccountType": "Kontotype",
|
||||
"LabelAbridged": "Forkortet",
|
||||
"LabelAbridgedChecked": "Forkortet (kontrolleret)",
|
||||
"LabelAbridgedUnchecked": "Uforkortet (ikke kontrolleret)",
|
||||
"LabelAccessibleBy": "Tilgængelig af",
|
||||
"LabelAccountType": "Brugertype",
|
||||
"LabelAccountTypeAdmin": "Administrator",
|
||||
"LabelAccountTypeGuest": "Gæst",
|
||||
"LabelAccountTypeUser": "Bruger",
|
||||
@@ -168,32 +224,56 @@
|
||||
"LabelAddToCollectionBatch": "Tilføj {0} Bøger til Samling",
|
||||
"LabelAddToPlaylist": "Tilføj til Afspilningsliste",
|
||||
"LabelAddToPlaylistBatch": "Tilføj {0} Elementer til Afspilningsliste",
|
||||
"LabelAddedAt": "Tilføjet Kl.",
|
||||
"LabelAddedAt": "Tilføjet",
|
||||
"LabelAddedDate": "Tilføjet {0}",
|
||||
"LabelAdminUsersOnly": "Kun Administratorer",
|
||||
"LabelAll": "Alle",
|
||||
"LabelAllUsers": "Alle Brugere",
|
||||
"LabelAllUsersExcludingGuests": "Alle bruger eksklusiv gæster",
|
||||
"LabelAllUsersIncludingGuests": "Alle bruger inklusiv gæster",
|
||||
"LabelAlreadyInYourLibrary": "Allerede i dit bibliotek",
|
||||
"LabelApiToken": "API Token",
|
||||
"LabelAppend": "Tilføj",
|
||||
"LabelAudioBitrate": "Lydbitrate (f.eks. 128k)",
|
||||
"LabelAudioChannels": "Lydkanaler (1 eller 2)",
|
||||
"LabelAudioCodec": "Lydkodek",
|
||||
"LabelAuthor": "Forfatter",
|
||||
"LabelAuthorFirstLast": "Forfatter (Fornavn Efternavn)",
|
||||
"LabelAuthorLastFirst": "Forfatter (Efternavn, Fornavn)",
|
||||
"LabelAuthors": "Forfattere",
|
||||
"LabelAutoDownloadEpisodes": "Auto Download Episoder",
|
||||
"LabelAutoFetchMetadata": "Automatisk Hent Metadata",
|
||||
"LabelAutoFetchMetadataHelp": "Henter metadata for titler, forfatter og serier for at strømligne uploading. Ekstra metadata har måske brug for at blive matchet efter upload.",
|
||||
"LabelAutoLaunch": "Åben Automatisk",
|
||||
"LabelAutoLaunchDescription": "Viderestil automatisk til login-udbyderen ved navigation til login-siden (manuel overstyring via <code>/login?autoLaunch=0</code>)",
|
||||
"LabelAutoRegister": "Registrer Automatisk",
|
||||
"LabelAutoRegisterDescription": "Automatisk oprettelse af nye brugere efter login",
|
||||
"LabelBackToUser": "Tilbage til Bruger",
|
||||
"LabelBackupAudioFiles": "Sikkerhedskopier lydfiler",
|
||||
"LabelBackupLocation": "Backup Placering",
|
||||
"LabelBackupsEnableAutomaticBackups": "Aktivér automatisk sikkerhedskopiering",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "Sikkerhedskopier gemt i /metadata/backups",
|
||||
"LabelBackupsMaxBackupSize": "Maksimal sikkerhedskopistørrelse (i GB)",
|
||||
"LabelBackupsMaxBackupSize": "Maksimal sikkerhedskopistørrelse (i GB) (0 for ubegrænset)",
|
||||
"LabelBackupsMaxBackupSizeHelp": "Som en beskyttelse mod fejlkonfiguration fejler sikkerhedskopier, hvis de overstiger den konfigurerede størrelse.",
|
||||
"LabelBackupsNumberToKeep": "Antal sikkerhedskopier at beholde",
|
||||
"LabelBackupsNumberToKeepHelp": "Kun 1 sikkerhedskopi fjernes ad gangen, så hvis du allerede har flere sikkerhedskopier end dette, skal du fjerne dem manuelt.",
|
||||
"LabelBitrate": "Bitrate",
|
||||
"LabelBonus": "Bonus",
|
||||
"LabelBooks": "Bøger",
|
||||
"LabelButtonText": "Knap tekst",
|
||||
"LabelByAuthor": "af {0}",
|
||||
"LabelChangePassword": "Ændre Adgangskode",
|
||||
"LabelChannels": "Kanaler",
|
||||
"LabelChapterCount": "{0} Kapitler",
|
||||
"LabelChapterTitle": "Kapitel Titel",
|
||||
"LabelChapters": "Kapitler",
|
||||
"LabelChaptersFound": "fundne kapitler",
|
||||
"LabelClickForMoreInfo": "Klik for mere info",
|
||||
"LabelClickToUseCurrentValue": "Klik for at bruge nuværende værdi",
|
||||
"LabelClosePlayer": "Luk afspiller",
|
||||
"LabelCodec": "Kodeks",
|
||||
"LabelCollapseSeries": "Fold Serier Sammen",
|
||||
"LabelCollapseSubSeries": "Fold underserie sammen",
|
||||
"LabelCollection": "Samling",
|
||||
"LabelCollections": "Samlinger",
|
||||
"LabelComplete": "Fuldfør",
|
||||
@@ -209,58 +289,100 @@
|
||||
"LabelCurrently": "Aktuelt:",
|
||||
"LabelCustomCronExpression": "Brugerdefineret Cron Udtryk:",
|
||||
"LabelDatetime": "Dato og Tid",
|
||||
"LabelDays": "Dage",
|
||||
"LabelDeleteFromFileSystemCheckbox": "Slet fra filsystem (afmarker kun for at fjerne fra databasen)",
|
||||
"LabelDescription": "Beskrivelse",
|
||||
"LabelDeselectAll": "Fravælg Alle",
|
||||
"LabelDevice": "Enheds",
|
||||
"LabelDeviceInfo": "Enhedsinformation",
|
||||
"LabelDeviceIsAvailableTo": "Enhed er tilgængelig for...",
|
||||
"LabelDirectory": "Mappe",
|
||||
"LabelDiscFromFilename": "Disk fra Filnavn",
|
||||
"LabelDiscFromMetadata": "Disk fra Metadata",
|
||||
"LabelDiscover": "Opdag",
|
||||
"LabelDownload": "Download",
|
||||
"LabelDownloadNEpisodes": "Download {0} episoder",
|
||||
"LabelDownloadable": "Downloadbar",
|
||||
"LabelDuration": "Varighed",
|
||||
"LabelDurationComparisonExactMatch": "(præcis match)",
|
||||
"LabelDurationComparisonLonger": "({0} længere)",
|
||||
"LabelDurationComparisonShorter": "({0} kortere)",
|
||||
"LabelDurationFound": "Fundet varighed:",
|
||||
"LabelEbook": "E-bog",
|
||||
"LabelEbooks": "E-bøger",
|
||||
"LabelEdit": "Rediger",
|
||||
"LabelEmail": "E-mail",
|
||||
"LabelEmailSettingsFromAddress": "Fra Adresse",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Afvis uautoriserede certifikater",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Deaktivering af SSL certifikat validering kan udsætte din forbindelse for sikkerhedsrisici, eksempelvis man-in-the-middle angreb. Deaktiver kun denne indstilling hvis du forstår de potentielle implikationer og stoler på den mailserver du forbinder til.",
|
||||
"LabelEmailSettingsSecure": "Sikker",
|
||||
"LabelEmailSettingsSecureHelp": "Hvis sandt, vil forbindelsen bruge TLS ved tilslutning til serveren. Hvis falsk, bruges TLS, hvis serveren understøtter STARTTLS-udvidelsen. I de fleste tilfælde skal denne værdi sættes til sandt, hvis du tilslutter til port 465. Til port 587 eller 25 skal du holde det falsk. (fra nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Adresse",
|
||||
"LabelEmbeddedCover": "Indlejret Omslag",
|
||||
"LabelEnable": "Aktivér",
|
||||
"LabelEncodingBackupLocation": "En sikkerhedskopi af dine originale lydfiler vil blive gemt under:",
|
||||
"LabelEncodingChaptersNotEmbedded": "Kapitler er ikke indlejret i multi spors lydbøger.",
|
||||
"LabelEncodingClearItemCache": "Sørg for periodisk at rense indholdscachen.",
|
||||
"LabelEncodingFinishedM4B": "Færdiggjort M4B som vil blive placeret i din lydbogsmappe ved:",
|
||||
"LabelEncodingInfoEmbedded": "Metadata vil blive indlejret i lydfiler i lydbogsmappen.",
|
||||
"LabelEncodingStartedNavigation": "Når opgaven er startet kan du navigere væk fra denne side.",
|
||||
"LabelEncodingTimeWarning": "Indkodning kan tage op til 30 minutter.",
|
||||
"LabelEncodingWarningAdvancedSettings": "Advarsel: Opdater ikke disse indstillinger med mindre du kender til ffmpeg indkodningsindstillinger.",
|
||||
"LabelEncodingWatcherDisabled": "Hvis du har watcheren deaktiveret skal du gen-scanne denne lydbog bagefter.",
|
||||
"LabelEnd": "Slut",
|
||||
"LabelEndOfChapter": "Slutningen af kapitel",
|
||||
"LabelEpisode": "Episode",
|
||||
"LabelEpisode": "Afsnit",
|
||||
"LabelEpisodeNotLinkedToRssFeed": "Afsnit er ikke koblet til RSS feed",
|
||||
"LabelEpisodeNumber": "Afsnit #{0}",
|
||||
"LabelEpisodeTitle": "Episodetitel",
|
||||
"LabelEpisodeType": "Episodetype",
|
||||
"LabelEpisodeUrlFromRssFeed": "Afsnit URL fra RSS feed",
|
||||
"LabelEpisodes": "Afsnit",
|
||||
"LabelEpisodic": "Afsnit",
|
||||
"LabelExample": "Eksempel",
|
||||
"LabelExpandSeries": "Udfold serie",
|
||||
"LabelExpandSubSeries": "Udfold underserie",
|
||||
"LabelExplicit": "Eksplisit",
|
||||
"LabelExplicitChecked": "Eksplicit (markeret)",
|
||||
"LabelExplicitUnchecked": "Ikke eksplicit (ikke markeret)",
|
||||
"LabelExportOPML": "Eksport OPML",
|
||||
"LabelFeedURL": "Feed URL",
|
||||
"LabelFetchingMetadata": "Henter metadata",
|
||||
"LabelFile": "Fil",
|
||||
"LabelFileBirthtime": "Oprettelsestidspunkt for fil",
|
||||
"LabelFileBornDate": "Født {0}",
|
||||
"LabelFileModified": "Fil ændret",
|
||||
"LabelFileModifiedDate": "Opdateret {0}",
|
||||
"LabelFilename": "Filnavn",
|
||||
"LabelFilterByUser": "Filtrér efter bruger",
|
||||
"LabelFindEpisodes": "Find episoder",
|
||||
"LabelFinished": "Færdig",
|
||||
"LabelFolder": "Mappe",
|
||||
"LabelFolders": "Mapper",
|
||||
"LabelFontBold": "Fed",
|
||||
"LabelFontBoldness": "Skrift tykkelse",
|
||||
"LabelFontFamily": "Fontfamilie",
|
||||
"LabelFontItalic": "Kursiv",
|
||||
"LabelFontScale": "Skriftstørrelse",
|
||||
"LabelFontStrikethrough": "Gennemstreget",
|
||||
"LabelFormat": "Format",
|
||||
"LabelFull": "Fuld",
|
||||
"LabelGenre": "Genre",
|
||||
"LabelGenres": "Genrer",
|
||||
"LabelHardDeleteFile": "Permanent slet fil",
|
||||
"LabelHasEbook": "Har e-bog",
|
||||
"LabelHasSupplementaryEbook": "Har supplerende e-bog",
|
||||
"LabelHideSubtitles": "Skjul undertitler",
|
||||
"LabelHighestPriority": "Højeste prioritet",
|
||||
"LabelHost": "Vært",
|
||||
"LabelHour": "Time",
|
||||
"LabelHours": "Timer",
|
||||
"LabelIcon": "Ikon",
|
||||
"LabelImageURLFromTheWeb": "Billede URL fra nettet",
|
||||
"LabelInProgress": "I gang",
|
||||
"LabelIncludeInTracklist": "Inkluder i afspilningsliste",
|
||||
"LabelIncomplete": "Ufuldstændig",
|
||||
"LabelInterval": "Interval",
|
||||
"LabelIntervalCustomDailyWeekly": "Tilpasset dagligt/ugentligt",
|
||||
"LabelIntervalEvery12Hours": "Hver 12. time",
|
||||
"LabelIntervalEvery15Minutes": "Hver 15. minut",
|
||||
@@ -271,8 +393,11 @@
|
||||
"LabelIntervalEveryHour": "Hver time",
|
||||
"LabelInvert": "Inverter",
|
||||
"LabelItem": "Element",
|
||||
"LabelJumpBackwardAmount": "Spring bagud mængde",
|
||||
"LabelJumpForwardAmount": "Spring fremad mængde",
|
||||
"LabelLanguage": "Sprog",
|
||||
"LabelLanguageDefaultServer": "Standard server sprog",
|
||||
"LabelLanguages": "Sprog",
|
||||
"LabelLastBookAdded": "Senest tilføjede bog",
|
||||
"LabelLastBookUpdated": "Senest opdaterede bog",
|
||||
"LabelLastSeen": "Sidst set",
|
||||
@@ -284,6 +409,7 @@
|
||||
"LabelLess": "Mindre",
|
||||
"LabelLibrariesAccessibleToUser": "Biblioteker tilgængelige for bruger",
|
||||
"LabelLibrary": "Bibliotek",
|
||||
"LabelLibraryFilterSublistEmpty": "Nej {0}",
|
||||
"LabelLibraryItem": "Bibliotekselement",
|
||||
"LabelLibraryName": "Biblioteksnavn",
|
||||
"LabelLimit": "Grænse",
|
||||
@@ -293,24 +419,38 @@
|
||||
"LabelLogLevelInfo": "Information",
|
||||
"LabelLogLevelWarn": "Advarsel",
|
||||
"LabelLookForNewEpisodesAfterDate": "Søg efter nye episoder efter denne dato",
|
||||
"LabelLowestPriority": "Laveste prioritet",
|
||||
"LabelMatchExistingUsersBy": "Match eksisterende brugere ved",
|
||||
"LabelMatchExistingUsersByDescription": "Anvendt for at forbinde brugere. Når forbundet, brugere vil blive matchet ved unikt id fra din SSO udbyder",
|
||||
"LabelMaxEpisodesToDownload": "Max # afsnit for at downloade. Anvend 0 for ubegrænset.",
|
||||
"LabelMaxEpisodesToDownloadPerCheck": "Max # afsnit til at downloade per check",
|
||||
"LabelMaxEpisodesToKeep": "Max # afsnit at beholde",
|
||||
"LabelMaxEpisodesToKeepHelp": "Værdi af 0 sætter intet maks begrænsning. After et nyt afsnit er automatisk downloaded vil det ældste afsnit blive slettet hvis du har mere end X afsnit. Dette vil kun slette 1 afsnit for hvert nye download.",
|
||||
"LabelMediaPlayer": "Medieafspiller",
|
||||
"LabelMediaType": "Medietype",
|
||||
"LabelMetaTag": "Meta-tag",
|
||||
"LabelMetaTags": "Meta-tags",
|
||||
"LabelMetadataOrderOfPrecedenceDescription": "Højeste prioritet metadata kilder vil overskrive de lavest prioriterede metadata kilder",
|
||||
"LabelMetadataProvider": "Metadataudbyder",
|
||||
"LabelMinute": "Minut",
|
||||
"LabelMinutes": "Minutter",
|
||||
"LabelMissing": "Mangler",
|
||||
"LabelMissingEbook": "Har ingen ebog",
|
||||
"LabelMissingSupplementaryEbook": "Har ingen tillægsbog",
|
||||
"LabelMobileRedirectURIs": "Godkendte mobil redirect URI'er",
|
||||
"LabelMobileRedirectURIsDescription": "Dete vil whiteliste en gyldig omdirigerings URL for mobile apps. Den standarde er <code>audiobookshelf://oauth</code> som du kan fjerne eller supplere med flere URI'er for tredjeparts app integration. Anvend en stjerne (<code>*</code>) som den eneste indstilling for at tilade en hvilkensomhelst URI.",
|
||||
"LabelMore": "Mere",
|
||||
"LabelMoreInfo": "Mere info",
|
||||
"LabelName": "Navn",
|
||||
"LabelNarrator": "Fortæller",
|
||||
"LabelNarrators": "Fortællere",
|
||||
"LabelNew": "Ny",
|
||||
"LabelNewPassword": "Nyt kodeord",
|
||||
"LabelNewPassword": "Ny adgangskode",
|
||||
"LabelNewestAuthors": "Nyeste forfattere",
|
||||
"LabelNewestEpisodes": "Nyeste episoder",
|
||||
"LabelNextBackupDate": "Næste sikkerhedskopi dato",
|
||||
"LabelNextScheduledRun": "Næste planlagte kørsel",
|
||||
"LabelNoCustomMetadataProviders": "Ingen brugerdefinerede metadata udbydere",
|
||||
"LabelNoEpisodesSelected": "Ingen episoder valgt",
|
||||
"LabelNotFinished": "Ikke færdig",
|
||||
"LabelNotStarted": "Ikke påbegyndt",
|
||||
@@ -325,31 +465,48 @@
|
||||
"LabelNotificationsMaxQueueSize": "Maksimal køstørrelse for meddelelseshændelser",
|
||||
"LabelNotificationsMaxQueueSizeHelp": "Hændelser begrænses til at udløse en gang pr. sekund. Hændelser ignoreres, hvis køen er fyldt. Dette forhindrer meddelelsesspam.",
|
||||
"LabelNumberOfBooks": "Antal bøger",
|
||||
"LabelNumberOfEpisodes": "Antal episoder",
|
||||
"LabelNumberOfEpisodes": "# afsnit",
|
||||
"LabelOpenIDAdvancedPermsClaimDescription": "Navnet af OpenID claimet som indeholder avancerede brugerhandlinger inden i applikationen som vil gælde for ikke administrative roller (<b>hvis konfigureret</b>). Hvis et claim mangler fra svaret vil adgang til ABS blive nægtet. Hvis en enkelt indstilling/option mangler, vil det bliver behandlet som <code>false</code>. Sørg for at identity provider's claim matcher den forventede struktur:",
|
||||
"LabelOpenIDClaims": "Efterlad de følgende indstillinger tomme for at deaktivere avanceret gruppe og adgangsindstilling, ved automatisk at assigne 'Bruger' grupper.",
|
||||
"LabelOpenIDGroupClaimDescription": "Navnet af det OpenID claim som skal indeholde brugerens grupper. Mest kendt som <code>groups</code>. <b>hvis konfigureret</b>, vil applikationen automatiske tildele roller baseret p[ brugerens gruppemedlemsskaber, givet disse grupper er navngivet (uden forbehold for store og små bogstaver) 'admin', 'user' eller 'guest' i claimet. Claimet burde indeholde en liste (og hvis brugeren tilhøre flere grupper) som applikationen vil tildele roller med højeste adgangsnvieau. Hvis ingen grupper matcher vil adgang blive nægtet.",
|
||||
"LabelOpenRSSFeed": "Åbn RSS-feed",
|
||||
"LabelOverwrite": "Overskriv",
|
||||
"LabelPassword": "Kodeord",
|
||||
"LabelPaginationPageXOfY": "Side {0} af {1}",
|
||||
"LabelPassword": "Adgangskode",
|
||||
"LabelPath": "Sti",
|
||||
"LabelPermanent": "Permanent",
|
||||
"LabelPermissionsAccessAllLibraries": "Kan få adgang til alle biblioteker",
|
||||
"LabelPermissionsAccessAllTags": "Kan få adgang til alle tags",
|
||||
"LabelPermissionsAccessExplicitContent": "Kan få adgang til eksplicit indhold",
|
||||
"LabelPermissionsCreateEreader": "Kan oprette elæser",
|
||||
"LabelPermissionsDelete": "Kan slette",
|
||||
"LabelPermissionsDownload": "Kan downloade",
|
||||
"LabelPermissionsUpdate": "Kan opdatere",
|
||||
"LabelPermissionsUpload": "Kan uploade",
|
||||
"LabelPersonalYearReview": "Dit år i review ({0})",
|
||||
"LabelPhotoPathURL": "Foto sti/URL",
|
||||
"LabelPlayMethod": "Afspilningsmetode",
|
||||
"LabelPlaybackRateIncrementDecrement": "Afspilningshastighed øges/sænkes med",
|
||||
"LabelPlayerChapterNumberMarker": "{0} af {1}",
|
||||
"LabelPlaylists": "Afspilningslister",
|
||||
"LabelPodcast": "Podcast",
|
||||
"LabelPodcastSearchRegion": "Podcast søgeområde",
|
||||
"LabelPodcastType": "Podcast type",
|
||||
"LabelPodcasts": "Podcast",
|
||||
"LabelPort": "Port",
|
||||
"LabelPrefixesToIgnore": "Præfikser der skal ignoreres (skal ikke skelne mellem store og små bogstaver)",
|
||||
"LabelPreventIndexing": "Forhindrer, at dit feed bliver indekseret af iTunes og Google podcastkataloger",
|
||||
"LabelPrimaryEbook": "Primær e-bog",
|
||||
"LabelProgress": "Fremskridt",
|
||||
"LabelProvider": "Udbyder",
|
||||
"LabelProviderAuthorizationValue": "Authorization Header værdi",
|
||||
"LabelPubDate": "Udgivelsesdato",
|
||||
"LabelPublishYear": "Udgivelsesår",
|
||||
"LabelPublishedDate": "Publiceret {0}",
|
||||
"LabelPublishedDecade": "Publiceret årti",
|
||||
"LabelPublishedDecades": "Publiceret årtier",
|
||||
"LabelPublisher": "Forlag",
|
||||
"LabelPublishers": "Forlag",
|
||||
"LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail",
|
||||
"LabelRSSFeedCustomOwnerName": "Brugerdefineret ejerens navn",
|
||||
"LabelRSSFeedOpen": "Åben RSS-feed",
|
||||
@@ -357,27 +514,42 @@
|
||||
"LabelRSSFeedSlug": "RSS-feed-slug",
|
||||
"LabelRSSFeedURL": "RSS-feed-URL",
|
||||
"LabelRandomly": "Tilfældigt",
|
||||
"LabelReAddSeriesToContinueListening": "Gentilføj serier til Fortsæt Lytning",
|
||||
"LabelRead": "Læst",
|
||||
"LabelReadAgain": "Læs igen",
|
||||
"LabelReadAgain": "Læs Igen",
|
||||
"LabelReadEbookWithoutProgress": "Læs e-bog uden at følge fremskridt",
|
||||
"LabelRecentSeries": "Seneste serier",
|
||||
"LabelRecentlyAdded": "Senest tilføjet",
|
||||
"LabelRecommended": "Anbefalet",
|
||||
"LabelRedo": "Gøre igen",
|
||||
"LabelRegion": "Region",
|
||||
"LabelReleaseDate": "Udgivelsesdato",
|
||||
"LabelRemoveAllMetadataAbs": "Fjern alle metadata.abs filer",
|
||||
"LabelRemoveAllMetadataJson": "Fjern alle metadata.json filer",
|
||||
"LabelRemoveCover": "Fjern omslag",
|
||||
"LabelRemoveMetadataFile": "Fjern alle metadata filer i biblioteksmapper",
|
||||
"LabelRemoveMetadataFileHelp": "Fjern alle metadata.json og metadata.abs filer i dine {0} mapper.",
|
||||
"LabelRowsPerPage": "Rækker per side",
|
||||
"LabelSearchTerm": "Søgeterm",
|
||||
"LabelSearchTitle": "Søg efter titel",
|
||||
"LabelSearchTitleOrASIN": "Søg efter titel eller ASIN",
|
||||
"LabelSeason": "Sæson",
|
||||
"LabelSeasonNumber": "Sæson {0}",
|
||||
"LabelSelectAll": "Vælg alle",
|
||||
"LabelSelectAllEpisodes": "Vælg alle episoder",
|
||||
"LabelSelectEpisodesShowing": "Vælg {0} episoder vist",
|
||||
"LabelSelectUsers": "Valgte brugere",
|
||||
"LabelSendEbookToDevice": "Send e-bog til...",
|
||||
"LabelSequence": "Sekvens",
|
||||
"LabelSerial": "Seriel",
|
||||
"LabelSeries": "Serie",
|
||||
"LabelSeriesName": "Serienavn",
|
||||
"LabelSeriesProgress": "Seriefremskridt",
|
||||
"LabelServerLogLevel": "Server log niveau",
|
||||
"LabelServerYearReview": "Server år i review ({0})",
|
||||
"LabelSetEbookAsPrimary": "Indstil som primær",
|
||||
"LabelSetEbookAsSupplementary": "Indstil som supplerende",
|
||||
"LabelSettingsAllowIframe": "Tillad embedding i en iframe",
|
||||
"LabelSettingsAudiobooksOnly": "Kun lydbøger",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "Aktivering af denne indstilling vil ignorere e-bogsfiler, medmindre de er inde i en lydbogmappe, hvor de vil blive indstillet som supplerende e-bøger",
|
||||
"LabelSettingsBookshelfViewHelp": "Skeumorfisk design med træhylder",
|
||||
@@ -389,6 +561,8 @@
|
||||
"LabelSettingsEnableWatcher": "Aktiver overvågning",
|
||||
"LabelSettingsEnableWatcherForLibrary": "Aktiver mappeovervågning for bibliotek",
|
||||
"LabelSettingsEnableWatcherHelp": "Aktiverer automatisk tilføjelse/opdatering af elementer, når filændringer registreres. *Kræver servergenstart",
|
||||
"LabelSettingsEpubsAllowScriptedContent": "Tillad scriptet indhold i epub",
|
||||
"LabelSettingsEpubsAllowScriptedContentHelp": "Tillad epub filer at køre scripts. Det anbefales at holde denne indstilling deaktiveret med mindre du stoler på kilderne af epub filerne.",
|
||||
"LabelSettingsExperimentalFeatures": "Eksperimentelle funktioner",
|
||||
"LabelSettingsExperimentalFeaturesHelp": "Funktioner under udvikling, der kunne bruge din feedback og hjælp til test. Klik for at åbne Github-diskussionen.",
|
||||
"LabelSettingsFindCovers": "Find omslag",
|
||||
@@ -397,12 +571,17 @@
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Serier med en enkelt bog vil blive skjult fra serie-siden og hjemmesidehylder.",
|
||||
"LabelSettingsHomePageBookshelfView": "Brug bogreolvisning på startside",
|
||||
"LabelSettingsLibraryBookshelfView": "Brug bogreolvisning i biblioteket",
|
||||
"LabelSettingsParseSubtitles": "Fortolk undertekster",
|
||||
"LabelSettingsLibraryMarkAsFinishedPercentComplete": "Procent gennemført er større end",
|
||||
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Tid tilbage er mindre end (sekunder)",
|
||||
"LabelSettingsLibraryMarkAsFinishedWhen": "Marker medie indhold som færdigt når",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Spring til tidligere bøger i Fortsæt serie",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Fortsæt Serien siden hylde viser de første bøger som ikke er startet i serier med mindst en bog som ikke er startet og ingen bøger i gang. Aktivering af denne indstilling vil fortsætte serien fra den sidst gennemførte bog modsat den først ikke startede bog.",
|
||||
"LabelSettingsParseSubtitles": "Fortolk undertitler",
|
||||
"LabelSettingsParseSubtitlesHelp": "Udtræk undertekster fra lydbogsmappenavne.<br>Undertitler skal adskilles af \" - \"<br>f.eks. \"Bogtitel - En undertitel her\" har undertitlen \"En undertitel her\"",
|
||||
"LabelSettingsPreferMatchedMetadata": "Foretræk matchede metadata",
|
||||
"LabelSettingsPreferMatchedMetadataHelp": "Matchede data vil tilsidesætte elementdetaljer ved brug af Hurtig Match. Som standard udfylder Hurtig Match kun manglende detaljer.",
|
||||
"LabelSettingsSkipMatchingBooksWithASIN": "Spring over matchende bøger, der allerede har en ASIN",
|
||||
"LabelSettingsSkipMatchingBooksWithISBN": "Spring over matchende bøger, der allerede har en ISBN",
|
||||
"LabelSettingsSkipMatchingBooksWithISBN": "Spring matchende bøger over, som allerede har et ISBN-nummer",
|
||||
"LabelSettingsSortingIgnorePrefixes": "Ignorer præfikser ved sortering",
|
||||
"LabelSettingsSortingIgnorePrefixesHelp": "f.eks. for præfikset \"the\" vil bogtitlen \"The Book Title\" blive sorteret som \"Book Title, The\"",
|
||||
"LabelSettingsSquareBookCovers": "Brug kvadratiske bogomslag",
|
||||
@@ -412,9 +591,19 @@
|
||||
"LabelSettingsStoreMetadataWithItem": "Gem metadata med element",
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Som standard gemmes metadatafiler i /metadata/items, aktivering af denne indstilling vil gemme metadatafiler i dine bibliotekselementmapper",
|
||||
"LabelSettingsTimeFormat": "Tidsformat",
|
||||
"LabelShare": "Del",
|
||||
"LabelShareDownloadableHelp": "Tillad brugere at dele link til at downloade en zip fil af dette biblioteksindhold.",
|
||||
"LabelShareOpen": "Del åben",
|
||||
"LabelShareURL": "Del URL",
|
||||
"LabelShowAll": "Vis alle",
|
||||
"LabelShowSeconds": "Vis sekunder",
|
||||
"LabelShowSubtitles": "Vis undertitler",
|
||||
"LabelSize": "Størrelse",
|
||||
"LabelSleepTimer": "Søvntimer",
|
||||
"LabelSlug": "Snegl",
|
||||
"LabelSortAscending": "Stigende",
|
||||
"LabelSortDescending": "Faldende",
|
||||
"LabelStart": "Start",
|
||||
"LabelStartTime": "Starttid",
|
||||
"LabelStarted": "Startet",
|
||||
"LabelStartedAt": "Startet klokken",
|
||||
@@ -440,10 +629,19 @@
|
||||
"LabelTagsAccessibleToUser": "Mærker tilgængelige for bruger",
|
||||
"LabelTagsNotAccessibleToUser": "Mærker ikke tilgængelige for bruger",
|
||||
"LabelTasks": "Kører opgaver",
|
||||
"LabelTextEditorBulletedList": "Punktopstilling",
|
||||
"LabelTextEditorLink": "Link",
|
||||
"LabelTextEditorNumberedList": "Nummeropstilling",
|
||||
"LabelTextEditorUnlink": "Aflink",
|
||||
"LabelTheme": "Tema",
|
||||
"LabelThemeDark": "Mørk",
|
||||
"LabelThemeLight": "Lys",
|
||||
"LabelTimeBase": "Tidsbase",
|
||||
"LabelTimeDurationXHours": "{0} timer",
|
||||
"LabelTimeDurationXMinutes": "{0} minutter",
|
||||
"LabelTimeDurationXSeconds": "{0} sekunder",
|
||||
"LabelTimeInMinutes": "Tid i minutter",
|
||||
"LabelTimeLeft": "{0} tilbage",
|
||||
"LabelTimeListened": "Tid hørt",
|
||||
"LabelTimeListenedToday": "Tid hørt i dag",
|
||||
"LabelTimeRemaining": "{0} tilbage",
|
||||
@@ -451,6 +649,7 @@
|
||||
"LabelTitle": "Titel",
|
||||
"LabelToolsEmbedMetadata": "Indlejre metadata",
|
||||
"LabelToolsEmbedMetadataDescription": "Indlejr metadata i lydfiler, inklusive omslag og kapitler.",
|
||||
"LabelToolsM4bEncoder": "M4B indkoder",
|
||||
"LabelToolsMakeM4b": "Lav M4B lydbogsfil",
|
||||
"LabelToolsMakeM4bDescription": "Generer en .M4B lydbogsfil med indlejret metadata, omslag og kapitler.",
|
||||
"LabelToolsSplitM4b": "Opdel M4B til MP3'er",
|
||||
@@ -463,25 +662,41 @@
|
||||
"LabelTracksMultiTrack": "Flerspors",
|
||||
"LabelTracksNone": "Ingen spor",
|
||||
"LabelTracksSingleTrack": "Enkeltspors",
|
||||
"LabelTrailer": "Trailer",
|
||||
"LabelType": "Type",
|
||||
"LabelUnabridged": "Uforkortet",
|
||||
"LabelUndo": "Fortryd",
|
||||
"LabelUnknown": "Ukendt",
|
||||
"LabelUnknownPublishDate": "Ukendt publiceringsdato",
|
||||
"LabelUpdateCover": "Opdater omslag",
|
||||
"LabelUpdateCoverHelp": "Tillad overskrivning af eksisterende omslag for de valgte bøger, når der findes en match",
|
||||
"LabelUpdateDetails": "Opdater detaljer",
|
||||
"LabelUpdateDetailsHelp": "Tillad overskrivning af eksisterende detaljer for de valgte bøger, når der findes en match",
|
||||
"LabelUpdatedAt": "Opdateret ved",
|
||||
"LabelUploaderDragAndDrop": "Træk og slip filer eller mapper",
|
||||
"LabelUploaderDragAndDropFilesOnly": "Træk og slip filer",
|
||||
"LabelUploaderDropFiles": "Smid filer",
|
||||
"LabelUploaderItemFetchMetadataHelp": "Automatisk hent titel, forfatter og serie",
|
||||
"LabelUseAdvancedOptions": "Anvend avancerede indstillinger",
|
||||
"LabelUseChapterTrack": "Brug kapitel-spor",
|
||||
"LabelUseFullTrack": "Brug fuldt spor",
|
||||
"LabelUseZeroForUnlimited": "Anvend 0 for ubegrænset",
|
||||
"LabelUser": "Bruger",
|
||||
"LabelUsername": "Brugernavn",
|
||||
"LabelValue": "Værdi",
|
||||
"LabelVersion": "Version",
|
||||
"LabelViewBookmarks": "Se bogmærker",
|
||||
"LabelViewChapters": "Se kapitler",
|
||||
"LabelViewPlayerSettings": "Vis afspiller indstillinger",
|
||||
"LabelViewQueue": "Se afspilningskø",
|
||||
"LabelVolume": "Volumen",
|
||||
"LabelWebRedirectURLsDescription": "Godkend disse URL'er i din OAuth udgiver for at tillade omdirigering tilbage til hjemmesiden efter login:",
|
||||
"LabelWebRedirectURLsSubfolder": "Undermapper for omdirigerings URL'er",
|
||||
"LabelWeekdaysToRun": "Ugedage til kørsel",
|
||||
"LabelXBooks": "{0} bøger",
|
||||
"LabelXItems": "{0} genstande",
|
||||
"LabelYearReviewHide": "Skjul år i review",
|
||||
"LabelYearReviewShow": "Vis år i review",
|
||||
"LabelYourAudiobookDuration": "Din lydbogsvarighed",
|
||||
"LabelYourBookmarks": "Dine bogmærker",
|
||||
"LabelYourPlaylists": "Dine spillelister",
|
||||
@@ -489,10 +704,17 @@
|
||||
"MessageAddToPlayerQueue": "Tilføj til afspilningskø",
|
||||
"MessageAppriseDescription": "For at bruge denne funktion skal du have en instans af <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> kørende eller en API, der håndterer de samme anmodninger. <br /> Apprise API-webadressen skal være den fulde URL-sti for at sende underretningen, f.eks. hvis din API-instans er tilgængelig på <code>http://192.168.1.1:8337</code>, så skal du bruge <code>http://192.168.1.1:8337/notify</code>.",
|
||||
"MessageBackupsDescription": "Backups inkluderer brugere, brugerfremskridt, biblioteksvareoplysninger, serverindstillinger og billeder gemt i <code>/metadata/items</code> og <code>/metadata/authors</code>. Backups inkluderer <strong>ikke</strong> nogen filer gemt i dine biblioteksmapper.",
|
||||
"MessageBackupsLocationEditNote": "Note: Opdatering af backup sti vil ikke fjerne eller modificere eksisterende backups",
|
||||
"MessageBackupsLocationNoEditNote": "Note: Backup sti er sat igennem miljøvariabel og kan ikke ændres her.",
|
||||
"MessageBackupsLocationPathEmpty": "Backup sti kan ikke være tom",
|
||||
"MessageBatchEditPopulateMapDetailsAllHelp": "Opret felter slået til med data fra alle genstande. Felter med flere værdier vil blive sammenflettet",
|
||||
"MessageBatchEditPopulateMapDetailsItemHelp": "Opret kort med værdier der er slået til fra felter med data fra denne genstand",
|
||||
"MessageBatchQuickMatchDescription": "Quick Match vil forsøge at tilføje manglende omslag og metadata til de valgte elementer. Aktivér indstillingerne nedenfor for at tillade Quick Match at overskrive eksisterende omslag og/eller metadata.",
|
||||
"MessageBookshelfNoCollections": "Du har ikke oprettet nogen samlinger endnu",
|
||||
"MessageBookshelfNoCollectionsHelp": "Samlinger er offentlige. Alle brugere med adgang til biblioteket kan se dem.",
|
||||
"MessageBookshelfNoRSSFeeds": "Ingen RSS-feeds er åbne",
|
||||
"MessageBookshelfNoResultsForFilter": "Ingen resultater for filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "Intet resultat for query",
|
||||
"MessageBookshelfNoSeries": "Du har ingen serier",
|
||||
"MessageChapterEndIsAfter": "Kapitelslutningen er efter slutningen af din lydbog",
|
||||
"MessageChapterErrorFirstNotZero": "Første kapitel skal starte ved 0",
|
||||
@@ -502,19 +724,35 @@
|
||||
"MessageCheckingCron": "Tjekker cron...",
|
||||
"MessageConfirmCloseFeed": "Er du sikker på, at du vil lukke dette feed?",
|
||||
"MessageConfirmDeleteBackup": "Er du sikker på, at du vil slette backup for {0}?",
|
||||
"MessageConfirmDeleteDevice": "Er du sikker på at du vil fjerne elæser enhed \"{0}\"?",
|
||||
"MessageConfirmDeleteFile": "Dette vil slette filen fra dit filsystem. Er du sikker?",
|
||||
"MessageConfirmDeleteLibrary": "Er du sikker på, at du vil slette biblioteket permanent \"{0}\"?",
|
||||
"MessageConfirmDeleteLibraryItem": "Dette vil slette biblioteksgenstanden fra databasen og dit filsystem. Er du sikker?",
|
||||
"MessageConfirmDeleteLibraryItems": "Dette vil slette {0} biblioteksgenstande fra din database og filsystem. Er du sikker?",
|
||||
"MessageConfirmDeleteMetadataProvider": "Er du sikker på at du vil fjerne brugerdefineret metadata udgiver \"{0}\"?",
|
||||
"MessageConfirmDeleteNotification": "Er du sikker på at du vil fjerne denne notifikation?",
|
||||
"MessageConfirmDeleteSession": "Er du sikker på, at du vil slette denne session?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Er du sikker på at du vil indlejre metadata i {0} lydbogsfiler?",
|
||||
"MessageConfirmForceReScan": "Er du sikker på, at du vil tvinge en genindlæsning?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Er du sikker på, at du vil markere alle episoder som afsluttet?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Er du sikker på, at du vil markere alle episoder som ikke afsluttet?",
|
||||
"MessageConfirmMarkItemFinished": "Er du sikker på at du vil markere \"{0}\" som færdig?",
|
||||
"MessageConfirmMarkItemNotFinished": "Er du sikker på at du vil markere \"{0}\" som ikke færdige?",
|
||||
"MessageConfirmMarkSeriesFinished": "Er du sikker på, at du vil markere alle bøger i denne serie som afsluttet?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Er du sikker på, at du vil markere alle bøger i denne serie som ikke afsluttet?",
|
||||
"MessageConfirmNotificationTestTrigger": "Trigger denne notifikation med testdata?",
|
||||
"MessageConfirmPurgeCache": "Rensning af cache vil slette hele mappen ved <code>/metadata/cache</code>.<br /><br />Er dy sikker på at du vil fjerne cache mappen?",
|
||||
"MessageConfirmPurgeItemsCache": "Rensning af cache vil slette hele mappen ved <code>/metadata/cache/items</code>.<br />Er du sikker?",
|
||||
"MessageConfirmQuickEmbed": "Advarsel! Hurtigindlejring vil ikke backe dine lydfiler op. S'rg for at du har en backup af dine lydfiler. <br /><br />Vil du fortsætte?",
|
||||
"MessageConfirmQuickMatchEpisodes": "Hurtig match af afsnit vil overskrive detaljer givet et match kan findes. Kun ikke-matchede vil blive opdateret. Er du sikker?",
|
||||
"MessageConfirmReScanLibraryItems": "Er du sikker på at du vil genscanne {0} genstande?",
|
||||
"MessageConfirmRemoveAllChapters": "Er du sikker på, at du vil fjerne alle kapitler?",
|
||||
"MessageConfirmRemoveAuthor": "Er du sikker på, at du vil fjerne forfatteren \"{0}\"?",
|
||||
"MessageConfirmRemoveCollection": "Er du sikker på, at du vil fjerne samlingen \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Er du sikker på, at du vil fjerne episoden \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Er du sikker på, at du vil fjerne {0} episoder?",
|
||||
"MessageConfirmRemoveListeningSessions": "Er du sikker på at du vil fjerne {0} lytte sessioner?",
|
||||
"MessageConfirmRemoveMetadataFiles": "Er du sikker på at du vil fjerne alle metadata.{0} filer i dine biblioteksfoldere?",
|
||||
"MessageConfirmRemoveNarrator": "Er du sikker på, at du vil fjerne fortælleren \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "Er du sikker på, at du vil fjerne din spilleliste \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "Er du sikker på, at du vil omdøbe genre \"{0}\" til \"{1}\" for alle elementer?",
|
||||
@@ -523,11 +761,17 @@
|
||||
"MessageConfirmRenameTag": "Er du sikker på, at du vil omdøbe tag \"{0}\" til \"{1}\" for alle elementer?",
|
||||
"MessageConfirmRenameTagMergeNote": "Bemærk: Dette tag findes allerede, så de vil blive fusioneret.",
|
||||
"MessageConfirmRenameTagWarning": "Advarsel! Et lignende tag med en anden skrivemåde eksisterer allerede \"{0}\".",
|
||||
"MessageConfirmResetProgress": "Er du sikker på at du vil nulstille dit fremskridt?",
|
||||
"MessageConfirmSendEbookToDevice": "Er du sikker på, at du vil sende {0} e-bog \"{1}\" til enhed \"{2}\"?",
|
||||
"MessageConfirmUnlinkOpenId": "Er du sikker på at du vil fjerne linket mellem denne bruger og OpenID?",
|
||||
"MessageDaysListenedInTheLastYear": "{0} dage lyttet i løbet af det sidste år",
|
||||
"MessageDownloadingEpisode": "Downloader episode",
|
||||
"MessageDragFilesIntoTrackOrder": "Træk filer ind i korrekt spororden",
|
||||
"MessageEmbedFailed": "Indlejring fejlede!",
|
||||
"MessageEmbedFinished": "Indlejring færdig!",
|
||||
"MessageEmbedQueue": "Sat i kø for metadata indlejring ({0} i kø)",
|
||||
"MessageEpisodesQueuedForDownload": "{0} episoder er sat i kø til download",
|
||||
"MessageEreaderDevices": "For at sikre levering af ebøger, skal du eventuelt tilføje mailadressen som en gyldig afsender for hver enhed angivet forneden.",
|
||||
"MessageFeedURLWillBe": "Feed-URL vil være {0}",
|
||||
"MessageFetching": "Henter...",
|
||||
"MessageForceReScanDescription": "vil scanne alle filer igen som en frisk scanning. Lydfilens ID3-tags, OPF-filer og tekstfiler scannes som nye.",
|
||||
@@ -536,9 +780,9 @@
|
||||
"MessageItemsSelected": "{0} elementer valgt",
|
||||
"MessageItemsUpdated": "{0} elementer opdateret",
|
||||
"MessageJoinUsOn": "Deltag i os på",
|
||||
"MessageListeningSessionsInTheLastYear": "{0} lyttesessioner i det sidste år",
|
||||
"MessageLoading": "Indlæser...",
|
||||
"MessageLoadingFolders": "Indlæser mapper...",
|
||||
"MessageLogsDescription": "Logfiler er gemt i <code>/metadata/logs</code> som JSON filer. Crash log er gemt i <code>/metadata/logs/crash_logs.txt</code>.",
|
||||
"MessageM4BFailed": "M4B mislykkedes!",
|
||||
"MessageM4BFinished": "M4B afsluttet!",
|
||||
"MessageMapChapterTitles": "Tilknyt kapiteloverskrifter til dine eksisterende lydbogskapitler uden at justere tidsstempler",
|
||||
@@ -555,6 +799,7 @@
|
||||
"MessageNoCollections": "Ingen samlinger",
|
||||
"MessageNoCoversFound": "Ingen omslag fundet",
|
||||
"MessageNoDescription": "Ingen beskrivelse",
|
||||
"MessageNoDevices": "Ingen enheder",
|
||||
"MessageNoDownloadsInProgress": "Ingen downloads i gang lige nu",
|
||||
"MessageNoDownloadsQueued": "Ingen downloads i kø",
|
||||
"MessageNoEpisodeMatchesFound": "Ingen episode-matcher fundet",
|
||||
@@ -568,6 +813,7 @@
|
||||
"MessageNoLogs": "Ingen logfiler",
|
||||
"MessageNoMediaProgress": "Ingen medieforløb",
|
||||
"MessageNoNotifications": "Ingen meddelelser",
|
||||
"MessageNoPodcastFeed": "Invalid podcast: Intet feed",
|
||||
"MessageNoPodcastsFound": "Ingen podcasts fundet",
|
||||
"MessageNoResults": "Ingen resultater",
|
||||
"MessageNoSearchResultsFor": "Ingen søgeresultater for \"{0}\"",
|
||||
@@ -576,12 +822,19 @@
|
||||
"MessageNoTasksRunning": "Ingen opgaver kører",
|
||||
"MessageNoUpdatesWereNecessary": "Ingen opdateringer var nødvendige",
|
||||
"MessageNoUserPlaylists": "Du har ingen afspilningslister",
|
||||
"MessageNoUserPlaylistsHelp": "Playlister er private. Kun brugere som opretter dem kan se dem.",
|
||||
"MessageNotYetImplemented": "Endnu ikke implementeret",
|
||||
"MessageOpmlPreviewNote": "Note: Dette er en forhåndsvisning af den indlæste OPML fil. Podcast titel vil blive taget fra RSS feedet.",
|
||||
"MessageOr": "eller",
|
||||
"MessagePauseChapter": "Pause kapitelafspilning",
|
||||
"MessagePlayChapter": "Lyt til begyndelsen af kapitlet",
|
||||
"MessagePlaylistCreateFromCollection": "Opret afspilningsliste fra samling",
|
||||
"MessagePleaseWait": "Vent venligst...",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast har ingen RSS-feed-URL at bruge til matchning",
|
||||
"MessagePodcastSearchField": "Indtast søgeterm eller RSS URL",
|
||||
"MessageQuickEmbedInProgress": "Hurtig indlejring igang",
|
||||
"MessageQuickEmbedQueue": "I kø for hurtigindlejring ({0} i kø)",
|
||||
"MessageQuickMatchAllEpisodes": "Hurtig match alle afsnit",
|
||||
"MessageQuickMatchDescription": "Udfyld tomme elementoplysninger og omslag med første matchresultat fra '{0}'. Overskriver ikke oplysninger, medmindre serverindstillingen 'Foretræk matchet metadata' er aktiveret.",
|
||||
"MessageRemoveChapter": "Fjern kapitel",
|
||||
"MessageRemoveEpisodes": "Fjern {0} episode(r)",
|
||||
@@ -591,10 +844,50 @@
|
||||
"MessageResetChaptersConfirm": "Er du sikker på, at du vil nulstille kapitler og annullere ændringerne, du har foretaget?",
|
||||
"MessageRestoreBackupConfirm": "Er du sikker på, at du vil gendanne sikkerhedskopien oprettet den",
|
||||
"MessageRestoreBackupWarning": "Gendannelse af en sikkerhedskopi vil overskrive hele databasen, som er placeret på /config, og omslagsbilleder i /metadata/items & /metadata/authors.<br /><br />Sikkerhedskopier ændrer ikke nogen filer i dine biblioteksmapper. Hvis du har aktiveret serverindstillinger for at gemme omslagskunst og metadata i dine biblioteksmapper, sikkerhedskopieres eller overskrives disse ikke.<br /><br />Alle klienter, der bruger din server, opdateres automatisk.",
|
||||
"MessageScheduleLibraryScanNote": "For de fleste brugere, er det anbefalet at efterlade denne funktion deaktiveret for at holde mappe lurer indstilling aktiveret. Mappe lureren vil automatisk opdage ændringer i biblioteksmapper. Mappe lureren virker ikke for alle filsystemer (så som NFS) så schedulerede biblioteksscans vil blive anvendt.",
|
||||
"MessageSearchResultsFor": "Søgeresultater for",
|
||||
"MessageSelected": "{0} valgt",
|
||||
"MessageServerCouldNotBeReached": "Serveren kunne ikke nås",
|
||||
"MessageSetChaptersFromTracksDescription": "Indstil kapitler ved at bruge hver lydfil som et kapitel og kapiteloverskrift som lydfilnavn",
|
||||
"MessageShareExpirationWillBe": "Udløb vil være <strong>{0}</strong>",
|
||||
"MessageShareExpiresIn": "Udløber om {0}",
|
||||
"MessageShareURLWillBe": "Del URL vil være <strong>{0}</strong>",
|
||||
"MessageStartPlaybackAtTime": "Start afspilning for \"{0}\" kl. {1}?",
|
||||
"MessageTaskAudioFileNotWritable": "Lydbogsfil \"{0}\" er ikke skrivebar",
|
||||
"MessageTaskCanceledByUser": "Opgave annulleret af bruger",
|
||||
"MessageTaskDownloadingEpisodeDescription": "Download afsnit \"{0}\"",
|
||||
"MessageTaskEmbeddingMetadata": "Indlejring af metadata",
|
||||
"MessageTaskEmbeddingMetadataDescription": "Indlejring af metadata i lydbog \"{0}\"",
|
||||
"MessageTaskEncodingM4b": "Indkodning M4B",
|
||||
"MessageTaskEncodingM4bDescription": "Indkodning lydog \"{0}\" ind i en enkelt M4B fil",
|
||||
"MessageTaskFailed": "Fejlet",
|
||||
"MessageTaskFailedToBackupAudioFile": "Fejlede backup af lydbogsfil \"{0}\"",
|
||||
"MessageTaskFailedToCreateCacheDirectory": "Fejlede at oprette cache mappe",
|
||||
"MessageTaskFailedToEmbedMetadataInFile": "Fejlede at indkode metadata i fil \"{0}\"",
|
||||
"MessageTaskFailedToMergeAudioFiles": "Fejlede at sammenflette lydbogsfiler",
|
||||
"MessageTaskFailedToMoveM4bFile": "Fejlede i at flytte M4B fil",
|
||||
"MessageTaskFailedToWriteMetadataFile": "Fejlede i at skrive metadata fil",
|
||||
"MessageTaskMatchingBooksInLibrary": "Matchede bøger i bibliotek \"{0}\"",
|
||||
"MessageTaskNoFilesToScan": "Ingen filer at scanne",
|
||||
"MessageTaskOpmlImport": "OPML import",
|
||||
"MessageTaskOpmlImportDescription": "Oprettelse af podcasts fra {0} RSS feeds",
|
||||
"MessageTaskOpmlImportFeed": "OPML importering fejlede",
|
||||
"MessageTaskOpmlImportFeedDescription": "Importering af RSS feed \"{0}\"",
|
||||
"MessageTaskOpmlImportFeedFailed": "Fejlede at hente podcast feed",
|
||||
"MessageTaskOpmlImportFeedPodcastDescription": "Opretter podcast \"{0}\"",
|
||||
"MessageTaskOpmlImportFeedPodcastExists": "Podcast ligger allerede på filsti",
|
||||
"MessageTaskOpmlImportFeedPodcastFailed": "Fejlede i at oprette podcast",
|
||||
"MessageTaskOpmlImportFinished": "Tilføjede {0} podcasts",
|
||||
"MessageTaskOpmlParseFailed": "Fejlede i at læse OPML fil",
|
||||
"MessageTaskOpmlParseFastFail": "Forkert OPML <opml> tag ikke fundet ELLER et <outline> tag var ikke fundet",
|
||||
"MessageTaskOpmlParseNoneFound": "Ingen feeds fundet i OPML fil",
|
||||
"MessageTaskScanItemsAdded": "{0} tilføjet",
|
||||
"MessageTaskScanItemsMissing": "{0} mangler",
|
||||
"MessageTaskScanItemsUpdated": "{0} opdateret",
|
||||
"MessageTaskScanNoChangesNeeded": "Ingen ændringer nødvendigt",
|
||||
"MessageTaskScanningFileChanges": "Scanner filændringer i \"{0}\"",
|
||||
"MessageTaskScanningLibrary": "Scanning af \"{0}\" bibliotek",
|
||||
"MessageTaskTargetDirectoryNotWritable": "Mål sti er ikke skrivebar",
|
||||
"MessageThinking": "Tænker...",
|
||||
"MessageUploaderItemFailed": "Fejl ved upload",
|
||||
"MessageUploaderItemSuccess": "Uploadet med succes!",
|
||||
@@ -612,40 +905,102 @@
|
||||
"NoteUploaderFoldersWithMediaFiles": "Mapper med mediefiler håndteres som separate bibliotekselementer.",
|
||||
"NoteUploaderOnlyAudioFiles": "Hvis du kun uploader lydfiler, håndteres hver lydfil som en separat lydbog.",
|
||||
"NoteUploaderUnsupportedFiles": "Ikke-understøttede filer ignoreres. Når du vælger eller slipper en mappe, ignoreres andre filer, der ikke er i en emnemappe.",
|
||||
"NotificationOnBackupCompletedDescription": "Udløst når backup er færdig",
|
||||
"NotificationOnBackupFailedDescription": "Udløst når backup fejler",
|
||||
"NotificationOnEpisodeDownloadedDescription": "Udløst når et podcast afsnit er automatisk downloadet",
|
||||
"NotificationOnTestDescription": "Event for test af notifikationssystemet",
|
||||
"PlaceholderNewCollection": "Nyt samlingnavn",
|
||||
"PlaceholderNewFolderPath": "Ny mappes sti",
|
||||
"PlaceholderNewPlaylist": "Nyt afspilningslistnavn",
|
||||
"PlaceholderSearch": "Søg..",
|
||||
"PlaceholderSearchEpisode": "Søg efter episode..",
|
||||
"StatsAuthorsAdded": "forfattere tilføjet",
|
||||
"StatsBooksAdded": "bøger tilføjet",
|
||||
"StatsBooksAdditional": "Nogle tilføjelser inkludere…",
|
||||
"StatsBooksFinished": "bøger færdige",
|
||||
"StatsBooksFinishedThisYear": "Nogle bøger færdiggjort i år.…",
|
||||
"StatsBooksListenedTo": "bøger lyttet til",
|
||||
"StatsCollectionGrewTo": "Din bog kollektion voksede til…",
|
||||
"StatsSessions": "sessioner",
|
||||
"StatsSpentListening": "brugt at lytte",
|
||||
"StatsTopAuthor": "TOP FORFATTER",
|
||||
"StatsTopAuthors": "TOP FORFATTERE",
|
||||
"StatsTopGenre": "TOP GENRE",
|
||||
"StatsTopGenres": "TOP GENRER",
|
||||
"StatsTopMonth": "TOP MÅNED",
|
||||
"StatsTopNarrator": "TOP OPLÆSER",
|
||||
"StatsTopNarrators": "TOP OPLÆSERE",
|
||||
"StatsTotalDuration": "Med den totale varighed af…",
|
||||
"StatsYearInReview": "ÅR I REVIEW",
|
||||
"ToastAccountUpdateSuccess": "Konto opdateret",
|
||||
"ToastAppriseUrlRequired": "Skal indtaste en Apprise URL",
|
||||
"ToastAsinRequired": "ASIN er påkrævet",
|
||||
"ToastAuthorImageRemoveSuccess": "Forfatterbillede fjernet",
|
||||
"ToastAuthorNotFound": "Forfatter \"{0}\" ikke fundet",
|
||||
"ToastAuthorRemoveSuccess": "Forfatter fjernet",
|
||||
"ToastAuthorSearchNotFound": "Forfatter ikke fundet",
|
||||
"ToastAuthorUpdateMerged": "Forfatter fusioneret",
|
||||
"ToastAuthorUpdateSuccess": "Forfatter opdateret",
|
||||
"ToastAuthorUpdateSuccessNoImageFound": "Forfatter opdateret (ingen billede fundet)",
|
||||
"ToastBackupAppliedSuccess": "Backup indlæst",
|
||||
"ToastBackupCreateFailed": "Mislykkedes oprettelse af sikkerhedskopi",
|
||||
"ToastBackupCreateSuccess": "Sikkerhedskopi oprettet",
|
||||
"ToastBackupDeleteFailed": "Mislykkedes sletning af sikkerhedskopi",
|
||||
"ToastBackupDeleteSuccess": "Sikkerhedskopi slettet",
|
||||
"ToastBackupInvalidMaxKeep": "Forkert antal backups at beholde",
|
||||
"ToastBackupInvalidMaxSize": "Forkert maks backup størrelse",
|
||||
"ToastBackupRestoreFailed": "Mislykkedes gendannelse af sikkerhedskopi",
|
||||
"ToastBackupUploadFailed": "Mislykkedes upload af sikkerhedskopi",
|
||||
"ToastBackupUploadSuccess": "Sikkerhedskopi uploadet",
|
||||
"ToastBatchDeleteFailed": "Batch slet fejlede",
|
||||
"ToastBatchDeleteSuccess": "Batch slet succes",
|
||||
"ToastBatchQuickMatchFailed": "Batch Hurtig Match fejlede!",
|
||||
"ToastBatchQuickMatchStarted": "Batch Hurtig Match af {0} bøger startet!",
|
||||
"ToastBatchUpdateFailed": "Mislykkedes batchopdatering",
|
||||
"ToastBatchUpdateSuccess": "Batchopdatering lykkedes",
|
||||
"ToastBookmarkCreateFailed": "Mislykkedes oprettelse af bogmærke",
|
||||
"ToastBookmarkCreateSuccess": "Bogmærke tilføjet",
|
||||
"ToastBookmarkRemoveSuccess": "Bogmærke fjernet",
|
||||
"ToastBookmarkUpdateSuccess": "Bogmærke opdateret",
|
||||
"ToastCachePurgeFailed": "Fejlede at opryde cache",
|
||||
"ToastCachePurgeSuccess": "Cache ryddet op i succesfuldt",
|
||||
"ToastChaptersHaveErrors": "Kapitler har fejl",
|
||||
"ToastChaptersMustHaveTitles": "Kapitler skal have titler",
|
||||
"ToastCollectionItemsRemoveSuccess": "Element(er) fjernet fra samlingen",
|
||||
"ToastChaptersRemoved": "Kapitler fjernet",
|
||||
"ToastChaptersUpdated": "Kapitler opdateret",
|
||||
"ToastCollectionItemsAddFailed": "Genstand(e) tilføjet til kollektion fejlet",
|
||||
"ToastCollectionRemoveSuccess": "Samling fjernet",
|
||||
"ToastCollectionUpdateSuccess": "Samling opdateret",
|
||||
"ToastCoverUpdateFailed": "Cover opdatering fejlede",
|
||||
"ToastDateTimeInvalidOrIncomplete": "Dato og tid er forkert eller ufærdig",
|
||||
"ToastDeleteFileFailed": "Slet fil fejlede",
|
||||
"ToastDeleteFileSuccess": "Fil slettet",
|
||||
"ToastDeviceAddFailed": "Fejlede at tilføje enhed",
|
||||
"ToastDeviceNameAlreadyExists": "Elæser enhed med det navn eksistere allerede",
|
||||
"ToastDeviceTestEmailFailed": "Fejlede at sende test mail",
|
||||
"ToastDeviceTestEmailSuccess": "Test mail sendt",
|
||||
"ToastEmailSettingsUpdateSuccess": "Mail indstillinger opdateret",
|
||||
"ToastEncodeCancelFailed": "Fejlede at afbryde indkodning",
|
||||
"ToastEncodeCancelSucces": "Indkodning afbrudt",
|
||||
"ToastEpisodeDownloadQueueClearFailed": "Fejlede at rydde op i kø",
|
||||
"ToastEpisodeDownloadQueueClearSuccess": "Afsnit download kø renset",
|
||||
"ToastEpisodeUpdateSuccess": "{0} afsnit opdateret",
|
||||
"ToastErrorCannotShare": "Kan ikke dele på denne enhed",
|
||||
"ToastFailedToLoadData": "Fejlede at indlæse data",
|
||||
"ToastFailedToMatch": "Fejlet match",
|
||||
"ToastFailedToShare": "Fejlet deling",
|
||||
"ToastFailedToUpdate": "Fejlet opdatering",
|
||||
"ToastInvalidImageUrl": "Forkert billede URL",
|
||||
"ToastInvalidMaxEpisodesToDownload": "Forkert maks afsnit at hente",
|
||||
"ToastInvalidUrl": "Forkert URL",
|
||||
"ToastItemCoverUpdateSuccess": "Varens omslag opdateret",
|
||||
"ToastItemDeletedFailed": "Fejlede at slette genstand",
|
||||
"ToastItemDeletedSuccess": "Genstand slettet",
|
||||
"ToastItemDetailsUpdateSuccess": "Varedetaljer opdateret",
|
||||
"ToastItemMarkedAsFinishedFailed": "Mislykkedes markering som afsluttet",
|
||||
"ToastItemMarkedAsFinishedSuccess": "Vare markeret som afsluttet",
|
||||
"ToastItemMarkedAsNotFinishedFailed": "Mislykkedes markering som ikke afsluttet",
|
||||
"ToastItemMarkedAsNotFinishedSuccess": "Vare markeret som ikke afsluttet",
|
||||
"ToastItemUpdateSuccess": "Genstand opdateret",
|
||||
"ToastLibraryCreateFailed": "Mislykkedes oprettelse af bibliotek",
|
||||
"ToastLibraryCreateSuccess": "Bibliotek \"{0}\" oprettet",
|
||||
"ToastLibraryDeleteFailed": "Mislykkedes sletning af bibliotek",
|
||||
@@ -653,25 +1008,84 @@
|
||||
"ToastLibraryScanFailedToStart": "Mislykkedes start af skanning",
|
||||
"ToastLibraryScanStarted": "Biblioteksskanning startet",
|
||||
"ToastLibraryUpdateSuccess": "Bibliotek \"{0}\" opdateret",
|
||||
"ToastMatchAllAuthorsFailed": "Fejlede at matche alle forfattere",
|
||||
"ToastMetadataFilesRemovedError": "Fejlet at fjerne metadata.{0} filer",
|
||||
"ToastMetadataFilesRemovedNoneFound": "Ingen metadata.{0} filer fundet i bibliotek",
|
||||
"ToastMetadataFilesRemovedNoneRemoved": "Ingen metadata.{0} filer slettet",
|
||||
"ToastMetadataFilesRemovedSuccess": "{0} metadata.{1} filer slettet",
|
||||
"ToastMustHaveAtLeastOnePath": "Skal have mindst en sti",
|
||||
"ToastNameEmailRequired": "Navn og email påkrævet",
|
||||
"ToastNameRequired": "Navn påkrævet",
|
||||
"ToastNewEpisodesFound": "{0} nye afsnit fundet",
|
||||
"ToastNewUserCreatedFailed": "Fejlede at oprette konto: \"{0}\"",
|
||||
"ToastNewUserCreatedSuccess": "Ny konto oprettet",
|
||||
"ToastNewUserLibraryError": "Skal vælge mindst et bibliotek",
|
||||
"ToastNewUserPasswordError": "Skal have et password, kun root brugeren kan have et tomt password",
|
||||
"ToastNewUserTagError": "Skal vælge mindst et tag",
|
||||
"ToastNewUserUsernameError": "Angiv brugernavn",
|
||||
"ToastNoNewEpisodesFound": "Ingen nye afsnit fundet",
|
||||
"ToastNoRSSFeed": "Podcast har ingen RSS feed",
|
||||
"ToastNoUpdatesNecessary": "Ingen opdateringer nødvendige",
|
||||
"ToastNotificationCreateFailed": "Fejlede at oprette notifikation",
|
||||
"ToastNotificationDeleteFailed": "Fejlede at slette notifikation",
|
||||
"ToastNotificationFailedMaximum": "Maks forsøg skal være >= 0",
|
||||
"ToastNotificationQueueMaximum": "Maks notifikationskø skal være >= 0",
|
||||
"ToastNotificationSettingsUpdateSuccess": "Notifikationsindstillinger opdateret",
|
||||
"ToastNotificationTestTriggerFailed": "Fejlede at oprette en test notifikation",
|
||||
"ToastNotificationTestTriggerSuccess": "Test notifikation oprettet",
|
||||
"ToastNotificationUpdateSuccess": "Notifikation opdateret",
|
||||
"ToastPlaylistCreateFailed": "Mislykkedes oprettelse af afspilningsliste",
|
||||
"ToastPlaylistCreateSuccess": "Afspilningsliste oprettet",
|
||||
"ToastPlaylistRemoveSuccess": "Afspilningsliste fjernet",
|
||||
"ToastPlaylistUpdateSuccess": "Afspilningsliste opdateret",
|
||||
"ToastPodcastCreateFailed": "Mislykkedes oprettelse af podcast",
|
||||
"ToastPodcastCreateSuccess": "Podcast oprettet med succes",
|
||||
"ToastPodcastGetFeedFailed": "Fejlede at hente podcast feed",
|
||||
"ToastPodcastNoEpisodesInFeed": "Ingen nye afsnit fundet i RSS feed",
|
||||
"ToastPodcastNoRssFeed": "Podcast har ingen RSS feed",
|
||||
"ToastProgressIsNotBeingSynced": "Fremskridt ikke synkroniseret, genstart afspilning",
|
||||
"ToastProviderCreatedFailed": "Fejlede at tilføje udbyder",
|
||||
"ToastProviderCreatedSuccess": "Ny udbyder tilføjet",
|
||||
"ToastProviderNameAndUrlRequired": "Navn og URL påkrævet",
|
||||
"ToastProviderRemoveSuccess": "Udbyder fjernet",
|
||||
"ToastRSSFeedCloseFailed": "Mislykkedes lukning af RSS-feed",
|
||||
"ToastRSSFeedCloseSuccess": "RSS-feed lukket",
|
||||
"ToastRemoveFailed": "Fejlede at slette",
|
||||
"ToastRemoveItemFromCollectionFailed": "Mislykkedes fjernelse af element fra samling",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Element fjernet fra samling",
|
||||
"ToastRemoveItemsWithIssuesFailed": "Fejlede at slette genstande med fejl",
|
||||
"ToastRemoveItemsWithIssuesSuccess": "Slettede genstande med fejl",
|
||||
"ToastRenameFailed": "Fejlede at omdøbe",
|
||||
"ToastRescanFailed": "Genscan fejlede for {0}",
|
||||
"ToastRescanRemoved": "Genscan gennemført, genstand blev fjernet",
|
||||
"ToastRescanUpToDate": "Genscan gennemført, genstand var opdateret",
|
||||
"ToastRescanUpdated": "Genscan gennemført, genstand blev opdateret",
|
||||
"ToastScanFailed": "Fejlede at scanne biblioteksgenstand",
|
||||
"ToastSelectAtLeastOneUser": "Vælg mindst en bruger",
|
||||
"ToastSendEbookToDeviceFailed": "Mislykkedes afsendelse af e-bog til enhed",
|
||||
"ToastSendEbookToDeviceSuccess": "E-bog afsendt til enhed \"{0}\"",
|
||||
"ToastSeriesUpdateFailed": "Mislykkedes opdatering af serie",
|
||||
"ToastSeriesUpdateSuccess": "Serieopdatering lykkedes",
|
||||
"ToastServerSettingsUpdateSuccess": "Server indstillinger opdateret",
|
||||
"ToastSessionCloseFailed": "Luk session fejlede",
|
||||
"ToastSessionDeleteFailed": "Mislykkedes sletning af session",
|
||||
"ToastSessionDeleteSuccess": "Session slettet",
|
||||
"ToastSleepTimerDone": "Sleep timer færdig... zZzzZz",
|
||||
"ToastSlugMustChange": "Snegl indeholder ugyldige karakterer",
|
||||
"ToastSlugRequired": "Snegl påkrævet",
|
||||
"ToastSocketConnected": "Socket forbundet",
|
||||
"ToastSocketDisconnected": "Socket afbrudt",
|
||||
"ToastSocketFailedToConnect": "Socket kunne ikke oprettes",
|
||||
"ToastSortingPrefixesEmptyError": "Skal indeholde mindst 1 sorteringspræfiks",
|
||||
"ToastSortingPrefixesUpdateSuccess": "Sortering af præfiks opdateret ({0} genstande)",
|
||||
"ToastTitleRequired": "Titel påkrævet",
|
||||
"ToastUnknownError": "Ukendt fejl",
|
||||
"ToastUnlinkOpenIdFailed": "Fejlede i af afkoble bruger fra OpenID",
|
||||
"ToastUnlinkOpenIdSuccess": "Bruger afkoblet fra OpenID",
|
||||
"ToastUserDeleteFailed": "Mislykkedes sletning af bruger",
|
||||
"ToastUserDeleteSuccess": "Bruger slettet"
|
||||
"ToastUserDeleteSuccess": "Bruger slettet",
|
||||
"ToastUserPasswordChangeSuccess": "Password ændret",
|
||||
"ToastUserPasswordMismatch": "Passwords passer ikke sammen",
|
||||
"ToastUserPasswordMustChange": "Nyt password må ikke være det gamle",
|
||||
"ToastUserRootRequireName": "Skal indholde et root brugernavn"
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"ButtonApplyChapters": "Kapitel anwenden",
|
||||
"ButtonAuthors": "Autoren",
|
||||
"ButtonBack": "Zurück",
|
||||
"ButtonBatchEditPopulateFromExisting": "Auffüllen aus vorhandenem",
|
||||
"ButtonBatchEditPopulateMapDetails": "Kartendetails auffüllen",
|
||||
"ButtonBrowseForFolder": "Ordnersuche",
|
||||
"ButtonCancel": "Abbrechen",
|
||||
"ButtonCancelEncode": "Codierung abbrechen",
|
||||
@@ -51,7 +53,7 @@
|
||||
"ButtonNext": "Vor",
|
||||
"ButtonNextChapter": "Nächstes Kapitel",
|
||||
"ButtonNextItemInQueue": "Das nächste Element in der Warteschlange",
|
||||
"ButtonOk": "OK",
|
||||
"ButtonOk": "Einverstanden",
|
||||
"ButtonOpenFeed": "Feed öffnen",
|
||||
"ButtonOpenManager": "Manager öffnen",
|
||||
"ButtonPause": "Pausieren",
|
||||
@@ -88,6 +90,8 @@
|
||||
"ButtonSaveTracklist": "Speichere die Titelliste",
|
||||
"ButtonScan": "Partial-Scan (nur geänderte/neue Medien)",
|
||||
"ButtonScanLibrary": "Bibliothek scannen",
|
||||
"ButtonScrollLeft": "Nach Links scrollen",
|
||||
"ButtonScrollRight": "Nach Rechts scrollen",
|
||||
"ButtonSearch": "Suchen",
|
||||
"ButtonSelectFolderPath": "Ordnerpfad auswählen",
|
||||
"ButtonSeries": "Serien",
|
||||
@@ -190,6 +194,7 @@
|
||||
"HeaderSettingsExperimental": "Experimentelle Funktionen",
|
||||
"HeaderSettingsGeneral": "Allgemein",
|
||||
"HeaderSettingsScanner": "Scanner",
|
||||
"HeaderSettingsWebClient": "Web-Client",
|
||||
"HeaderSleepTimer": "Sleep-Timer",
|
||||
"HeaderStatsLargestItems": "Größte Medien",
|
||||
"HeaderStatsLongestItems": "Längste Medien (h)",
|
||||
@@ -297,6 +302,7 @@
|
||||
"LabelDiscover": "Entdecken",
|
||||
"LabelDownload": "Herunterladen",
|
||||
"LabelDownloadNEpisodes": "Download {0} Episoden",
|
||||
"LabelDownloadable": "Herunterladbar",
|
||||
"LabelDuration": "Laufzeit",
|
||||
"LabelDurationComparisonExactMatch": "(genauer Treffer)",
|
||||
"LabelDurationComparisonLonger": "({0} länger)",
|
||||
@@ -480,6 +486,7 @@
|
||||
"LabelPersonalYearReview": "Dein Jahr in Übersicht ({0})",
|
||||
"LabelPhotoPathURL": "Foto Pfad/URL",
|
||||
"LabelPlayMethod": "Abspielmethode",
|
||||
"LabelPlaybackRateIncrementDecrement": "Wiedergaberate der Erhöhung/Verminderung",
|
||||
"LabelPlayerChapterNumberMarker": "{0} von {1}",
|
||||
"LabelPlaylists": "Wiedergabelisten",
|
||||
"LabelPodcast": "Podcast",
|
||||
@@ -542,6 +549,7 @@
|
||||
"LabelServerYearReview": "Server Jahr in Übersicht ({0})",
|
||||
"LabelSetEbookAsPrimary": "Als Hauptbuch setzen",
|
||||
"LabelSetEbookAsSupplementary": "Als Ergänzung setzen",
|
||||
"LabelSettingsAllowIframe": "Einbetten in einem iFrame erlauben",
|
||||
"LabelSettingsAudiobooksOnly": "Nur Hörbücher",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "Wenn du diese Einstellung aktivierst, werden E-Buch-Dateien ignoriert, es sei denn, sie befinden sich in einem Hörbuchordner. In diesem Fall werden sie als zusätzliche E-Bücher festgelegt",
|
||||
"LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden",
|
||||
@@ -584,6 +592,7 @@
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet",
|
||||
"LabelSettingsTimeFormat": "Zeitformat",
|
||||
"LabelShare": "Freigeben",
|
||||
"LabelShareDownloadableHelp": "Erlaubt es einem Nutzer, mit dem Link, die Dateien des Mediums als ZIP herunterzuladen.",
|
||||
"LabelShareOpen": "Freigeben",
|
||||
"LabelShareURL": "Freigabe URL",
|
||||
"LabelShowAll": "Alles anzeigen",
|
||||
@@ -592,6 +601,8 @@
|
||||
"LabelSize": "Größe",
|
||||
"LabelSleepTimer": "Schlummerfunktion",
|
||||
"LabelSlug": "URL Teil",
|
||||
"LabelSortAscending": "Aufsteigend",
|
||||
"LabelSortDescending": "Absteigend",
|
||||
"LabelStart": "Start",
|
||||
"LabelStartTime": "Startzeit",
|
||||
"LabelStarted": "Gestartet",
|
||||
@@ -637,7 +648,7 @@
|
||||
"LabelTimeToShift": "Zeit bis zum Wechsel in Sekunden",
|
||||
"LabelTitle": "Titel",
|
||||
"LabelToolsEmbedMetadata": "Metadaten einbetten",
|
||||
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.",
|
||||
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodateien ein.",
|
||||
"LabelToolsM4bEncoder": "M4B Kodierer",
|
||||
"LabelToolsMakeM4b": "M4B-Datei erstellen",
|
||||
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Datei (Endung \".m4b\") welche mehrere mp3-Dateien in einer einzigen Datei inkl. derer Metadaten (Beschreibung, Titelbild, Kapitel, ...) zusammenfasst. M4B-Datei können darüber hinaus Lesezeichen speichern und mit einem Abspielschutz (Passwort) versehen werden.",
|
||||
@@ -679,6 +690,8 @@
|
||||
"LabelViewPlayerSettings": "Zeige player Einstellungen",
|
||||
"LabelViewQueue": "Player-Warteschlange anzeigen",
|
||||
"LabelVolume": "Lautstärke",
|
||||
"LabelWebRedirectURLsDescription": "Autorisiere diese URLs bei deinem OAuth-Anbieter, um die Weiterleitung zurück zur Webanwendung nach dem Login zu ermöglichen:",
|
||||
"LabelWebRedirectURLsSubfolder": "Unterordner für Weiterleitung-URLs",
|
||||
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
||||
"LabelXBooks": "{0} Bücher",
|
||||
"LabelXItems": "{0} Medien",
|
||||
@@ -694,8 +707,10 @@
|
||||
"MessageBackupsLocationEditNote": "Hinweis: Durch das Aktualisieren des Backup-Speicherorts werden vorhandene Sicherungen nicht verschoben oder geändert",
|
||||
"MessageBackupsLocationNoEditNote": "Hinweis: Der Sicherungsspeicherort wird über eine Umgebungsvariable festgelegt und kann hier nicht geändert werden.",
|
||||
"MessageBackupsLocationPathEmpty": "Der Backup-Pfad darf nicht leer sein",
|
||||
"MessageBatchEditPopulateMapDetailsAllHelp": "Fülle die aktivierten Felder mit Daten aus allen Elementen. Felder mit mehreren Werten werden zusammengeführt",
|
||||
"MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktiviere die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.",
|
||||
"MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
|
||||
"MessageBookshelfNoCollectionsHelp": "Sammlungen sind öffentlich. Alle Benutzer mit Zugriff auf die Bibliothek können sie sehen.",
|
||||
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
|
||||
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für Filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "Keine Ergebnisse für die Abfrage",
|
||||
@@ -748,6 +763,7 @@
|
||||
"MessageConfirmResetProgress": "Möchtest du Ihren Fortschritt wirklich zurücksetzen?",
|
||||
"MessageConfirmSendEbookToDevice": "{0} E-Buch „{1}“ wird auf das Gerät „{2}“ gesendet! Bist du dir sicher?",
|
||||
"MessageConfirmUnlinkOpenId": "Möchtest du die Verknüpfung dieses Benutzers mit OpenID wirklich löschen?",
|
||||
"MessageDaysListenedInTheLastYear": "{0} Tage in dem letzten Jahr gehört",
|
||||
"MessageDownloadingEpisode": "Episode wird heruntergeladen",
|
||||
"MessageDragFilesIntoTrackOrder": "Verschiebe die Dateien in die richtige Reihenfolge",
|
||||
"MessageEmbedFailed": "Einbetten fehlgeschlagen!",
|
||||
@@ -763,7 +779,6 @@
|
||||
"MessageItemsSelected": "{0} ausgewählte Medien",
|
||||
"MessageItemsUpdated": "{0} Medien aktualisiert",
|
||||
"MessageJoinUsOn": "Besuche uns auf",
|
||||
"MessageListeningSessionsInTheLastYear": "{0} Ereignisse im letzten Jahr",
|
||||
"MessageLoading": "Wird geladen …",
|
||||
"MessageLoadingFolders": "Lade Ordner...",
|
||||
"MessageLogsDescription": "Die Logs werdern in <code>/metadata/logs</code> als JSON Dateien gespeichert. Crash logs werden in <code>/metadata/logs/crash_logs.txt</code> gespeichert.",
|
||||
@@ -806,6 +821,7 @@
|
||||
"MessageNoTasksRunning": "Keine laufenden Aufgaben",
|
||||
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
|
||||
"MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden",
|
||||
"MessageNoUserPlaylistsHelp": "Wiedergabelisten sind privat. Nur der Benutzer, der sie erstellt hat, kann sie sehen.",
|
||||
"MessageNotYetImplemented": "Noch nicht implementiert",
|
||||
"MessageOpmlPreviewNote": "Hinweis: Dies ist nur eine Vorschau der geparsten OPML Datei. Der eigentliche Podcast-Titel wird aus dem RSS-Feed übernommen.",
|
||||
"MessageOr": "Oder",
|
||||
@@ -827,6 +843,7 @@
|
||||
"MessageResetChaptersConfirm": "Kapitel und vorgenommenen Änderungen werden zurückgesetzt und rückgängig gemacht! Bist du dir sicher?",
|
||||
"MessageRestoreBackupConfirm": "Bist du dir sicher, dass du die Sicherung wiederherstellen willst, welche am",
|
||||
"MessageRestoreBackupWarning": "Bei der Wiederherstellung einer Sicherung wird die gesamte Datenbank unter /config und die Titelbilder in /metadata/items und /metadata/authors überschrieben.<br /><br />Bei der Sicherung werden keine Dateien in deinen Bibliotheksordnern verändert. Wenn du die Servereinstellungen aktiviert hast, um Cover und Metadaten in deinen Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.<br /><br />Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.",
|
||||
"MessageScheduleLibraryScanNote": "Für die meisten Nutzer wird empfohlen, diese Funktion deaktiviert zu lassen und stattdessen die Ordnerüberwachung aktiviert zu lassen. Die Ordnerüberwachung erkennt automatisch Änderungen in deinen Bibliotheksordnern. Da die Ordnerüberwachung jedoch nicht mit jedem Dateisystem (z.B. NFS) funktioniert, können alternativ hier geplante Bibliotheks-Scans aktiviert werden.",
|
||||
"MessageSearchResultsFor": "Suchergebnisse für",
|
||||
"MessageSelected": "{0} ausgewählt",
|
||||
"MessageServerCouldNotBeReached": "Server kann nicht erreicht werden",
|
||||
@@ -902,7 +919,7 @@
|
||||
"StatsBooksFinished": "Beendete Bücher",
|
||||
"StatsBooksFinishedThisYear": "Einige Bücher, die dieses Jahr beendet wurden…",
|
||||
"StatsBooksListenedTo": "gehörte Bücher",
|
||||
"StatsCollectionGrewTo": "Deine Bückersammlung ist gewachsen auf…",
|
||||
"StatsCollectionGrewTo": "Deine Büchersammlung ist gewachsen auf…",
|
||||
"StatsSessions": "Sitzungen",
|
||||
"StatsSpentListening": "zugehört",
|
||||
"StatsTopAuthor": "TOP AUTOR",
|
||||
@@ -943,7 +960,6 @@
|
||||
"ToastBookmarkCreateFailed": "Lesezeichen konnte nicht erstellt werden",
|
||||
"ToastBookmarkCreateSuccess": "Lesezeichen hinzugefügt",
|
||||
"ToastBookmarkRemoveSuccess": "Lesezeichen entfernt",
|
||||
"ToastBookmarkUpdateSuccess": "Lesezeichen aktualisiert",
|
||||
"ToastCachePurgeFailed": "Cache leeren fehlgeschlagen",
|
||||
"ToastCachePurgeSuccess": "Cache geleert",
|
||||
"ToastChaptersHaveErrors": "Kapitel sind fehlerhaft",
|
||||
@@ -951,11 +967,10 @@
|
||||
"ToastChaptersRemoved": "Kapitel entfernt",
|
||||
"ToastChaptersUpdated": "Kapitel aktualisiert",
|
||||
"ToastCollectionItemsAddFailed": "Das Hinzufügen von Element(en) zur Sammlung ist fehlgeschlagen",
|
||||
"ToastCollectionItemsAddSuccess": "Element(e) erfolgreich zur Sammlung hinzugefügt",
|
||||
"ToastCollectionItemsRemoveSuccess": "Medien aus der Sammlung entfernt",
|
||||
"ToastCollectionRemoveSuccess": "Sammlung entfernt",
|
||||
"ToastCollectionUpdateSuccess": "Sammlung aktualisiert",
|
||||
"ToastCoverUpdateFailed": "Cover-Update fehlgeschlagen",
|
||||
"ToastDateTimeInvalidOrIncomplete": "Datum und Zeit sind ungültig oder unvollständig",
|
||||
"ToastDeleteFileFailed": "Die Datei konnte nicht gelöscht werden",
|
||||
"ToastDeleteFileSuccess": "Datei gelöscht",
|
||||
"ToastDeviceAddFailed": "Gerät konnte nicht hinzugefügt werden",
|
||||
@@ -1008,6 +1023,7 @@
|
||||
"ToastNewUserTagError": "Mindestens ein Tag muss ausgewählt sein",
|
||||
"ToastNewUserUsernameError": "Nutzername eingeben",
|
||||
"ToastNoNewEpisodesFound": "Keine neuen Episoden gefunden",
|
||||
"ToastNoRSSFeed": "Podcast hat keinen RSS-Feed",
|
||||
"ToastNoUpdatesNecessary": "Keine Änderungen nötig",
|
||||
"ToastNotificationCreateFailed": "Fehler beim erstellen der Benachrichtig",
|
||||
"ToastNotificationDeleteFailed": "Fehler beim löschen der Benachrichtigung",
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"ButtonApplyChapters": "Apply Chapters",
|
||||
"ButtonAuthors": "Authors",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBatchEditPopulateFromExisting": "Populate from existing",
|
||||
"ButtonBatchEditPopulateMapDetails": "Populate map details",
|
||||
"ButtonBrowseForFolder": "Browse for Folder",
|
||||
"ButtonCancel": "Cancel",
|
||||
"ButtonCancelEncode": "Cancel Encode",
|
||||
@@ -88,6 +90,8 @@
|
||||
"ButtonSaveTracklist": "Save Tracklist",
|
||||
"ButtonScan": "Scan",
|
||||
"ButtonScanLibrary": "Scan Library",
|
||||
"ButtonScrollLeft": "Scroll Left",
|
||||
"ButtonScrollRight": "Scroll Right",
|
||||
"ButtonSearch": "Search",
|
||||
"ButtonSelectFolderPath": "Select Folder Path",
|
||||
"ButtonSeries": "Series",
|
||||
@@ -190,6 +194,7 @@
|
||||
"HeaderSettingsExperimental": "Experimental Features",
|
||||
"HeaderSettingsGeneral": "General",
|
||||
"HeaderSettingsScanner": "Scanner",
|
||||
"HeaderSettingsWebClient": "Web Client",
|
||||
"HeaderSleepTimer": "Sleep Timer",
|
||||
"HeaderStatsLargestItems": "Largest Items",
|
||||
"HeaderStatsLongestItems": "Longest Items (hrs)",
|
||||
@@ -297,6 +302,7 @@
|
||||
"LabelDiscover": "Discover",
|
||||
"LabelDownload": "Download",
|
||||
"LabelDownloadNEpisodes": "Download {0} episodes",
|
||||
"LabelDownloadable": "Downloadable",
|
||||
"LabelDuration": "Duration",
|
||||
"LabelDurationComparisonExactMatch": "(exact match)",
|
||||
"LabelDurationComparisonLonger": "({0} longer)",
|
||||
@@ -480,6 +486,7 @@
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPhotoPathURL": "Photo Path/URL",
|
||||
"LabelPlayMethod": "Play Method",
|
||||
"LabelPlaybackRateIncrementDecrement": "Playback Rate Increment/Decrement Amount",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Playlists",
|
||||
"LabelPodcast": "Podcast",
|
||||
@@ -542,6 +549,7 @@
|
||||
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||
"LabelSetEbookAsPrimary": "Set as primary",
|
||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
||||
"LabelSettingsAllowIframe": "Allow embedding in an iframe",
|
||||
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
|
||||
"LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves",
|
||||
@@ -584,6 +592,7 @@
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders",
|
||||
"LabelSettingsTimeFormat": "Time Format",
|
||||
"LabelShare": "Share",
|
||||
"LabelShareDownloadableHelp": "Allows users with the share link to download a zip file of the library item.",
|
||||
"LabelShareOpen": "Share Open",
|
||||
"LabelShareURL": "Share URL",
|
||||
"LabelShowAll": "Show All",
|
||||
@@ -592,6 +601,8 @@
|
||||
"LabelSize": "Size",
|
||||
"LabelSleepTimer": "Sleep timer",
|
||||
"LabelSlug": "Slug",
|
||||
"LabelSortAscending": "Ascending",
|
||||
"LabelSortDescending": "Descending",
|
||||
"LabelStart": "Start",
|
||||
"LabelStartTime": "Start Time",
|
||||
"LabelStarted": "Started",
|
||||
@@ -696,8 +707,11 @@
|
||||
"MessageBackupsLocationEditNote": "Note: Updating the backup location will not move or modify existing backups",
|
||||
"MessageBackupsLocationNoEditNote": "Note: The backup location is set through an environment variable and cannot be changed here.",
|
||||
"MessageBackupsLocationPathEmpty": "Backup location path cannot be empty",
|
||||
"MessageBatchEditPopulateMapDetailsAllHelp": "Populate enabled fields with data from all items. Fields with multiple values will be merged",
|
||||
"MessageBatchEditPopulateMapDetailsItemHelp": "Populate enabled map details fields with data from this item",
|
||||
"MessageBatchQuickMatchDescription": "Quick Match will attempt to add missing covers and metadata for the selected items. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.",
|
||||
"MessageBookshelfNoCollections": "You haven't made any collections yet",
|
||||
"MessageBookshelfNoCollectionsHelp": "Collections are public. All users with access to the library can see them.",
|
||||
"MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
|
||||
"MessageBookshelfNoResultsForFilter": "No results for filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
@@ -750,6 +764,7 @@
|
||||
"MessageConfirmResetProgress": "Are you sure you want to reset your progress?",
|
||||
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
||||
"MessageConfirmUnlinkOpenId": "Are you sure you want to unlink this user from OpenID?",
|
||||
"MessageDaysListenedInTheLastYear": "{0} days listened in the last year",
|
||||
"MessageDownloadingEpisode": "Downloading episode",
|
||||
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
||||
"MessageEmbedFailed": "Embed Failed!",
|
||||
@@ -765,7 +780,6 @@
|
||||
"MessageItemsSelected": "{0} Items Selected",
|
||||
"MessageItemsUpdated": "{0} Items Updated",
|
||||
"MessageJoinUsOn": "Join us on",
|
||||
"MessageListeningSessionsInTheLastYear": "{0} listening sessions in the last year",
|
||||
"MessageLoading": "Loading...",
|
||||
"MessageLoadingFolders": "Loading folders...",
|
||||
"MessageLogsDescription": "Logs are stored in <code>/metadata/logs</code> as JSON files. Crash logs are stored in <code>/metadata/logs/crash_logs.txt</code>.",
|
||||
@@ -808,6 +822,7 @@
|
||||
"MessageNoTasksRunning": "No Tasks Running",
|
||||
"MessageNoUpdatesWereNecessary": "No updates were necessary",
|
||||
"MessageNoUserPlaylists": "You have no playlists",
|
||||
"MessageNoUserPlaylistsHelp": "Playlists are private. Only the user who creates them can see them.",
|
||||
"MessageNotYetImplemented": "Not yet implemented",
|
||||
"MessageOpmlPreviewNote": "Note: This is a preview of the parsed OPML file. The actual podcast title will be taken from the RSS feed.",
|
||||
"MessageOr": "or",
|
||||
@@ -829,6 +844,7 @@
|
||||
"MessageResetChaptersConfirm": "Are you sure you want to reset chapters and undo the changes you made?",
|
||||
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
|
||||
"MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.",
|
||||
"MessageScheduleLibraryScanNote": "For most users, it is recommended to leave this feature disabled and keep the folder watcher setting enabled. The folder watcher will automatically detect changes in your library folders. The folder watcher doesn't work for every file system (like NFS) so scheduled library scans can be used instead.",
|
||||
"MessageSearchResultsFor": "Search results for",
|
||||
"MessageSelected": "{0} selected",
|
||||
"MessageServerCouldNotBeReached": "Server could not be reached",
|
||||
@@ -945,7 +961,6 @@
|
||||
"ToastBookmarkCreateFailed": "Failed to create bookmark",
|
||||
"ToastBookmarkCreateSuccess": "Bookmark added",
|
||||
"ToastBookmarkRemoveSuccess": "Bookmark removed",
|
||||
"ToastBookmarkUpdateSuccess": "Bookmark updated",
|
||||
"ToastCachePurgeFailed": "Failed to purge cache",
|
||||
"ToastCachePurgeSuccess": "Cache purged successfully",
|
||||
"ToastChaptersHaveErrors": "Chapters have errors",
|
||||
@@ -953,11 +968,10 @@
|
||||
"ToastChaptersRemoved": "Chapters removed",
|
||||
"ToastChaptersUpdated": "Chapters updated",
|
||||
"ToastCollectionItemsAddFailed": "Item(s) added to collection failed",
|
||||
"ToastCollectionItemsAddSuccess": "Item(s) added to collection success",
|
||||
"ToastCollectionItemsRemoveSuccess": "Item(s) removed from collection",
|
||||
"ToastCollectionRemoveSuccess": "Collection removed",
|
||||
"ToastCollectionUpdateSuccess": "Collection updated",
|
||||
"ToastCoverUpdateFailed": "Cover update failed",
|
||||
"ToastDateTimeInvalidOrIncomplete": "Date and time is invalid or incomplete",
|
||||
"ToastDeleteFileFailed": "Failed to delete file",
|
||||
"ToastDeleteFileSuccess": "File deleted",
|
||||
"ToastDeviceAddFailed": "Failed to add device",
|
||||
@@ -1010,6 +1024,7 @@
|
||||
"ToastNewUserTagError": "Must select at least one tag",
|
||||
"ToastNewUserUsernameError": "Enter a username",
|
||||
"ToastNoNewEpisodesFound": "No new episodes found",
|
||||
"ToastNoRSSFeed": "Podcast does not have an RSS Feed",
|
||||
"ToastNoUpdatesNecessary": "No updates necessary",
|
||||
"ToastNotificationCreateFailed": "Failed to create notification",
|
||||
"ToastNotificationDeleteFailed": "Failed to delete notification",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user