mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-31 11:38:47 -05:00
Compare commits
1225 Commits
v2.11.0
...
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 | ||
|
|
890b0b949e | ||
|
|
b19e360bbb | ||
|
|
1ff7952074 | ||
|
|
259d93d882 | ||
|
|
14f60a593b | ||
|
|
7334580c8c | ||
|
|
f467c44543 | ||
|
|
867354e59d | ||
|
|
67952cc577 | ||
|
|
079a15541c | ||
|
|
658ac04268 | ||
|
|
cbee6d8f5e | ||
|
|
68413ae2f6 | ||
|
|
252a233282 | ||
|
|
c35185fff7 | ||
|
|
9774b2cfa5 | ||
|
|
344890fb45 | ||
|
|
5fa0897ad7 | ||
|
|
95c80a5b18 | ||
|
|
0f1b64b883 | ||
|
|
615ed26f0f | ||
|
|
84803cef82 | ||
|
|
605bd73c11 | ||
|
|
cc89db059b | ||
|
|
a03146e09c | ||
|
|
33aa4f1952 | ||
|
|
c03f18b90a | ||
|
|
0dedb09a07 | ||
|
|
2b5484243b | ||
|
|
c496db7c95 | ||
|
|
ea4d5ff665 | ||
|
|
468a547864 | ||
|
|
cd9999d192 | ||
|
|
31e302ea59 | ||
|
|
1ff1ba66fd | ||
|
|
a5457d7e22 | ||
|
|
ddcbfd4500 | ||
|
|
293e530297 | ||
|
|
7278ad4ee7 | ||
|
|
0449fb5ef9 | ||
|
|
d2c28fc69c | ||
|
|
60ba0163af | ||
|
|
02ca926d88 | ||
|
|
4b52f31d58 | ||
|
|
9917f2d358 | ||
|
|
8c3ba67583 | ||
|
|
6d8720b404 | ||
|
|
843dd0b1b2 | ||
|
|
70f466d03c | ||
|
|
ef82e8b0d0 | ||
|
|
c643d4cec8 | ||
|
|
718d8b5999 | ||
|
|
2ba0f9157d | ||
|
|
53fdb5273c | ||
|
|
fabdfd5517 | ||
|
|
950993f652 | ||
|
|
5a968b002a | ||
|
|
3acd29fab3 | ||
|
|
315b21db00 | ||
|
|
f9aaeb3a34 | ||
|
|
d19bb909b3 | ||
|
|
f850db23fe | ||
|
|
5f81010f6a | ||
|
|
daf2493f50 | ||
|
|
57222f3611 | ||
|
|
62b185979e | ||
|
|
ebcc85acc4 | ||
|
|
33a7ba4acd | ||
|
|
1d4e6993fc | ||
|
|
784b761629 | ||
|
|
268fb2ce9a | ||
|
|
fc5f35b388 | ||
|
|
ff026a06bb | ||
|
|
b148a57c98 | ||
|
|
ee6e2d2983 | ||
|
|
ea3a6fd75e | ||
|
|
22f85d3af9 | ||
|
|
75f4c2ee99 | ||
|
|
dd3467efa2 | ||
|
|
4adb15c11b | ||
|
|
a5e38d1473 | ||
|
|
778256ca16 | ||
|
|
2b0ba7d1e2 | ||
|
|
772f3fedb3 | ||
|
|
fe25d1dccd | ||
|
|
10a7cd0987 | ||
|
|
6786df6965 | ||
|
|
4cfd18c81a | ||
|
|
d25a21cd32 | ||
|
|
b5f0a6f4a6 | ||
|
|
cf19dd23cf | ||
|
|
3e6a2d670e | ||
|
|
26ef33a4b6 | ||
|
|
9940f1d6db | ||
|
|
75eef8d722 | ||
|
|
46a3c3de33 | ||
|
|
2b7e3f0efe | ||
|
|
d5fbc1d455 | ||
|
|
bbe59499ad | ||
|
|
4c88e9c8d2 | ||
|
|
5ccf5d7150 | ||
|
|
27c9381e1d | ||
|
|
45f8b06d56 | ||
|
|
2a62992c75 | ||
|
|
997afc1b2f | ||
|
|
f941ea6500 | ||
|
|
92d083164f | ||
|
|
2dd30c7a26 | ||
|
|
3f0347253e | ||
|
|
bb6377fb22 | ||
|
|
12c2071358 | ||
|
|
ec4c4a4d5a | ||
|
|
876fcf3296 | ||
|
|
023ceed286 | ||
|
|
cc42aa32ef | ||
|
|
7cbb1c60a2 | ||
|
|
4ad130a11a | ||
|
|
9bf46b6367 | ||
|
|
4be2909b24 | ||
|
|
f161158d83 | ||
|
|
3a5f6ab6f1 | ||
|
|
c1b626da14 | ||
|
|
48e0a3c450 | ||
|
|
8626fa3e00 | ||
|
|
b50d7f0927 | ||
|
|
0d54b57151 | ||
|
|
5a2bdc58da | ||
|
|
01446c02aa | ||
|
|
a382482173 | ||
|
|
2e970cbb39 | ||
|
|
161a3f4da9 | ||
|
|
713bdcbc41 | ||
|
|
1fa67535f9 | ||
|
|
e8d8b67c0a | ||
|
|
e57d4cc544 | ||
|
|
435b7fda7e | ||
|
|
d7e810fc2f | ||
|
|
850ed48955 | ||
|
|
a5ebd89817 | ||
|
|
a8ec07cfc9 | ||
|
|
41fe5373a7 | ||
|
|
0812e189f7 | ||
|
|
588def6d33 | ||
|
|
0c244cbf95 | ||
|
|
7ef14aabed | ||
|
|
978c2b05f2 | ||
|
|
68fd1d67cb | ||
|
|
bf8407274e | ||
|
|
3bc2941445 | ||
|
|
654b1d6b34 | ||
|
|
7a49681dd2 | ||
|
|
7a1623e6a1 | ||
|
|
c25acb41fa | ||
|
|
4224b8a486 | ||
|
|
9e990d7927 | ||
|
|
431ae97593 | ||
|
|
633ff810cf | ||
|
|
f3d2b781ab | ||
|
|
a0b3960ee4 | ||
|
|
e55db0afdc | ||
|
|
ae9efe6359 | ||
|
|
32105665c1 | ||
|
|
2b18efdfdc | ||
|
|
e0c66ea6df | ||
|
|
667c7361d7 | ||
|
|
63fdf0d18e | ||
|
|
e05cb0ef4d | ||
|
|
925c7f7dc7 | ||
|
|
c69e97ea24 | ||
|
|
5e2aebc724 | ||
|
|
6eba467b91 | ||
|
|
524cf5ec5b | ||
|
|
50fd659749 | ||
|
|
8169afb59b | ||
|
|
d40086fea1 | ||
|
|
399c40debd | ||
|
|
d986673dfd | ||
|
|
f83f4d41f1 | ||
|
|
7ed711730e | ||
|
|
94e2ea9df3 | ||
|
|
8c8c4a15c3 | ||
|
|
2a9159f106 | ||
|
|
8f113d17c2 | ||
|
|
9084055b95 | ||
|
|
fba9cce82e | ||
|
|
92cfb46c14 | ||
|
|
449dc1a0e2 | ||
|
|
d9c345b0f3 | ||
|
|
69a639f76c | ||
|
|
d576efe759 | ||
|
|
9ba2ecbc21 | ||
|
|
84003cd67e | ||
|
|
be8c447216 | ||
|
|
e534daf5d4 | ||
|
|
1fefc1af92 | ||
|
|
e76c4ed2a4 | ||
|
|
e1caf13233 | ||
|
|
a7a2fbbca8 | ||
|
|
28d93d9160 | ||
|
|
4e90f90c28 | ||
|
|
2243fdddd3 | ||
|
|
39be3a2ef9 | ||
|
|
ecc30b85bc | ||
|
|
6905b288d2 | ||
|
|
0782146682 | ||
|
|
91aea4f754 | ||
|
|
6ca277a21d | ||
|
|
c47c75aefe | ||
|
|
9896e4381b | ||
|
|
953ffe889e | ||
|
|
72e59e77a7 | ||
|
|
35e2681ea9 | ||
|
|
84012d9090 | ||
|
|
e8a1ea3b54 | ||
|
|
ea6882d9ab | ||
|
|
1fa80e31d1 | ||
|
|
d80752cc9d | ||
|
|
b764e848c7 | ||
|
|
b037c4e8a3 | ||
|
|
6ba2360790 | ||
|
|
ca4eb507f0 | ||
|
|
965b094470 | ||
|
|
0fe313ecfd | ||
|
|
35a2f8d44f | ||
|
|
50797879d5 | ||
|
|
9327331ee9 | ||
|
|
1c15007e32 | ||
|
|
2151ffa114 | ||
|
|
49ed208a54 | ||
|
|
d668462529 | ||
|
|
f2102a0a23 | ||
|
|
5efc6b82c1 | ||
|
|
1e4e9768da | ||
|
|
cc5109c305 | ||
|
|
e858d6a1d5 | ||
|
|
b4cd5d2862 | ||
|
|
0633a44cfb | ||
|
|
5748126b83 | ||
|
|
06375743a3 | ||
|
|
2a41c186aa | ||
|
|
af51b7254c | ||
|
|
f63dfd769f | ||
|
|
a1512f3174 | ||
|
|
245751e2ce | ||
|
|
37001d9425 | ||
|
|
9d1f51c6ba | ||
|
|
cb234fe1fc | ||
|
|
cb85e0255b | ||
|
|
61b4cfdab7 | ||
|
|
d2c405c126 | ||
|
|
cbca560f92 | ||
|
|
2d7b63b4cf | ||
|
|
217038b085 | ||
|
|
13dd4edd6a | ||
|
|
a7288b4fbf | ||
|
|
3020e8104e | ||
|
|
8fdeeaaf38 | ||
|
|
42616b59de | ||
|
|
bf16681bea | ||
|
|
027190b5a4 | ||
|
|
241c02be30 | ||
|
|
dd87268848 | ||
|
|
f2ac24e623 | ||
|
|
80e0cac474 | ||
|
|
37273dd51c | ||
|
|
926a85fff0 | ||
|
|
70273ba2ba | ||
|
|
158cdeed57 | ||
|
|
ba9595a1be | ||
|
|
347e3ff674 | ||
|
|
2b6fb46cdb | ||
|
|
465775bd55 | ||
|
|
44e82fc454 | ||
|
|
c4963d0de8 | ||
|
|
ff81d70cb1 | ||
|
|
d7a543e143 | ||
|
|
cba547083d | ||
|
|
47b1d2a2c2 | ||
|
|
abc378954c | ||
|
|
fdf871af17 | ||
|
|
83fcb0efdc | ||
|
|
0c43f3d15a | ||
|
|
88e087d50f | ||
|
|
a9fb6eb8bc | ||
|
|
08acfdcd24 | ||
|
|
576eb9106f | ||
|
|
ddd2c0ae4e | ||
|
|
e58d7db03b | ||
|
|
1cac42aec5 | ||
|
|
f94449a659 | ||
|
|
df6afc957f | ||
|
|
99ffd3050c | ||
|
|
69dd82d329 | ||
|
|
076f71d490 | ||
|
|
33eae1e03a | ||
|
|
8a20510cde | ||
|
|
c33b470fca | ||
|
|
29db5f1990 | ||
|
|
f98f78a5bd | ||
|
|
d258b42e01 | ||
|
|
a6da32430f | ||
|
|
cfae607310 | ||
|
|
7653e72e88 | ||
|
|
f38b6636e3 | ||
|
|
e42db121ea | ||
|
|
0adceaa3f0 | ||
|
|
e6db1495ab | ||
|
|
e6e494a92c | ||
|
|
549f95b259 | ||
|
|
d92626071e | ||
|
|
a7ac82b023 | ||
|
|
64b78b5822 | ||
|
|
8ba17db877 | ||
|
|
6820d9ae4e | ||
|
|
0bdc2fb05e | ||
|
|
cf5598aeb9 | ||
|
|
8cf3d648ea | ||
|
|
212311a980 | ||
|
|
c9522dc25d | ||
|
|
37af753402 | ||
|
|
d8c5627cf8 | ||
|
|
4f926b37db | ||
|
|
fefc16bd13 | ||
|
|
1b1b71a9b6 | ||
|
|
086532652e | ||
|
|
4e8b4720a1 | ||
|
|
4a7ada28fb | ||
|
|
1710285674 | ||
|
|
a6bb61d998 | ||
|
|
5ec05dfa84 | ||
|
|
83e854aa13 | ||
|
|
634f809159 | ||
|
|
e5cf141834 | ||
|
|
8610b68d3f | ||
|
|
f3e3bddc94 | ||
|
|
7ef3284cc5 | ||
|
|
3494586f77 | ||
|
|
faaf99e6bb | ||
|
|
1078ba2111 | ||
|
|
2ad69300f5 | ||
|
|
d2f3fa7fdf | ||
|
|
64fcb6270b | ||
|
|
562c30cff4 | ||
|
|
7108501d24 | ||
|
|
37eae3406c | ||
|
|
501dc938e6 | ||
|
|
c5ecd35fe9 | ||
|
|
7cd8d7f44d | ||
|
|
567a9a4e58 | ||
|
|
58f4a0cfbb | ||
|
|
e6c0b697aa | ||
|
|
35f60d699d | ||
|
|
c219be0970 | ||
|
|
c72ce843fa | ||
|
|
c606059a3a | ||
|
|
049a8bdc6d | ||
|
|
9752f744ca | ||
|
|
4be6fb789c | ||
|
|
afc56e5259 | ||
|
|
d47f8521d5 | ||
|
|
7f853d426a | ||
|
|
e9008c615d | ||
|
|
01f081ef5a | ||
|
|
7ee174e0d5 | ||
|
|
24439f86e0 | ||
|
|
fbd3ce3b72 | ||
|
|
96f8b54b51 | ||
|
|
9c94a78e29 | ||
|
|
a14e3dd137 | ||
|
|
e37673bd67 | ||
|
|
6aa10d20a1 | ||
|
|
68a92acb7a | ||
|
|
8aa7cc9ca5 | ||
|
|
e6c087c3bb | ||
|
|
39a2097152 | ||
|
|
6a8003917e | ||
|
|
d5a17ddc8c | ||
|
|
48bbf0d649 | ||
|
|
0bc58c254f | ||
|
|
b2d41f0583 | ||
|
|
0d31d20f0f | ||
|
|
5154e31c1c | ||
|
|
c67b5e950e | ||
|
|
8a7b5cc87d | ||
|
|
bb7938f66d | ||
|
|
5b22e945da | ||
|
|
decde230aa | ||
|
|
1dec8ae122 | ||
|
|
8512d5e693 | ||
|
|
bb481ccfb4 | ||
|
|
12bce48ef5 | ||
|
|
013c7c776e | ||
|
|
8f96d20a23 | ||
|
|
1a8811b69a | ||
|
|
d796849d74 | ||
|
|
942bd0859f | ||
|
|
072028c740 | ||
|
|
0d08aecd56 | ||
|
|
66b290577c | ||
|
|
22ad16e11b | ||
|
|
2f49a08c7d | ||
|
|
fcacda74cb | ||
|
|
fa0c90de70 | ||
|
|
c1197314ac | ||
|
|
0b31792660 | ||
|
|
8b95dd65d9 | ||
|
|
691ed88096 | ||
|
|
836d772cd4 | ||
|
|
999ada03d1 | ||
|
|
b35fabbe55 | ||
|
|
8cd8a157a6 | ||
|
|
86aece6828 | ||
|
|
f9edadbafd | ||
|
|
6a388cd4fe | ||
|
|
9d17e9ff48 | ||
|
|
662b7d01b8 | ||
|
|
a19bc4b4e4 | ||
|
|
a545aa5c39 | ||
|
|
fa451f362b | ||
|
|
868659a2f1 | ||
|
|
8ae62da138 | ||
|
|
bedba39af9 | ||
|
|
8493e56b11 | ||
|
|
21c77dccce | ||
|
|
55164803b0 | ||
|
|
c163f84aec | ||
|
|
2711b989e1 | ||
|
|
5c49a8ce6a | ||
|
|
854f308eae | ||
|
|
16ba6b53ba | ||
|
|
0af29a378a | ||
|
|
def34a860b | ||
|
|
f8034e1b78 | ||
|
|
01fbea02f1 | ||
|
|
3d9af89e24 | ||
|
|
d430d9f3ed | ||
|
|
0c24a1e626 | ||
|
|
1099dbe642 | ||
|
|
2df3277dcd | ||
|
|
6ae14213f5 | ||
|
|
61bd029303 | ||
|
|
5b09bd8242 | ||
|
|
703477b157 | ||
|
|
03ff5d8ae1 | ||
|
|
220f7ef7cd | ||
|
|
682a99dd43 | ||
|
|
fac5de582d | ||
|
|
7cbf9de8ca | ||
|
|
ce213c3d89 | ||
|
|
32cd0360e6 | ||
|
|
1ec23a5699 | ||
|
|
48330f6432 | ||
|
|
28358debbc | ||
|
|
54b7ed6117 | ||
|
|
0cfd2ee63b | ||
|
|
37a0990741 | ||
|
|
7a0cd1eb34 | ||
|
|
ac3277da09 | ||
|
|
65d1e7be56 | ||
|
|
80685afa7e | ||
|
|
f892453892 | ||
|
|
422bb8c31c | ||
|
|
6fb1202c1c | ||
|
|
4ddd2788f0 | ||
|
|
8a28029809 | ||
|
|
423a2129d1 | ||
|
|
a338097514 | ||
|
|
84b67abb03 | ||
|
|
5ec8406653 | ||
|
|
b3ce300d32 | ||
|
|
3f93b93d9e | ||
|
|
e32c83db63 | ||
|
|
0344a63b48 | ||
|
|
24923c0009 | ||
|
|
a9036c9738 | ||
|
|
f9f7fbed33 | ||
|
|
53b5bee736 | ||
|
|
d0b3726905 | ||
|
|
7a6864507e | ||
|
|
e20563f2e1 | ||
|
|
fea5f8f3d4 | ||
|
|
f9bb529b85 | ||
|
|
60e348fcc1 | ||
|
|
f194c5be0e | ||
|
|
47712e63f1 | ||
|
|
790c1fb34a | ||
|
|
9cca731acc | ||
|
|
48f232790a | ||
|
|
3c55aa5f43 | ||
|
|
8c1edb30a6 | ||
|
|
5e64af4448 | ||
|
|
9f60017cfe | ||
|
|
b6a86d11d2 | ||
|
|
db86bfd63d | ||
|
|
7ff72a8920 | ||
|
|
2c4f86d148 | ||
|
|
1a9f26e804 | ||
|
|
42f8194bde | ||
|
|
8634b7058c | ||
|
|
fc276b330a | ||
|
|
5b22d7430a | ||
|
|
8883debc74 | ||
|
|
c92cb08f6f | ||
|
|
1254b668de | ||
|
|
48b703bf9f | ||
|
|
064679c057 | ||
|
|
ba23d258e7 | ||
|
|
98cd19d440 | ||
|
|
4c8b91e9d9 | ||
|
|
ba742563c2 | ||
|
|
f0e70ed27b | ||
|
|
acc4bdbc5e | ||
|
|
c45c82306e | ||
|
|
fd827b2214 | ||
|
|
df1c157994 | ||
|
|
a92e417581 | ||
|
|
6ad0719880 | ||
|
|
5383d0b5f7 | ||
|
|
b3cefc075d | ||
|
|
ac62d18007 | ||
|
|
fe14c26782 | ||
|
|
b33a3cabf9 | ||
|
|
6224163ecd | ||
|
|
05aabb2843 | ||
|
|
7d2d5f6bf4 | ||
|
|
c938685679 | ||
|
|
e6ecc28001 | ||
|
|
93fa6ba466 | ||
|
|
a8f459e4fa | ||
|
|
2441bb1cec | ||
|
|
25cc24fca5 | ||
|
|
ff4cbc6d5f | ||
|
|
f79bfae95d | ||
|
|
2f99efcc60 | ||
|
|
45b13571a5 | ||
|
|
04da8812df | ||
|
|
840304ee04 | ||
|
|
41bd9a9358 | ||
|
|
1e0a9918fd | ||
|
|
799acf5db8 | ||
|
|
1326d29fad | ||
|
|
9b35530956 | ||
|
|
0ae054c5d7 | ||
|
|
c72eac9987 | ||
|
|
159ccd807f | ||
|
|
5d13faef33 | ||
|
|
e0de59a4b6 | ||
|
|
519a1b0eaf | ||
|
|
4d8e1b7cef | ||
|
|
6d3e096e08 | ||
|
|
38edcdca4b | ||
|
|
8774e6be71 | ||
|
|
ec197b2e13 | ||
|
|
1c0d6e9c67 | ||
|
|
7d711da381 | ||
|
|
f66cea9829 | ||
|
|
5f572face5 | ||
|
|
88a4cf9f12 | ||
|
|
0b860e0d40 | ||
|
|
149bb3e5b2 | ||
|
|
7a7a779824 | ||
|
|
20a3657063 | ||
|
|
9c87c3a095 | ||
|
|
4de65b4369 | ||
|
|
996c78d760 | ||
|
|
ccdc3d60c4 | ||
|
|
8be08882d8 | ||
|
|
26d2c5a8f0 | ||
|
|
bae39e3a2d | ||
|
|
bb1a72269a | ||
|
|
9674cfd258 | ||
|
|
627ddd2f70 | ||
|
|
27b3a44147 | ||
|
|
5308fd8b46 | ||
|
|
1b914d5d4f | ||
|
|
9e0f17f7c6 | ||
|
|
1320b6d785 | ||
|
|
f1ddbeadaf | ||
|
|
f9f89e1e51 | ||
|
|
bbf214fa4c | ||
|
|
f1582177e1 | ||
|
|
d5712a564c | ||
|
|
1c274862d8 | ||
|
|
663c9e0fa9 | ||
|
|
bcb0bc75c9 | ||
|
|
603823d6ea | ||
|
|
20c04d3ed3 | ||
|
|
02e5d608d0 | ||
|
|
e53ac6566b | ||
|
|
2472b86284 | ||
|
|
29a15858f4 | ||
|
|
afc16358ca | ||
|
|
9facf77ff1 | ||
|
|
1923854202 | ||
|
|
9cd92c7b7f | ||
|
|
8e0b723207 | ||
|
|
68ef3a07a7 | ||
|
|
202ceb02b5 | ||
|
|
59370cae81 | ||
|
|
52a3bc224a | ||
|
|
54d67e5216 | ||
|
|
b55d8250cc | ||
|
|
3a1e9abd68 | ||
|
|
c5ba40a178 | ||
|
|
f0c6dccadb | ||
|
|
e701d1ab6a | ||
|
|
e10c8093c9 | ||
|
|
e81b3461b2 | ||
|
|
9345cb3934 | ||
|
|
eb36a0b3dd | ||
|
|
7e442ecb3d | ||
|
|
f07c5eb725 | ||
|
|
a486be92cb | ||
|
|
4d84060036 | ||
|
|
fc503691fe | ||
|
|
c80dd43a3e | ||
|
|
a4a62e0c18 | ||
|
|
2f98cb9b6d | ||
|
|
91dc6eebb0 | ||
|
|
d72e0a4418 | ||
|
|
2c8ebd43cc | ||
|
|
9f561aa296 | ||
|
|
930bacd45d | ||
|
|
ef2d736b20 | ||
|
|
f3a453be20 | ||
|
|
45c97a778d | ||
|
|
6ebc64f73b | ||
|
|
52807d0d49 | ||
|
|
a5e18e99bc | ||
|
|
f545b3e745 | ||
|
|
e0877803e3 | ||
|
|
4916887c8d | ||
|
|
20eb573897 | ||
|
|
8ff7b6b6e6 | ||
|
|
06eaee8909 | ||
|
|
8f9487ba70 | ||
|
|
eca51457b7 | ||
|
|
15c6fce648 | ||
|
|
6c872263c6 | ||
|
|
4d3b3d1740 | ||
|
|
bba8920855 | ||
|
|
f56b9487ff | ||
|
|
1946d8296b | ||
|
|
41e5d7f820 | ||
|
|
2507568103 | ||
|
|
19733798fa | ||
|
|
427d6da360 | ||
|
|
2b67d3d1c5 | ||
|
|
6926a40ad6 | ||
|
|
7a8da5bf3a | ||
|
|
fc8fa17c6f | ||
|
|
0a88659a9f | ||
|
|
9967858c44 | ||
|
|
e2ce388f90 | ||
|
|
f31649f1d2 | ||
|
|
a55c167dde | ||
|
|
642cf232ba | ||
|
|
164b4525c4 | ||
|
|
39c26d2bee | ||
|
|
2a69955cc1 | ||
|
|
4a5345dd5d | ||
|
|
1e6dd0e3e0 | ||
|
|
91cca2e358 | ||
|
|
816a9be618 | ||
|
|
9eb0ec76fe | ||
|
|
49054d5239 | ||
|
|
787c4e45a8 | ||
|
|
34cb7a4d02 | ||
|
|
006241163b | ||
|
|
03818fadee | ||
|
|
897c3ea625 | ||
|
|
73e4293f04 | ||
|
|
6f5ffcb1f8 | ||
|
|
ed70f3af83 | ||
|
|
73196f9be8 | ||
|
|
a77f4e9d77 | ||
|
|
294490f814 | ||
|
|
6183001fca | ||
|
|
3ac604c665 | ||
|
|
e342b07cd0 | ||
|
|
b524cbd1b3 | ||
|
|
88693d73bd | ||
|
|
2c453a34ee | ||
|
|
3d2b2e43b1 | ||
|
|
c3f3fca896 | ||
|
|
dedf6e5d4b | ||
|
|
6c379fc3a7 | ||
|
|
329e9c9eb2 | ||
|
|
ee53086444 | ||
|
|
43d6c6678f | ||
|
|
82f136ba79 | ||
|
|
e40d3dd64d | ||
|
|
a5897fd64b | ||
|
|
e786e3c057 | ||
|
|
d347645475 | ||
|
|
215b78c162 | ||
|
|
ee271519f9 | ||
|
|
b350277bbc | ||
|
|
604ae080ac | ||
|
|
a191dab359 | ||
|
|
3223011b13 | ||
|
|
f746e246e4 | ||
|
|
0476b68585 | ||
|
|
ec395bed72 | ||
|
|
bff56220c2 | ||
|
|
3006405a52 | ||
|
|
9cd0ac80b1 | ||
|
|
da51d38ba2 | ||
|
|
5ba6459069 | ||
|
|
75899242fd | ||
|
|
7faf42d892 | ||
|
|
10f5f331d7 | ||
|
|
b1414388e1 | ||
|
|
eb0f5b2e1b | ||
|
|
7af02ad2e2 | ||
|
|
8330dabc46 | ||
|
|
dbc7ad0b3b | ||
|
|
c0fd24770e | ||
|
|
4289fe4990 | ||
|
|
e925e9b23f | ||
|
|
71cd86fdd5 | ||
|
|
03be947ad6 | ||
|
|
96f9084f2e | ||
|
|
bbccfcbd12 | ||
|
|
9a697f48db | ||
|
|
37ad1cced2 | ||
|
|
26db20f63d | ||
|
|
ff788e3591 | ||
|
|
4b482488de | ||
|
|
e230b6640f | ||
|
|
2bc949fae3 | ||
|
|
b1bc472205 | ||
|
|
5c7a38c292 | ||
|
|
bbd6c51eb6 | ||
|
|
d17f9b0687 | ||
|
|
4d2bdb6eee | ||
|
|
b6a1014c72 | ||
|
|
b99885c806 | ||
|
|
f422c9b820 | ||
|
|
0befe91360 | ||
|
|
da671e3fd5 | ||
|
|
fec94c18aa | ||
|
|
11c6fc7d90 | ||
|
|
7ea5e7dc95 | ||
|
|
2a98e2c361 | ||
|
|
7fb499b301 | ||
|
|
af9aee76cf | ||
|
|
075ec15f02 | ||
|
|
1c650473f8 | ||
|
|
0efdf50821 | ||
|
|
df65ef2191 | ||
|
|
bc3b1d9565 | ||
|
|
2998d3ba6a | ||
|
|
ea11153032 | ||
|
|
733f61075f | ||
|
|
618e69775c | ||
|
|
eabfa90121 | ||
|
|
43b7ccd61a | ||
|
|
b6875a44cf | ||
|
|
c0004dd532 | ||
|
|
0ee3b89760 | ||
|
|
c5e60d30e1 | ||
|
|
acaf1ac196 | ||
|
|
8dc4538c95 | ||
|
|
e224fd2595 | ||
|
|
f0a1ea4d6d | ||
|
|
10cb8ebf3b | ||
|
|
8c4afa1866 | ||
|
|
eb5af47bbf | ||
|
|
4fd93ce64c | ||
|
|
7ba4e9e66d | ||
|
|
e2e5449d25 | ||
|
|
abc76ca155 | ||
|
|
0fc84a8684 | ||
|
|
a76600e53b | ||
|
|
e55cf30705 | ||
|
|
2c65b8fd2b | ||
|
|
20b8e35132 | ||
|
|
8007225a41 | ||
|
|
63a6da6680 | ||
|
|
31c8cb476a | ||
|
|
79bd6a25d9 | ||
|
|
0042604e6d | ||
|
|
54f2bb1092 | ||
|
|
6b6df619f5 |
@@ -6,5 +6,5 @@ module.exports.config = {
|
||||
MetadataPath: Path.resolve('metadata'),
|
||||
FFmpegPath: '/usr/bin/ffmpeg',
|
||||
FFProbePath: '/usr/bin/ffprobe',
|
||||
SkipBinariesCheck: true
|
||||
}
|
||||
SkipBinariesCheck: false
|
||||
}
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -3,6 +3,3 @@ contact_links:
|
||||
- name: Discord
|
||||
url: https://discord.gg/HQgCbd6E75
|
||||
about: Ask questions, get help troubleshooting, and join the Abs community here.
|
||||
- name: Matrix
|
||||
url: https://matrix.to/#/#audiobookshelf:matrix.org
|
||||
about: Ask questions, get help troubleshooting, and join the Abs community here.
|
||||
|
||||
33
.github/pull_request_template.md
vendored
Normal file
33
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
<!--
|
||||
For Work In Progress Pull Requests, please use the Draft PR feature,
|
||||
see https://github.blog/2019-02-14-introducing-draft-pull-requests/ for further details.
|
||||
|
||||
If you do not follow this template, the PR may be closed without review.
|
||||
|
||||
Please ensure all checks pass.
|
||||
If you are a new contributor, the workflows will need to be manually approved before they run.
|
||||
-->
|
||||
|
||||
## Brief summary
|
||||
|
||||
<!-- Please provide a brief summary of what your PR attempts to achieve. -->
|
||||
|
||||
## Which issue is fixed?
|
||||
|
||||
<!-- Which issue number does this PR fix? Ex: "Fixes #1234" -->
|
||||
|
||||
## In-depth Description
|
||||
|
||||
<!--
|
||||
Describe your solution in more depth.
|
||||
How does it work? Why is this the best solution?
|
||||
Does it solve a problem that affects multiple users or is this an edge case for your setup?
|
||||
-->
|
||||
|
||||
## How have you tested this?
|
||||
|
||||
<!-- Please describe in detail with reproducible steps how you tested your changes. -->
|
||||
|
||||
## Screenshots
|
||||
|
||||
<!-- If your PR includes any changes to the web client, please include screenshots or a short video from before and after your changes. -->
|
||||
55
.github/workflows/apply_comments.yaml
vendored
Normal file
55
.github/workflows/apply_comments.yaml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: Add issue comments by label
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- labeled
|
||||
jobs:
|
||||
help-wanted:
|
||||
if: github.event.label.name == 'help wanted'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Help wanted comment
|
||||
run: gh issue comment "$NUMBER" --body "$BODY"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
NUMBER: ${{ github.event.issue.number }}
|
||||
BODY: >
|
||||
This issue is not able to be completed due to limited bandwidth or access to the required test hardware.
|
||||
|
||||
This issue is available for anyone to work on.
|
||||
|
||||
|
||||
config-issue:
|
||||
if: github.event.label.name == 'config-issue'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Config issue comment
|
||||
run: gh issue close "$NUMBER" --reason "not planned" --comment "$BODY"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
NUMBER: ${{ github.event.issue.number }}
|
||||
BODY: >
|
||||
After reviewing this issue, this appears to be a problem with your setup and not Audiobookshelf. This issue is being closed to keep the issue tracker focused on Audiobookshelf itself. Please reach out on the Audiobookshelf Discord for community support.
|
||||
|
||||
Some common search terms to help you find the solution to your problem:
|
||||
- Reverse proxy
|
||||
- Enabling websockets
|
||||
- SSL (https vs http)
|
||||
- Configuring a static IP
|
||||
- `localhost` versus IP address
|
||||
- hairpin NAT
|
||||
- VPN
|
||||
- firewall ports
|
||||
- public versus private network
|
||||
- bridge versus host mode
|
||||
- Docker networking
|
||||
- DNS (such as EAI_AGAIN errors)
|
||||
|
||||
After you have followed these steps, please post the solution or steps you followed to fix the problem to help others in the future, or show that it is a problem with Audiobookshelf so we can reopen the issue.
|
||||
|
||||
20
.github/workflows/close-issues-on-release.yml
vendored
Normal file
20
.github/workflows/close-issues-on-release.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Close fixed issues on release.
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Close issues marked as fixed upon a release.
|
||||
uses: gcampbell-msft/fixed-pending-release@7fa1b75a0c04bcd4b375110522878e5f6100cff5
|
||||
with:
|
||||
label: 'awaiting release'
|
||||
removeLabel: true
|
||||
applyToAll: true
|
||||
message: Fixed in [${releaseTag}](${releaseUrl}).
|
||||
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"
|
||||
});
|
||||
}
|
||||
77
.github/workflows/codeql.yml
vendored
77
.github/workflows/codeql.yml
vendored
@@ -1,11 +1,25 @@
|
||||
name: "CodeQL"
|
||||
name: 'CodeQL'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'master' ]
|
||||
branches: ['master']
|
||||
# Only build when files in these directories have been changed
|
||||
paths:
|
||||
- client/**
|
||||
- server/**
|
||||
- test/**
|
||||
- index.js
|
||||
- package.json
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ 'master' ]
|
||||
branches: ['master']
|
||||
# Only build when files in these directories have been changed
|
||||
paths:
|
||||
- client/**
|
||||
- server/**
|
||||
- test/**
|
||||
- index.js
|
||||
- package.json
|
||||
schedule:
|
||||
- cron: '16 5 * * 4'
|
||||
|
||||
@@ -21,45 +35,44 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
language: ['javascript']
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Use only 'java' to analyze code written in Java, Kotlin or both
|
||||
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
1
.github/workflows/docker-build.yml
vendored
1
.github/workflows/docker-build.yml
vendored
@@ -70,6 +70,7 @@ jobs:
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
tags: ${{ github.event.inputs.tags || steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
|
||||
7
.github/workflows/integration-test.yml
vendored
7
.github/workflows/integration-test.yml
vendored
@@ -5,6 +5,13 @@ on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'dependabot/**' # Don't run dependabot branches, as they are already covered by pull requests
|
||||
# Only build when files in these directories have been changed
|
||||
paths:
|
||||
- client/**
|
||||
- server/**
|
||||
- test/**
|
||||
- index.js
|
||||
- package.json
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
12
.github/workflows/lint-openapi.yml
vendored
12
.github/workflows/lint-openapi.yml
vendored
@@ -1,13 +1,15 @@
|
||||
name: API linting
|
||||
|
||||
# Run on pull requests or pushes when there is a change to the OpenAPI file
|
||||
# Run on pull requests or pushes when there is a change to any OpenAPI files in docs/
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
paths:
|
||||
- docs/
|
||||
pull_request:
|
||||
paths:
|
||||
- docs/
|
||||
- 'docs/**'
|
||||
|
||||
# This action only needs read permissions
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -7,6 +7,7 @@
|
||||
/podcasts/
|
||||
/media/
|
||||
/metadata/
|
||||
/plugins/
|
||||
/client/.nuxt/
|
||||
/client/dist/
|
||||
/dist/
|
||||
@@ -15,8 +16,10 @@
|
||||
/.nyc_output/
|
||||
/ffmpeg*
|
||||
/ffprobe*
|
||||
/unicode*
|
||||
/libnusqlite3*
|
||||
|
||||
sw.*
|
||||
.DS_STORE
|
||||
.idea/*
|
||||
tailwind.compiled.css
|
||||
tailwind.compiled.css
|
||||
|
||||
37
Dockerfile
37
Dockerfile
@@ -11,24 +11,45 @@ FROM node:20-alpine
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache --update \
|
||||
curl \
|
||||
tzdata \
|
||||
ffmpeg \
|
||||
make \
|
||||
python3 \
|
||||
g++ \
|
||||
tini
|
||||
apk add --no-cache --update \
|
||||
curl \
|
||||
tzdata \
|
||||
ffmpeg \
|
||||
make \
|
||||
python3 \
|
||||
g++ \
|
||||
tini \
|
||||
unzip
|
||||
|
||||
COPY --from=build /client/dist /client/dist
|
||||
COPY index.js package* /
|
||||
COPY server server
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
ENV NUSQLITE3_DIR="/usr/local/lib/nusqlite3"
|
||||
ENV NUSQLITE3_PATH="${NUSQLITE3_DIR}/libnusqlite3.so"
|
||||
|
||||
RUN case "$TARGETPLATFORM" in \
|
||||
"linux/amd64") \
|
||||
curl -L -o /tmp/library.zip "https://github.com/mikiher/nunicode-sqlite/releases/download/v1.2/libnusqlite3-linux-musl-x64.zip" ;; \
|
||||
"linux/arm64") \
|
||||
curl -L -o /tmp/library.zip "https://github.com/mikiher/nunicode-sqlite/releases/download/v1.2/libnusqlite3-linux-musl-arm64.zip" ;; \
|
||||
*) echo "Unsupported platform: $TARGETPLATFORM" && exit 1 ;; \
|
||||
esac && \
|
||||
unzip /tmp/library.zip -d $NUSQLITE3_DIR && \
|
||||
rm /tmp/library.zip
|
||||
|
||||
RUN npm ci --only=production
|
||||
|
||||
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"]
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
FFMPEG_INSTALL_DIR="/usr/lib/audiobookshelf-ffmpeg"
|
||||
DEFAULT_DATA_DIR="/usr/share/audiobookshelf"
|
||||
CONFIG_PATH="/etc/default/audiobookshelf"
|
||||
DEFAULT_PORT=13378
|
||||
@@ -46,25 +45,6 @@ add_group() {
|
||||
fi
|
||||
}
|
||||
|
||||
install_ffmpeg() {
|
||||
echo "Starting FFMPEG Install"
|
||||
|
||||
WGET="wget https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz --output-document=ffmpeg-git-amd64-static.tar.xz"
|
||||
|
||||
if ! cd "$FFMPEG_INSTALL_DIR"; then
|
||||
echo "Creating ffmpeg install dir at $FFMPEG_INSTALL_DIR"
|
||||
mkdir "$FFMPEG_INSTALL_DIR"
|
||||
chown -R 'audiobookshelf:audiobookshelf' "$FFMPEG_INSTALL_DIR"
|
||||
cd "$FFMPEG_INSTALL_DIR"
|
||||
fi
|
||||
|
||||
$WGET
|
||||
tar xvf ffmpeg-git-amd64-static.tar.xz --strip-components=1 --no-same-owner
|
||||
rm ffmpeg-git-amd64-static.tar.xz
|
||||
|
||||
echo "Good to go on Ffmpeg... hopefully"
|
||||
}
|
||||
|
||||
setup_config() {
|
||||
if [ -f "$CONFIG_PATH" ]; then
|
||||
echo "Existing config found."
|
||||
@@ -83,8 +63,6 @@ setup_config() {
|
||||
|
||||
config_text="METADATA_PATH=$DEFAULT_DATA_DIR/metadata
|
||||
CONFIG_PATH=$DEFAULT_DATA_DIR/config
|
||||
FFMPEG_PATH=$FFMPEG_INSTALL_DIR/ffmpeg
|
||||
FFPROBE_PATH=$FFMPEG_INSTALL_DIR/ffprobe
|
||||
PORT=$DEFAULT_PORT
|
||||
HOST=$DEFAULT_HOST"
|
||||
|
||||
@@ -101,5 +79,3 @@ add_group 'audiobookshelf' ''
|
||||
add_user 'audiobookshelf' '' 'audiobookshelf' 'audiobookshelf user-daemon' '/bin/false'
|
||||
|
||||
setup_config
|
||||
|
||||
install_ffmpeg
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-family: 'Material Symbols Rounded';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(~static/fonts/MaterialIcons.woff2) format('woff2');
|
||||
src: url(~static/fonts/MaterialSymbolsRounded.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Material Icons Outlined';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(~static/fonts/MaterialIconsOutlined.woff2) format('woff2');
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-family: 'Material Icons';
|
||||
.material-symbols {
|
||||
font-family: 'Material Symbols Rounded';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
line-height: 1;
|
||||
@@ -27,26 +20,9 @@
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.material-icons:not([class*="text-"]) {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.material-icons-outlined {
|
||||
font-family: 'Material Icons Outlined';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.material-icons-outlined:not([class*="text-"]) {
|
||||
font-size: 1.5rem;
|
||||
.material-symbols.fill {
|
||||
font-variation-settings:
|
||||
'FILL' 1
|
||||
}
|
||||
|
||||
/* cyrillic-ext */
|
||||
@@ -317,4 +293,4 @@
|
||||
font-display: swap;
|
||||
src: url(~static/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="flex-grow" />
|
||||
|
||||
<ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
|
||||
<span class="material-icons-outlined text-2xl text-warning text-opacity-50"> cast </span>
|
||||
<span class="material-symbols text-2xl text-warning text-opacity-50"> cast </span>
|
||||
</ui-tooltip>
|
||||
<div v-if="isChromecastInitialized" class="w-6 min-w-6 h-6 ml-2 mr-1 sm:mx-2 cursor-pointer">
|
||||
<google-cast-launcher></google-cast-launcher>
|
||||
@@ -26,19 +26,19 @@
|
||||
|
||||
<nuxt-link v-if="currentLibrary" to="/config/stats" class="hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
|
||||
<ui-tooltip :text="$strings.HeaderYourStats" direction="bottom" class="flex items-center">
|
||||
<span class="material-icons text-2xl" aria-label="User Stats" role="button">equalizer</span>
|
||||
<span class="material-symbols text-2xl" aria-label="User Stats" role="button"></span>
|
||||
</ui-tooltip>
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="userCanUpload && currentLibrary" to="/upload" class="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
||||
<ui-tooltip :text="$strings.ButtonUpload" direction="bottom" class="flex items-center">
|
||||
<span class="material-icons text-2xl" aria-label="Upload Media" role="button">upload</span>
|
||||
<span class="material-symbols text-2xl" aria-label="Upload Media" role="button"></span>
|
||||
</ui-tooltip>
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="userIsAdminOrUp" to="/config" class="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
||||
<ui-tooltip :text="$strings.HeaderSettings" direction="bottom" class="flex items-center">
|
||||
<span class="material-icons text-2xl" aria-label="System Settings" role="button">settings</span>
|
||||
<span class="material-symbols text-2xl" aria-label="System Settings" role="button"></span>
|
||||
</ui-tooltip>
|
||||
</nuxt-link>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<span class="block truncate">{{ username }}</span>
|
||||
</span>
|
||||
<span class="h-full md:ml-3 md:absolute inset-y-0 md:right-0 flex items-center justify-center md:pr-2 pointer-events-none">
|
||||
<span class="material-icons text-xl text-gray-100">person</span>
|
||||
<span class="material-symbols text-xl text-gray-100"></span>
|
||||
</span>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
@@ -55,7 +55,7 @@
|
||||
<h1 class="text-lg md:text-2xl px-4">{{ $getString('MessageItemsSelected', [numMediaItemsSelected]) }}</h1>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn v-if="!isPodcastLibrary && selectedMediaItemsArePlayable" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playSelectedItems">
|
||||
<span class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||
<span class="material-symbols fill text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||
{{ $strings.ButtonPlay }}
|
||||
</ui-btn>
|
||||
<ui-tooltip v-if="isBookLibrary" :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
||||
@@ -76,7 +76,7 @@
|
||||
<ui-context-menu-dropdown v-if="contextMenuItems.length && !processingBatch" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" />
|
||||
|
||||
<ui-tooltip :text="$strings.LabelDeselectAll" direction="bottom" class="flex items-center">
|
||||
<span class="material-icons text-3xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatch ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
|
||||
<span class="material-symbols text-3xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatch ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -170,13 +170,13 @@ export default {
|
||||
|
||||
if (!this.isPodcastLibrary && this.selectedMediaItemsArePlayable) {
|
||||
options.push({
|
||||
text: 'Quick Embed Metadata',
|
||||
text: this.$strings.ButtonQuickEmbedMetadata,
|
||||
action: 'quick-embed'
|
||||
})
|
||||
}
|
||||
|
||||
options.push({
|
||||
text: 'Re-Scan',
|
||||
text: this.$strings.ButtonReScan,
|
||||
action: 'rescan'
|
||||
})
|
||||
|
||||
@@ -264,7 +264,6 @@ export default {
|
||||
libraryItems.forEach((item) => {
|
||||
let subtitle = ''
|
||||
if (item.mediaType === 'book') subtitle = item.media.metadata.authors.map((au) => au.name).join(', ')
|
||||
else if (item.mediaType === 'music') subtitle = item.media.metadata.artists.join(', ')
|
||||
queueItems.push({
|
||||
libraryItemId: item.id,
|
||||
libraryId: item.libraryId,
|
||||
@@ -332,13 +331,13 @@ export default {
|
||||
libraryItemIds: this.selectedMediaItems.map((i) => i.id)
|
||||
})
|
||||
.then(() => {
|
||||
this.$toast.success('Batch delete success')
|
||||
this.$toast.success(this.$strings.ToastBatchDeleteSuccess)
|
||||
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Batch delete failed', error)
|
||||
this.$toast.error('Batch delete failed')
|
||||
this.$toast.error(this.$strings.ToastBatchDeleteFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.$store.commit('setProcessingBatch', false)
|
||||
|
||||
@@ -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>
|
||||
@@ -167,8 +167,19 @@ export default {
|
||||
this.loaded = true
|
||||
},
|
||||
async fetchCategories() {
|
||||
// Sets the limit for the number of items to be displayed based on the viewport width.
|
||||
const viewportWidth = window.innerWidth
|
||||
let limit
|
||||
if (viewportWidth >= 3240) {
|
||||
limit = 15
|
||||
} else if (viewportWidth >= 2880 && viewportWidth < 3240) {
|
||||
limit = 12
|
||||
}
|
||||
|
||||
const limitQuery = limit ? `&limit=${limit}` : ''
|
||||
|
||||
const categories = await this.$axios
|
||||
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete,share`)
|
||||
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete,share${limitQuery}`)
|
||||
.then((data) => {
|
||||
return data
|
||||
})
|
||||
@@ -336,6 +347,13 @@ export default {
|
||||
libraryItemsAdded(libraryItems) {
|
||||
console.log('libraryItems added', libraryItems)
|
||||
|
||||
// First items added to library
|
||||
const isThisLibrary = libraryItems.some((li) => li.libraryId === this.currentLibraryId)
|
||||
if (!this.shelves.length && !this.search && isThisLibrary) {
|
||||
this.fetchCategories()
|
||||
return
|
||||
}
|
||||
|
||||
const recentlyAddedShelf = this.shelves.find((shelf) => shelf.id === 'recently-added')
|
||||
if (!recentlyAddedShelf) return
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
<div v-if="shelf.type === 'authors'" class="flex items-center">
|
||||
<template v-for="entity in shelf.entities">
|
||||
<cards-author-card :key="entity.id" :author="entity" @hook:updated="updatedBookCard" class="mx-2e" @edit="editAuthor" />
|
||||
<cards-author-card :key="entity.id" :authorMount="entity" @hook:updated="updatedBookCard" class="mx-2e" @edit="editAuthor" />
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="shelf.type === 'narrators'" class="flex items-center">
|
||||
@@ -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">
|
||||
<span class="material-icons 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">
|
||||
<span class="material-icons text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_right</span>
|
||||
</div>
|
||||
<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>
|
||||
</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>
|
||||
</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)
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="flex-grow h-full flex justify-center items-center" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<p v-if="isPlaylistsPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonPlaylists }}</p>
|
||||
<span v-else class="material-icons-outlined text-lg">queue_music</span>
|
||||
<span v-else class="material-symbols text-lg"></span>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
|
||||
<span v-else class="material-icons-outlined text-lg">collections_bookmark</span>
|
||||
<span v-else class="material-symbols text-lg"></span>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
|
||||
<svg v-else class="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -42,18 +42,20 @@
|
||||
<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">
|
||||
{{ seriesName }}
|
||||
</p>
|
||||
<div class="w-6 h-6 rounded-full bg-black bg-opacity-30 flex items-center justify-center ml-3">
|
||||
<span class="font-mono">{{ numShowing }}</span>
|
||||
<span class="font-mono">{{ $formatNumber(numShowing) }}</span>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<ui-checkbox v-if="!isBatchSelecting" v-model="settings.collapseBookSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseBookSeries" />
|
||||
|
||||
<!-- RSS feed -->
|
||||
<ui-tooltip v-if="seriesRssFeed" :text="$strings.LabelOpenRSSFeed" direction="top">
|
||||
@@ -63,14 +65,11 @@
|
||||
<ui-context-menu-dropdown v-if="!isBatchSelecting && seriesContextMenuItems.length" :items="seriesContextMenuItems" class="mx-px" @action="seriesContextMenuAction" />
|
||||
</template>
|
||||
<!-- library & collections page -->
|
||||
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
|
||||
<p class="hidden md:block">{{ numShowing }} {{ entityName }}</p>
|
||||
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome && !isAuthorsPage">
|
||||
<p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p>
|
||||
|
||||
<div class="flex-grow hidden sm:inline-block" />
|
||||
|
||||
<!-- collapse series checkbox -->
|
||||
<ui-checkbox v-if="isLibraryPage && isBookLibrary && !isBatchSelecting" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
|
||||
|
||||
<!-- library filter select -->
|
||||
<controls-library-filter-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
|
||||
|
||||
@@ -84,7 +83,7 @@
|
||||
<controls-sort-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesSortBy" :descending.sync="settings.seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" />
|
||||
|
||||
<!-- issues page remove all button -->
|
||||
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn>
|
||||
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ $formatNumber(numShowing) }} {{ entityName }}</ui-btn>
|
||||
|
||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
|
||||
</template>
|
||||
@@ -93,14 +92,22 @@
|
||||
<div class="flex-grow" />
|
||||
<p>{{ $strings.MessageSearchResultsFor }} "{{ searchQuery }}"</p>
|
||||
<div class="flex-grow" />
|
||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
|
||||
</template>
|
||||
<!-- authors page -->
|
||||
<template v-else-if="page === 'authors'">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn v-if="userCanUpdate && authors && authors.length && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
|
||||
<template v-else-if="isAuthorsPage">
|
||||
<p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p>
|
||||
|
||||
<div class="flex-grow hidden sm:inline-block" />
|
||||
<ui-btn v-if="userCanUpdate && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
|
||||
|
||||
<!-- author sort select -->
|
||||
<controls-sort-select v-if="authors && authors.length" v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" />
|
||||
<controls-sort-select v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" />
|
||||
</template>
|
||||
<!-- home page -->
|
||||
<template v-else-if="isHome">
|
||||
<div class="flex-grow" />
|
||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,11 +122,7 @@ export default {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
searchQuery: String,
|
||||
authors: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
searchQuery: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -151,11 +154,14 @@ export default {
|
||||
|
||||
if (this.isSeriesRemovedFromContinueListening) {
|
||||
items.push({
|
||||
text: 'Re-Add Series to Continue Listening',
|
||||
text: this.$strings.LabelReAddSeriesToContinueListening,
|
||||
action: 're-add-to-continue-listening'
|
||||
})
|
||||
}
|
||||
|
||||
this.addSubtitlesMenuItem(items)
|
||||
this.addCollapseSubSeriesMenuItem(items)
|
||||
|
||||
return items
|
||||
},
|
||||
seriesSortItems() {
|
||||
@@ -183,6 +189,10 @@ export default {
|
||||
{
|
||||
text: this.$strings.LabelTotalDuration,
|
||||
value: 'totalDuration'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelRandomly,
|
||||
value: 'random'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -237,9 +247,6 @@ export default {
|
||||
isPodcastLibrary() {
|
||||
return this.currentLibraryMediaType === 'podcast'
|
||||
},
|
||||
isMusicLibrary() {
|
||||
return this.currentLibraryMediaType === 'music'
|
||||
},
|
||||
isLibraryPage() {
|
||||
return this.page === ''
|
||||
},
|
||||
@@ -261,8 +268,11 @@ export default {
|
||||
isPodcastLatestPage() {
|
||||
return this.$route.name === 'library-library-podcast-latest'
|
||||
},
|
||||
isPodcastDownloadQueuePage() {
|
||||
return this.$route.name === 'library-library-podcast-download-queue'
|
||||
},
|
||||
isAuthorsPage() {
|
||||
return this.$route.name === 'library-library-authors'
|
||||
return this.page === 'authors'
|
||||
},
|
||||
isAlbumsPage() {
|
||||
return this.page === 'albums'
|
||||
@@ -272,13 +282,13 @@ export default {
|
||||
},
|
||||
entityName() {
|
||||
if (this.isAlbumsPage) return 'Albums'
|
||||
if (this.isMusicLibrary) return 'Tracks'
|
||||
|
||||
if (this.isPodcastLibrary) return this.$strings.LabelPodcasts
|
||||
if (!this.page) return this.$strings.LabelBooks
|
||||
if (this.isSeriesPage) return this.$strings.LabelSeries
|
||||
if (this.isCollectionsPage) return this.$strings.LabelCollections
|
||||
if (this.isPlaylistsPage) return this.$strings.LabelPlaylists
|
||||
if (this.isAuthorsPage) return this.$strings.LabelAuthors
|
||||
return ''
|
||||
},
|
||||
seriesId() {
|
||||
@@ -318,11 +328,14 @@ export default {
|
||||
|
||||
if (this.isPodcastLibrary && this.isLibraryPage && this.userCanDownload) {
|
||||
items.push({
|
||||
text: 'Export OPML',
|
||||
text: this.$strings.LabelExportOPML,
|
||||
action: 'export-opml'
|
||||
})
|
||||
}
|
||||
|
||||
this.addSubtitlesMenuItem(items)
|
||||
this.addCollapseSeriesMenuItem(items)
|
||||
|
||||
return items
|
||||
},
|
||||
showPlaylists() {
|
||||
@@ -330,9 +343,98 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addSubtitlesMenuItem(items) {
|
||||
if (this.isBookLibrary && (!this.page || this.page === 'search')) {
|
||||
if (this.settings.showSubtitles) {
|
||||
items.push({
|
||||
text: this.$strings.LabelHideSubtitles,
|
||||
action: 'hide-subtitles'
|
||||
})
|
||||
} else {
|
||||
items.push({
|
||||
text: this.$strings.LabelShowSubtitles,
|
||||
action: 'show-subtitles'
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
addCollapseSeriesMenuItem(items) {
|
||||
if (this.isLibraryPage && this.isBookLibrary && !this.isBatchSelecting) {
|
||||
if (this.settings.collapseSeries) {
|
||||
items.push({
|
||||
text: this.$strings.LabelExpandSeries,
|
||||
action: 'expand-series'
|
||||
})
|
||||
} else {
|
||||
items.push({
|
||||
text: this.$strings.LabelCollapseSeries,
|
||||
action: 'collapse-series'
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
addCollapseSubSeriesMenuItem(items) {
|
||||
if (this.selectedSeries && this.isBookLibrary && !this.isBatchSelecting) {
|
||||
if (this.settings.collapseBookSeries) {
|
||||
items.push({
|
||||
text: this.$strings.LabelExpandSubSeries,
|
||||
action: 'expand-sub-series'
|
||||
})
|
||||
} else {
|
||||
items.push({
|
||||
text: this.$strings.LabelCollapseSubSeries,
|
||||
action: 'collapse-sub-series'
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
handleSubtitlesAction(action) {
|
||||
if (action === 'show-subtitles') {
|
||||
this.settings.showSubtitles = true
|
||||
this.updateShowSubtitles()
|
||||
return true
|
||||
}
|
||||
if (action === 'hide-subtitles') {
|
||||
this.settings.showSubtitles = false
|
||||
this.updateShowSubtitles()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
handleCollapseSeriesAction(action) {
|
||||
if (action === 'collapse-series') {
|
||||
this.settings.collapseSeries = true
|
||||
this.updateCollapseSeries()
|
||||
return true
|
||||
}
|
||||
if (action === 'expand-series') {
|
||||
this.settings.collapseSeries = false
|
||||
this.updateCollapseSeries()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
handleCollapseSubSeriesAction(action) {
|
||||
if (action === 'collapse-sub-series') {
|
||||
this.settings.collapseBookSeries = true
|
||||
this.updateCollapseSubSeries()
|
||||
return true
|
||||
}
|
||||
if (action === 'expand-sub-series') {
|
||||
this.settings.collapseBookSeries = false
|
||||
this.updateCollapseSubSeries()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
contextMenuAction({ action }) {
|
||||
if (action === 'export-opml') {
|
||||
this.exportOPML()
|
||||
return
|
||||
} else if (this.handleSubtitlesAction(action)) {
|
||||
return
|
||||
} else if (this.handleCollapseSeriesAction(action)) {
|
||||
return
|
||||
}
|
||||
},
|
||||
exportOPML() {
|
||||
@@ -353,6 +455,10 @@ export default {
|
||||
return
|
||||
}
|
||||
this.markSeriesFinished()
|
||||
} else if (this.handleSubtitlesAction(action)) {
|
||||
return
|
||||
} else if (this.handleCollapseSubSeriesAction(action)) {
|
||||
return
|
||||
}
|
||||
},
|
||||
showOpenSeriesRSSFeed() {
|
||||
@@ -368,46 +474,58 @@ export default {
|
||||
this.$axios
|
||||
.$get(`/api/me/series/${this.seriesId}/readd-to-continue-listening`)
|
||||
.then(() => {
|
||||
this.$toast.success('Series re-added to continue listening')
|
||||
this.$toast.success(this.$strings.ToastItemUpdateSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to re-add series to continue listening', error)
|
||||
this.$toast.error('Failed to re-add series to continue listening')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processingSeries = false
|
||||
})
|
||||
},
|
||||
async fetchAllAuthors() {
|
||||
// fetch all authors from the server, in the order that they are currently displayed
|
||||
const response = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/authors?sort=${this.settings.authorSortBy}&desc=${this.settings.authorSortDesc}`)
|
||||
return response.authors
|
||||
},
|
||||
async matchAllAuthors() {
|
||||
this.processingAuthors = true
|
||||
|
||||
for (const author of this.authors) {
|
||||
const payload = {}
|
||||
if (author.asin) payload.asin = author.asin
|
||||
else payload.q = author.name
|
||||
try {
|
||||
const authors = await this.fetchAllAuthors()
|
||||
|
||||
payload.region = 'us'
|
||||
if (this.libraryProvider.startsWith('audible.')) {
|
||||
payload.region = this.libraryProvider.split('.').pop() || 'us'
|
||||
for (const author of authors) {
|
||||
const payload = {}
|
||||
if (author.asin) payload.asin = author.asin
|
||||
else payload.q = author.name
|
||||
|
||||
payload.region = 'us'
|
||||
if (this.libraryProvider.startsWith('audible.')) {
|
||||
payload.region = this.libraryProvider.split('.').pop() || 'us'
|
||||
}
|
||||
|
||||
this.$eventBus.$emit(`searching-author-${author.id}`, true)
|
||||
|
||||
var response = await this.$axios.$post(`/api/authors/${author.id}/match`, payload).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
return null
|
||||
})
|
||||
if (!response) {
|
||||
console.error(`Author ${author.name} not found`)
|
||||
this.$toast.error(this.$getString('ToastAuthorNotFound', [author.name]))
|
||||
} else if (response.updated) {
|
||||
if (response.author.imagePath) console.log(`Author ${response.author.name} was updated`)
|
||||
else console.log(`Author ${response.author.name} was updated (no image found)`)
|
||||
} else {
|
||||
console.log(`No updates were made for Author ${response.author.name}`)
|
||||
}
|
||||
|
||||
this.$eventBus.$emit(`searching-author-${author.id}`, false)
|
||||
}
|
||||
|
||||
this.$eventBus.$emit(`searching-author-${author.id}`, true)
|
||||
|
||||
var response = await this.$axios.$post(`/api/authors/${author.id}/match`, payload).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
return null
|
||||
})
|
||||
if (!response) {
|
||||
console.error(`Author ${author.name} not found`)
|
||||
this.$toast.error(`Author ${author.name} not found`)
|
||||
} else if (response.updated) {
|
||||
if (response.author.imagePath) console.log(`Author ${response.author.name} was updated`)
|
||||
else console.log(`Author ${response.author.name} was updated (no image found)`)
|
||||
} else {
|
||||
console.log(`No updates were made for Author ${response.author.name}`)
|
||||
}
|
||||
|
||||
this.$eventBus.$emit(`searching-author-${author.id}`, false)
|
||||
} catch (error) {
|
||||
console.error('Failed to match all authors', error)
|
||||
this.$toast.error(this.$strings.ToastMatchAllAuthorsFailed)
|
||||
}
|
||||
this.processingAuthors = false
|
||||
},
|
||||
@@ -417,13 +535,13 @@ export default {
|
||||
this.$axios
|
||||
.$delete(`/api/libraries/${this.currentLibraryId}/issues`)
|
||||
.then(() => {
|
||||
this.$toast.success('Removed library items with issues')
|
||||
this.$toast.success(this.$strings.ToastRemoveItemsWithIssuesSuccess)
|
||||
this.$router.push(`/library/${this.currentLibraryId}/bookshelf`)
|
||||
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove library items with issues', error)
|
||||
this.$toast.error('Failed to remove library items with issues')
|
||||
this.$toast.error(this.$strings.ToastRemoveItemsWithIssuesFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processingIssues = false
|
||||
@@ -479,7 +597,10 @@ export default {
|
||||
updateCollapseSeries() {
|
||||
this.saveSettings()
|
||||
},
|
||||
updateCollapseBookSeries() {
|
||||
updateCollapseSubSeries() {
|
||||
this.saveSettings()
|
||||
},
|
||||
updateShowSubtitles() {
|
||||
this.saveSettings()
|
||||
},
|
||||
updateAuthorSort() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<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-icons text-2xl">arrow_back</span>
|
||||
<span class="material-symbols text-2xl">arrow_back</span>
|
||||
</div>
|
||||
|
||||
<nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-3 h-12 border-b border-primary border-opacity-30 flex items-center cursor-pointer relative" :class="routeName === route.id ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
|
||||
@@ -10,7 +10,7 @@
|
||||
<div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version" />
|
||||
<modals-changelog-view-modal v-model="showChangelogModal" :versionData="versionData" />
|
||||
</div>
|
||||
|
||||
<div class="w-44 h-12 px-4 border-t bg-bg border-black border-opacity-20 fixed left-0 flex flex-col justify-center" :class="wrapperClass" :style="{ bottom: streamLibraryItem ? '160px' : '0px' }">
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<p class="text-xs text-gray-300 italic">{{ Source }}</p>
|
||||
</div>
|
||||
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ latestVersion }}</a>
|
||||
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ versionData.latestVersion }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -114,9 +114,9 @@ export default {
|
||||
|
||||
if (this.currentLibraryId) {
|
||||
configRoutes.push({
|
||||
id: 'config-library-stats',
|
||||
id: 'library-stats',
|
||||
title: this.$strings.HeaderLibraryStats,
|
||||
path: '/config/library-stats'
|
||||
path: `/library/${this.currentLibraryId}/stats`
|
||||
})
|
||||
configRoutes.push({
|
||||
id: 'config-stats',
|
||||
@@ -156,15 +156,9 @@ export default {
|
||||
hasUpdate() {
|
||||
return !!this.versionData.hasUpdate
|
||||
},
|
||||
latestVersion() {
|
||||
return this.versionData.latestVersion
|
||||
},
|
||||
githubTagUrl() {
|
||||
return this.versionData.githubTagUrl
|
||||
},
|
||||
currentVersionChangelog() {
|
||||
return this.versionData.currentVersionChangelog || 'No Changelog Available'
|
||||
},
|
||||
streamLibraryItem() {
|
||||
return this.$store.state.streamLibraryItem
|
||||
}
|
||||
@@ -182,4 +176,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -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: {
|
||||
@@ -91,6 +109,7 @@ export default {
|
||||
if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries
|
||||
if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollections
|
||||
if (this.page === 'playlists') return this.$strings.MessageNoUserPlaylists
|
||||
if (this.page === 'authors') return this.$strings.MessageNoAuthors
|
||||
if (this.hasFilter) {
|
||||
if (this.filterName === 'Issues') return this.$strings.MessageNoIssues
|
||||
else if (this.filterName === 'Feed-open') return this.$strings.MessageBookshelfNoRSSFeeds
|
||||
@@ -98,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
|
||||
@@ -111,6 +135,12 @@ export default {
|
||||
seriesFilterBy() {
|
||||
return this.$store.getters['user/getUserSetting']('seriesFilterBy')
|
||||
},
|
||||
authorSortBy() {
|
||||
return this.$store.getters['user/getUserSetting']('authorSortBy')
|
||||
},
|
||||
authorSortDesc() {
|
||||
return !!this.$store.getters['user/getUserSetting']('authorSortDesc')
|
||||
},
|
||||
orderBy() {
|
||||
return this.$store.getters['user/getUserSetting']('orderBy')
|
||||
},
|
||||
@@ -164,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
|
||||
@@ -177,9 +204,6 @@ export default {
|
||||
entityWidth() {
|
||||
return this.cardWidth
|
||||
},
|
||||
entityHeight() {
|
||||
return this.cardHeight
|
||||
},
|
||||
shelfPaddingHeight() {
|
||||
return 16
|
||||
},
|
||||
@@ -217,6 +241,8 @@ export default {
|
||||
this.$store.commit('globals/setEditCollection', entity)
|
||||
} else if (this.entityName === 'playlists') {
|
||||
this.$store.commit('globals/setEditPlaylist', entity)
|
||||
} else if (this.entityName === 'authors') {
|
||||
this.$store.commit('globals/showEditAuthorModal', entity)
|
||||
}
|
||||
},
|
||||
clearSelectedEntities() {
|
||||
@@ -345,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) {
|
||||
@@ -396,8 +425,6 @@ export default {
|
||||
return
|
||||
}
|
||||
this.destroyEntityComponents()
|
||||
this.entityIndexesMounted = []
|
||||
this.entityComponentRefs = {}
|
||||
this.pagesLoaded = {}
|
||||
this.entities = []
|
||||
this.totalShelves = 0
|
||||
@@ -407,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') {
|
||||
@@ -457,6 +465,9 @@ export default {
|
||||
if (this.collapseBookSeries) {
|
||||
searchParams.set('collapseseries', 1)
|
||||
}
|
||||
} else if (this.page === 'authors') {
|
||||
searchParams.set('sort', this.authorSortBy)
|
||||
searchParams.set('desc', this.authorSortDesc ? 1 : 0)
|
||||
} else {
|
||||
if (this.filterBy && this.filterBy !== 'all') {
|
||||
searchParams.set('filter', this.filterBy)
|
||||
@@ -501,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) {
|
||||
@@ -601,6 +629,34 @@ export default {
|
||||
this.executeRebuild()
|
||||
}
|
||||
},
|
||||
authorAdded(author) {
|
||||
if (this.entityName !== 'authors') return
|
||||
console.log(`[LazyBookshelf] authorAdded ${author.id}`, author)
|
||||
this.resetEntities()
|
||||
},
|
||||
authorUpdated(author) {
|
||||
if (this.entityName !== 'authors') return
|
||||
console.log(`[LazyBookshelf] authorUpdated ${author.id}`, author)
|
||||
const indexOf = this.entities.findIndex((ent) => ent && ent.id === author.id)
|
||||
if (indexOf >= 0) {
|
||||
this.entities[indexOf] = author
|
||||
if (this.entityComponentRefs[indexOf]) {
|
||||
this.entityComponentRefs[indexOf].setEntity(author)
|
||||
}
|
||||
}
|
||||
},
|
||||
authorRemoved(author) {
|
||||
if (this.entityName !== 'authors') return
|
||||
console.log(`[LazyBookshelf] authorRemoved ${author.id}`, author)
|
||||
const indexOf = this.entities.findIndex((ent) => ent && ent.id === author.id)
|
||||
if (indexOf >= 0) {
|
||||
this.entities = this.entities.filter((ent) => ent.id !== author.id)
|
||||
this.totalEntities--
|
||||
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
|
||||
this.executeRebuild()
|
||||
}
|
||||
},
|
||||
|
||||
shareOpen(mediaItemShare) {
|
||||
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
||||
var indexOf = this.entities.findIndex((ent) => ent?.media?.id === mediaItemShare.mediaItemId)
|
||||
@@ -627,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
|
||||
}
|
||||
}
|
||||
@@ -648,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
|
||||
@@ -673,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) {
|
||||
@@ -707,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 })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -727,6 +782,9 @@ export default {
|
||||
this.$root.socket.on('playlist_added', this.playlistAdded)
|
||||
this.$root.socket.on('playlist_updated', this.playlistUpdated)
|
||||
this.$root.socket.on('playlist_removed', this.playlistRemoved)
|
||||
this.$root.socket.on('author_added', this.authorAdded)
|
||||
this.$root.socket.on('author_updated', this.authorUpdated)
|
||||
this.$root.socket.on('author_removed', this.authorRemoved)
|
||||
this.$root.socket.on('share_open', this.shareOpen)
|
||||
this.$root.socket.on('share_closed', this.shareClosed)
|
||||
} else {
|
||||
@@ -756,6 +814,9 @@ export default {
|
||||
this.$root.socket.off('playlist_added', this.playlistAdded)
|
||||
this.$root.socket.off('playlist_updated', this.playlistUpdated)
|
||||
this.$root.socket.off('playlist_removed', this.playlistRemoved)
|
||||
this.$root.socket.off('author_added', this.authorAdded)
|
||||
this.$root.socket.off('author_updated', this.authorUpdated)
|
||||
this.$root.socket.off('author_removed', this.authorRemoved)
|
||||
this.$root.socket.off('share_open', this.shareOpen)
|
||||
this.$root.socket.off('share_closed', this.shareClosed)
|
||||
} else {
|
||||
@@ -764,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
|
||||
@@ -780,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() {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<template>
|
||||
<div v-if="streamLibraryItem" id="mediaPlayerContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 lg:h-40 z-50 bg-primary px-2 lg:px-4 pb-1 lg:pb-4 pt-2">
|
||||
<div id="videoDock" />
|
||||
<div class="absolute left-2 top-2 lg:left-4 cursor-pointer">
|
||||
<covers-book-cover expand-on-click :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
||||
</div>
|
||||
<div class="flex items-start mb-6 lg:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
|
||||
<div class="flex items-start mb-6 lg:mb-0" :class="isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
|
||||
<div class="min-w-0 w-full">
|
||||
<div class="flex items-center">
|
||||
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
|
||||
@@ -12,10 +11,9 @@
|
||||
</nuxt-link>
|
||||
<widgets-explicit-indicator v-if="isExplicit" />
|
||||
</div>
|
||||
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5">
|
||||
<span class="material-icons text-sm">person</span>
|
||||
<div class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5">
|
||||
<span class="material-symbols text-sm">person</span>
|
||||
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
|
||||
<div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div>
|
||||
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate">
|
||||
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
||||
</div>
|
||||
@@ -23,41 +21,45 @@
|
||||
</div>
|
||||
|
||||
<div class="text-gray-400 flex items-center">
|
||||
<span class="material-icons text-xs">schedule</span>
|
||||
<span class="material-symbols text-xs">schedule</span>
|
||||
<p class="font-mono text-xs sm:text-sm pl-1 sm:pl-1.5 pb-px">{{ totalDurationPretty }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<ui-tooltip direction="top" :text="$strings.LabelClosePlayer">
|
||||
<button :aria-label="$strings.LabelClosePlayer" class="material-icons sm:px-2 py-1 lg:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</button>
|
||||
<button :aria-label="$strings.LabelClosePlayer" class="material-symbols sm:px-2 py-1 lg:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</button>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<player-ui
|
||||
ref="audioPlayer"
|
||||
:chapters="chapters"
|
||||
:current-chapter="currentChapter"
|
||||
:paused="!isPlaying"
|
||||
:loading="playerLoading"
|
||||
:bookmarks="bookmarks"
|
||||
:sleep-timer-set="sleepTimerSet"
|
||||
:sleep-timer-remaining="sleepTimerRemaining"
|
||||
:sleep-timer-type="sleepTimerType"
|
||||
:is-podcast="isPodcast"
|
||||
:hasNextItemInQueue="hasNextItemInQueue"
|
||||
@playPause="playPause"
|
||||
@jumpForward="jumpForward"
|
||||
@jumpBackward="jumpBackward"
|
||||
@setVolume="setVolume"
|
||||
@setPlaybackRate="setPlaybackRate"
|
||||
@seek="seek"
|
||||
@nextItemInQueue="playNextItemInQueue"
|
||||
@close="closePlayer"
|
||||
@showBookmarks="showBookmarks"
|
||||
@showSleepTimer="showSleepTimerModal = true"
|
||||
@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-time="sleepTimerTime" :remaining="sleepTimerRemaining" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
|
||||
<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" />
|
||||
|
||||
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" :library-item-id="libraryItemId" />
|
||||
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -77,13 +79,14 @@ export default {
|
||||
showSleepTimerModal: false,
|
||||
showPlayerQueueItemsModal: false,
|
||||
sleepTimerSet: false,
|
||||
sleepTimerTime: 0,
|
||||
sleepTimerRemaining: 0,
|
||||
sleepTimerType: null,
|
||||
sleepTimer: null,
|
||||
displayTitle: null,
|
||||
currentPlaybackRate: 1,
|
||||
syncFailedToast: null,
|
||||
coverAspectRatio: 1
|
||||
coverAspectRatio: 1,
|
||||
lastChapterId: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -132,9 +135,6 @@ export default {
|
||||
isPodcast() {
|
||||
return this.streamLibraryItem?.mediaType === 'podcast'
|
||||
},
|
||||
isMusic() {
|
||||
return this.streamLibraryItem?.mediaType === 'music'
|
||||
},
|
||||
isExplicit() {
|
||||
return !!this.mediaMetadata.explicit
|
||||
},
|
||||
@@ -145,6 +145,9 @@ export default {
|
||||
if (this.streamEpisode) return this.streamEpisode.chapters || []
|
||||
return this.media.chapters || []
|
||||
},
|
||||
currentChapter() {
|
||||
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
|
||||
},
|
||||
title() {
|
||||
if (this.playerHandler.displayTitle) return this.playerHandler.displayTitle
|
||||
return this.mediaMetadata.title || 'No Title'
|
||||
@@ -161,11 +164,17 @@ export default {
|
||||
},
|
||||
podcastAuthor() {
|
||||
if (!this.isPodcast) return null
|
||||
return this.mediaMetadata.author || 'Unknown'
|
||||
return this.mediaMetadata.author || this.$strings.LabelUnknown
|
||||
},
|
||||
musicArtists() {
|
||||
if (!this.isMusic) return null
|
||||
return this.mediaMetadata.artists.join(', ')
|
||||
hasNextItemInQueue() {
|
||||
return this.currentPlayerQueueIndex < this.playerQueueItems.length - 1
|
||||
},
|
||||
currentPlayerQueueIndex() {
|
||||
if (!this.libraryItemId) return -1
|
||||
return this.playerQueueItems.findIndex((i) => {
|
||||
if (this.streamEpisode?.id) return i.episodeId === this.streamEpisode.id
|
||||
return i.libraryItemId === this.libraryItemId
|
||||
})
|
||||
},
|
||||
playerQueueItems() {
|
||||
return this.$store.state.playerQueueItems || []
|
||||
@@ -204,14 +213,18 @@ export default {
|
||||
this.$store.commit('setIsPlaying', isPlaying)
|
||||
this.updateMediaSessionPlaybackState()
|
||||
},
|
||||
setSleepTimer(seconds) {
|
||||
setSleepTimer(time) {
|
||||
this.sleepTimerSet = true
|
||||
this.sleepTimerTime = seconds
|
||||
this.sleepTimerRemaining = seconds
|
||||
this.runSleepTimer()
|
||||
this.showSleepTimerModal = false
|
||||
|
||||
this.sleepTimerType = time.timerType
|
||||
if (this.sleepTimerType === this.$constants.SleepTimerTypes.COUNTDOWN) {
|
||||
this.runSleepTimer(time)
|
||||
}
|
||||
},
|
||||
runSleepTimer() {
|
||||
runSleepTimer(time) {
|
||||
this.sleepTimerRemaining = time.seconds
|
||||
|
||||
var lastTick = Date.now()
|
||||
clearInterval(this.sleepTimer)
|
||||
this.sleepTimer = setInterval(() => {
|
||||
@@ -220,12 +233,27 @@ export default {
|
||||
this.sleepTimerRemaining -= elapsed / 1000
|
||||
|
||||
if (this.sleepTimerRemaining <= 0) {
|
||||
this.clearSleepTimer()
|
||||
this.playerHandler.pause()
|
||||
this.$toast.info('Sleep Timer Done.. zZzzZz')
|
||||
this.sleepTimerEnd()
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
checkChapterEnd() {
|
||||
if (!this.currentChapter) return
|
||||
|
||||
// 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() {
|
||||
this.clearSleepTimer()
|
||||
this.playerHandler.pause()
|
||||
this.$toast.info(this.$strings.ToastSleepTimerDone)
|
||||
},
|
||||
cancelSleepTimer() {
|
||||
this.showSleepTimerModal = false
|
||||
this.clearSleepTimer()
|
||||
@@ -235,6 +263,7 @@ export default {
|
||||
this.sleepTimerRemaining = 0
|
||||
this.sleepTimer = null
|
||||
this.sleepTimerSet = false
|
||||
this.sleepTimerType = null
|
||||
},
|
||||
incrementSleepTimer(amount) {
|
||||
if (!this.sleepTimerSet) return
|
||||
@@ -275,6 +304,10 @@ export default {
|
||||
if (this.$refs.audioPlayer) {
|
||||
this.$refs.audioPlayer.setCurrentTime(time)
|
||||
}
|
||||
|
||||
if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER && this.sleepTimerSet) {
|
||||
this.checkChapterEnd()
|
||||
}
|
||||
},
|
||||
setDuration(duration) {
|
||||
this.totalDuration = duration
|
||||
@@ -346,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)
|
||||
|
||||
@@ -431,6 +473,30 @@ export default {
|
||||
this.playerHandler.switchPlayer()
|
||||
}
|
||||
},
|
||||
playNextItemInQueue() {
|
||||
if (this.hasNextItemInQueue) {
|
||||
this.playQueueItem({ index: this.currentPlayerQueueIndex + 1 })
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {{ index: number }} payload
|
||||
*/
|
||||
playQueueItem(payload) {
|
||||
if (payload?.index === undefined) {
|
||||
console.error('playQueueItem: No index provided')
|
||||
return
|
||||
}
|
||||
if (!this.playerQueueItems[payload.index]) {
|
||||
console.error('playQueueItem: No item found at index', payload.index)
|
||||
return
|
||||
}
|
||||
const item = this.playerQueueItems[payload.index]
|
||||
this.playLibraryItem({
|
||||
libraryItemId: item.libraryItemId,
|
||||
episodeId: item.episodeId || null,
|
||||
queueItems: this.playerQueueItems
|
||||
})
|
||||
},
|
||||
async playLibraryItem(payload) {
|
||||
const libraryItemId = payload.libraryItemId
|
||||
const episodeId = payload.episodeId || null
|
||||
@@ -469,7 +535,7 @@ export default {
|
||||
},
|
||||
showFailedProgressSyncs() {
|
||||
if (!isNaN(this.syncFailedToast)) this.$toast.dismiss(this.syncFailedToast)
|
||||
this.syncFailedToast = this.$toast('Progress is not being synced. Restart playback', { timeout: false, type: 'error' })
|
||||
this.syncFailedToast = this.$toast(this.$strings.ToastProgressIsNotBeingSynced, { timeout: false, type: 'error' })
|
||||
},
|
||||
sessionClosedEvent(sessionId) {
|
||||
if (this.playerHandler.currentSessionId === sessionId) {
|
||||
@@ -483,6 +549,7 @@ export default {
|
||||
this.$eventBus.$on('cast-session-active', this.castSessionActive)
|
||||
this.$eventBus.$on('playback-seek', this.seek)
|
||||
this.$eventBus.$on('playback-time-update', this.playbackTimeUpdate)
|
||||
this.$eventBus.$on('play-queue-item', this.playQueueItem)
|
||||
this.$eventBus.$on('play-item', this.playLibraryItem)
|
||||
this.$eventBus.$on('pause-item', this.pauseItem)
|
||||
},
|
||||
@@ -490,6 +557,7 @@ export default {
|
||||
this.$eventBus.$off('cast-session-active', this.castSessionActive)
|
||||
this.$eventBus.$off('playback-seek', this.seek)
|
||||
this.$eventBus.$off('playback-time-update', this.playbackTimeUpdate)
|
||||
this.$eventBus.$off('play-queue-item', this.playQueueItem)
|
||||
this.$eventBus.$off('play-item', this.playLibraryItem)
|
||||
this.$eventBus.$off('pause-item', this.pauseItem)
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
@@ -15,7 +15,7 @@
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" 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="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-icons text-2xl">format_list_bulleted</span>
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
|
||||
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-icons-outlined text-2xl">collections_bookmark</span>
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
|
||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
|
||||
|
||||
@@ -51,14 +51,14 @@
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-icons text-2.5xl">queue_music</span>
|
||||
<span class="material-symbols text-2.5xl"></span>
|
||||
|
||||
<p class="pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
|
||||
|
||||
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<svg class="w-6 h-6" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
@@ -72,13 +72,21 @@
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/narrators`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isNarratorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-icons text-2xl">record_voice_over</span>
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
|
||||
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.LabelNarrators }}</p>
|
||||
|
||||
<div v-show="isNarratorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/stats`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isStatsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
|
||||
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonStats }}</p>
|
||||
|
||||
<div v-show="isStatsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="abs-icons icon-podcast text-xl"></span>
|
||||
|
||||
@@ -87,24 +95,16 @@
|
||||
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isMusicLibrary" :to="`/library/${currentLibraryId}/bookshelf/albums`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isMusicAlbumsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-icons-outlined text-xl">album</span>
|
||||
|
||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">Albums</p>
|
||||
|
||||
<div v-show="isMusicAlbumsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-icons text-2xl">file_download</span>
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
|
||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonDownloadQueue }}</p>
|
||||
|
||||
<div v-show="isPodcastDownloadQueuePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
|
||||
<span class="material-icons text-2xl">warning</span>
|
||||
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : 'bg-error bg-opacity-20'">
|
||||
<span class="material-symbols text-2xl">warning</span>
|
||||
|
||||
<p class="pt-1.5 text-center leading-4" style="font-size: 1rem">{{ $strings.ButtonIssues }}</p>
|
||||
|
||||
@@ -164,9 +164,6 @@ export default {
|
||||
isPodcastLibrary() {
|
||||
return this.currentLibraryMediaType === 'podcast'
|
||||
},
|
||||
isMusicLibrary() {
|
||||
return this.currentLibraryMediaType === 'music'
|
||||
},
|
||||
isPodcastDownloadQueuePage() {
|
||||
return this.$route.name === 'library-library-podcast-download-queue'
|
||||
},
|
||||
@@ -176,9 +173,6 @@ export default {
|
||||
isPodcastLatestPage() {
|
||||
return this.$route.name === 'library-library-podcast-latest'
|
||||
},
|
||||
isMusicAlbumsPage() {
|
||||
return this.paramId === 'albums'
|
||||
},
|
||||
homePage() {
|
||||
return this.$route.name === 'library-library'
|
||||
},
|
||||
@@ -186,7 +180,7 @@ export default {
|
||||
return this.$route.name === 'library-library-series-id' || this.paramId === 'series'
|
||||
},
|
||||
isAuthorsPage() {
|
||||
return this.$route.name === 'library-library-authors'
|
||||
return this.libraryBookshelfPage && this.paramId === 'authors'
|
||||
},
|
||||
isNarratorsPage() {
|
||||
return this.$route.name === 'library-library-narrators'
|
||||
@@ -194,6 +188,9 @@ export default {
|
||||
isPlaylistsPage() {
|
||||
return this.paramId === 'playlists'
|
||||
},
|
||||
isStatsPage() {
|
||||
return this.$route.name === 'library-library-stats'
|
||||
},
|
||||
libraryBookshelfPage() {
|
||||
return this.$route.name === 'library-library-bookshelf-id'
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div :style="{ minWidth: cardWidth + 'px', maxWidth: cardWidth + 'px' }">
|
||||
<nuxt-link :to="`/author/${author.id}`">
|
||||
<div class="pb-3e" :style="{ minWidth: cardWidth + 'px', maxWidth: cardWidth + 'px' }">
|
||||
<nuxt-link :to="`/author/${author?.id}`">
|
||||
<div cy-id="card" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<div cy-id="imageArea" :style="{ height: cardHeight + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
||||
<!-- Image or placeholder -->
|
||||
@@ -15,12 +15,12 @@
|
||||
<!-- Search icon btn -->
|
||||
<div cy-id="match" v-show="!searching && isHovering && userCanUpdate" class="absolute top-0 left-0 p-2e cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="searchAuthor">
|
||||
<ui-tooltip :text="$strings.ButtonQuickMatch" direction="bottom">
|
||||
<span class="material-icons" :style="{ fontSize: 1.125 + 'em' }">search</span>
|
||||
<span class="material-symbols" :style="{ fontSize: 1.125 + 'em' }">search</span>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<div cy-id="edit" v-show="isHovering && !searching && userCanUpdate" class="absolute top-0 right-0 p-2e cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="$emit('edit', author)">
|
||||
<ui-tooltip :text="$strings.LabelEdit" direction="bottom">
|
||||
<span class="material-icons" :style="{ fontSize: 1.125 + 'em' }">edit</span>
|
||||
<span class="material-symbols" :style="{ fontSize: 1.125 + 'em' }">edit</span>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
author: {
|
||||
authorMount: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
@@ -57,7 +57,8 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
searching: false,
|
||||
isHovering: false
|
||||
isHovering: false,
|
||||
author: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -67,35 +68,41 @@ export default {
|
||||
cardHeight() {
|
||||
return this.height * this.sizeMultiplier
|
||||
},
|
||||
coverHeight() {
|
||||
return this.cardHeight
|
||||
},
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
return this.store.getters['user/getToken']
|
||||
},
|
||||
_author() {
|
||||
return this.author || {}
|
||||
},
|
||||
authorId() {
|
||||
return this._author.id
|
||||
return this._author?.id || ''
|
||||
},
|
||||
name() {
|
||||
return this._author.name || ''
|
||||
return this._author?.name || ''
|
||||
},
|
||||
asin() {
|
||||
return this._author.asin || ''
|
||||
return this._author?.asin || ''
|
||||
},
|
||||
numBooks() {
|
||||
return this._author.numBooks || 0
|
||||
return this._author?.numBooks || 0
|
||||
},
|
||||
store() {
|
||||
return this.$store || this.$nuxt.$store
|
||||
},
|
||||
userCanUpdate() {
|
||||
return this.$store.getters['user/getUserCanUpdate']
|
||||
return this.store.getters['user/getUserCanUpdate']
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
return this.store.state.libraries.currentLibraryId
|
||||
},
|
||||
libraryProvider() {
|
||||
return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
|
||||
return this.store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
|
||||
},
|
||||
sizeMultiplier() {
|
||||
return this.$store.getters['user/getSizeMultiplier']
|
||||
return this.store.getters['user/getSizeMultiplier']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -121,24 +128,54 @@ export default {
|
||||
return null
|
||||
})
|
||||
if (!response) {
|
||||
this.$toast.error(`Author ${this.name} not found`)
|
||||
this.$toast.error(this.$getString('ToastAuthorNotFound', [this.name]))
|
||||
} else if (response.updated) {
|
||||
if (response.author.imagePath) this.$toast.success(`Author ${response.author.name} was updated`)
|
||||
else this.$toast.success(`Author ${response.author.name} was updated (no image found)`)
|
||||
if (response.author.imagePath) {
|
||||
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||
} else {
|
||||
this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
||||
}
|
||||
} else {
|
||||
this.$toast.info(`No updates were made for Author ${response.author.name}`)
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
}
|
||||
this.searching = false
|
||||
},
|
||||
setSearching(isSearching) {
|
||||
this.searching = isSearching
|
||||
}
|
||||
},
|
||||
setEntity(author) {
|
||||
this.removeListeners()
|
||||
this.author = author
|
||||
this.addListeners()
|
||||
},
|
||||
addListeners() {
|
||||
if (this.author) {
|
||||
this.$eventBus.$on(`searching-author-${this.authorId}`, this.setSearching)
|
||||
}
|
||||
},
|
||||
removeListeners() {
|
||||
if (this.author) {
|
||||
this.$eventBus.$off(`searching-author-${this.authorId}`, this.setSearching)
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
// destroy the vue listeners, etc
|
||||
this.$destroy()
|
||||
|
||||
// remove the element from the DOM
|
||||
if (this.$el && this.$el.parentNode) {
|
||||
this.$el.parentNode.removeChild(this.$el)
|
||||
} else if (this.$el && this.$el.remove) {
|
||||
this.$el.remove()
|
||||
}
|
||||
},
|
||||
setSelectionMode(val) {}
|
||||
},
|
||||
mounted() {
|
||||
this.$eventBus.$on(`searching-author-${this.authorId}`, this.setSearching)
|
||||
if (this.authorMount) this.setEntity(this.authorMount)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$eventBus.$off(`searching-author-${this.authorId}`, this.setSearching)
|
||||
this.removeListeners()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
</div>
|
||||
<div class="flex-grow px-2 authorSearchCardContent h-full">
|
||||
<p class="truncate text-sm">{{ name }}</p>
|
||||
<p class="text-xs text-gray-400">{{ $getString('LabelXBooks', [numBooks]) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -23,6 +24,9 @@ export default {
|
||||
computed: {
|
||||
name() {
|
||||
return this.author.name
|
||||
},
|
||||
numBooks() {
|
||||
return this.author.numBooks
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
@@ -33,9 +37,9 @@ export default {
|
||||
<style>
|
||||
.authorSearchCardContent {
|
||||
width: calc(100% - 80px);
|
||||
height: 40px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="relative pointer-events-none" :style="{ width: standardWidth * 0.8 * 1.1 * scale + 'px', height: standardHeight * 1.1 * scale + 'px', marginBottom: 20 + 'px', marginTop: 15 + 'px' }">
|
||||
<div ref="card" class="wrap absolute origin-center transform duration-200" :style="{ transform: `scale(${scale * scaleMultiplier}) translateY(${hover2 ? '-40%' : '-50%'})` }">
|
||||
<div class="perspective">
|
||||
<div class="book-wrap transform duration-100 pointer-events-auto" :class="hover2 ? 'z-80' : 'rotate'" @mouseover="hover = true" @mouseout="hover = false">
|
||||
<div class="book book-1 box-shadow-book3d" ref="front"></div>
|
||||
<div class="title book-1 pointer-events-none" ref="left"></div>
|
||||
<div class="bottom book-1 pointer-events-none" ref="bottom"></div>
|
||||
<div class="book-back book-1 pointer-events-none">
|
||||
<div class="text pointer-events-none">
|
||||
<h3 class="mb-4">Book Back</h3>
|
||||
<p>
|
||||
<span>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Sunt earum doloremque aliquam culpa dolor nostrum consequatur quas dicta? Molestias repellendus minima pariatur libero vel, reiciendis optio magnam rerum, labore corporis.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
src: String,
|
||||
width: {
|
||||
type: Number,
|
||||
default: 200
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hover: false,
|
||||
hover2: false,
|
||||
standardWidth: 200,
|
||||
standardHeight: 320,
|
||||
isAttached: true,
|
||||
pageX: 0,
|
||||
pageY: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
src(newVal) {
|
||||
this.setCover()
|
||||
},
|
||||
width(newVal) {
|
||||
this.init()
|
||||
},
|
||||
hover(newVal) {
|
||||
if (newVal) {
|
||||
this.unattach()
|
||||
} else {
|
||||
this.attach()
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.hover2 = newVal
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
scaleMultiplier() {
|
||||
return this.hover2 ? 1.25 : 1
|
||||
},
|
||||
scale() {
|
||||
var scale = this.width / this.standardWidth
|
||||
return scale
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
unattach() {
|
||||
if (this.$refs.card && this.isAttached) {
|
||||
var bookshelf = document.getElementById('bookshelf')
|
||||
if (bookshelf) {
|
||||
var pos = this.$refs.wrapper.getBoundingClientRect()
|
||||
|
||||
this.pageX = pos.x
|
||||
this.pageY = pos.y
|
||||
document.body.appendChild(this.$refs.card)
|
||||
this.$refs.card.style.left = this.pageX + 'px'
|
||||
this.$refs.card.style.top = this.pageY + 'px'
|
||||
this.$refs.card.style.zIndex = 50
|
||||
this.isAttached = false
|
||||
} else if (bookshelf) {
|
||||
console.log(this.pageX, this.pageY)
|
||||
this.isAttached = false
|
||||
}
|
||||
}
|
||||
},
|
||||
attach() {
|
||||
if (this.$refs.card && !this.isAttached) {
|
||||
if (this.$refs.wrapper) {
|
||||
this.isAttached = true
|
||||
|
||||
this.$refs.wrapper.appendChild(this.$refs.card)
|
||||
this.$refs.card.style.left = '0px'
|
||||
this.$refs.card.style.top = '0px'
|
||||
}
|
||||
} else {
|
||||
console.log('Is attached already', this.isAttached)
|
||||
}
|
||||
},
|
||||
init() {
|
||||
var standardWidth = this.standardWidth
|
||||
document.documentElement.style.setProperty('--book-w', standardWidth + 'px')
|
||||
document.documentElement.style.setProperty('--book-wx', standardWidth + 1 + 'px')
|
||||
document.documentElement.style.setProperty('--book-h', standardWidth * 1.6 + 'px')
|
||||
document.documentElement.style.setProperty('--book-d', 40 + 'px')
|
||||
},
|
||||
setElBg(el) {
|
||||
el.style.backgroundImage = `url("${this.src}")`
|
||||
el.style.backgroundSize = 'cover'
|
||||
el.style.backgroundPosition = 'center center'
|
||||
el.style.backgroundRepeat = 'no-repeat'
|
||||
},
|
||||
setCover() {
|
||||
if (this.$refs.front) {
|
||||
this.setElBg(this.$refs.front)
|
||||
}
|
||||
if (this.$refs.bottom) {
|
||||
this.setElBg(this.$refs.bottom)
|
||||
this.$refs.bottom.style.backgroundSize = '2000%'
|
||||
this.$refs.bottom.style.filter = 'blur(1px)'
|
||||
}
|
||||
if (this.$refs.left) {
|
||||
this.setElBg(this.$refs.left)
|
||||
this.$refs.left.style.backgroundSize = '2000%'
|
||||
this.$refs.left.style.filter = 'blur(1px)'
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setCover()
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* :root {
|
||||
--book-w: 200px;
|
||||
--book-h: 320px;
|
||||
--book-d: 30px;
|
||||
--book-wx: 201px;
|
||||
} */
|
||||
/*
|
||||
.wrap {
|
||||
width: calc(1.1 * var(--book-w));
|
||||
height: calc(1.1 * var(--book-h));
|
||||
margin: 0 auto;
|
||||
}
|
||||
.perspective {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
perspective: 600px;
|
||||
transform-style: preserve-3d;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.book-wrap {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transform-style: preserve-3d;
|
||||
transition: 'all ease-out 0.6s';
|
||||
}
|
||||
|
||||
.book {
|
||||
width: var(--book-w);
|
||||
height: var(--book-h);
|
||||
background: url(https://covers.openlibrary.org/b/id/8303020-L.jpg) no-repeat center center;
|
||||
background-size: cover;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
.title {
|
||||
content: '';
|
||||
height: var(--book-h);
|
||||
width: var(--book-d);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: calc(var(--book-wx) * -1);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
background: #444;
|
||||
transform: rotateY(-80deg) translateX(-14px);
|
||||
|
||||
background: url(https://covers.openlibrary.org/b/id/8303020-L.jpg) no-repeat center center;
|
||||
background-size: 5000%;
|
||||
filter: blur(1px);
|
||||
}
|
||||
|
||||
.bottom {
|
||||
content: '';
|
||||
height: var(--book-d);
|
||||
width: var(--book-w);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: var(--book-h);
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
background: #444;
|
||||
transform: rotateY(0deg) rotateX(90deg) translateY(-15px) translateX(-2.5px) skewX(10deg);
|
||||
|
||||
background: url(https://covers.openlibrary.org/b/id/8303020-L.jpg) no-repeat center center;
|
||||
background-size: 5000%;
|
||||
filter: blur(1px);
|
||||
}
|
||||
|
||||
.book-back {
|
||||
width: var(--book-w);
|
||||
height: var(--book-h);
|
||||
background-color: #444;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
cursor: pointer;
|
||||
transform: rotate(180deg) translateZ(-30px) translateX(5px);
|
||||
}
|
||||
.book-back .text {
|
||||
transform: rotateX(180deg);
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
padding: 20px;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
}
|
||||
.book-back .text h3 {
|
||||
color: #fff;
|
||||
}
|
||||
.book-back .text span {
|
||||
display: block;
|
||||
margin-bottom: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.book-wrap.rotate {
|
||||
transform: rotateY(30deg) rotateX(0deg);
|
||||
}
|
||||
.book-wrap.flip {
|
||||
transform: rotateY(180deg);
|
||||
} */
|
||||
</style>
|
||||
@@ -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">
|
||||
|
||||
36
client/components/cards/GenreSearchCard.vue
Normal file
36
client/components/cards/GenreSearchCard.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="flex h-full px-1 overflow-hidden">
|
||||
<div class="w-10 h-10 flex items-center justify-center">
|
||||
<span class="material-symbols text-2xl text-gray-200">category</span>
|
||||
</div>
|
||||
<div class="flex-grow px-2 tagSearchCardContent h-full">
|
||||
<p class="truncate text-sm">{{ genre }}</p>
|
||||
<p class="text-xs text-gray-400">{{ $getString('LabelXItems', [numItems]) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
genre: String,
|
||||
numItems: Number
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tagSearchCardContent {
|
||||
width: calc(100% - 40px);
|
||||
height: 44px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -2,15 +2,9 @@
|
||||
<div class="flex items-center h-full px-1 overflow-hidden">
|
||||
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<div class="flex-grow px-2 audiobookSearchCardContent">
|
||||
<p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p>
|
||||
<p v-else class="truncate text-sm" v-html="matchHtml" />
|
||||
|
||||
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-gray-300" v-html="matchHtml" />
|
||||
|
||||
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">{{ $getString('LabelByAuthor', [authorName]) }}</p>
|
||||
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
|
||||
|
||||
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin' || matchKey === 'episode' || matchKey === 'narrators'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
|
||||
<p class="truncate text-sm">{{ title }}</p>
|
||||
<p v-if="subtitle" class="truncate text-xs text-gray-300">{{ subtitle }}</p>
|
||||
<p class="text-xs text-gray-200 truncate">{{ $getString('LabelByAuthor', [authorName]) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -21,10 +15,7 @@ export default {
|
||||
libraryItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
search: String,
|
||||
matchKey: String,
|
||||
matchText: String
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
@@ -58,23 +49,6 @@ export default {
|
||||
authorName() {
|
||||
if (this.isPodcast) return this.mediaMetadata.author || 'Unknown'
|
||||
return this.mediaMetadata.authorName || 'Unknown'
|
||||
},
|
||||
matchHtml() {
|
||||
if (!this.matchText || !this.search) return ''
|
||||
|
||||
// This used to highlight the part of the search found
|
||||
// but with removing commas periods etc this is no longer plausible
|
||||
const html = this.matchText
|
||||
|
||||
if (this.matchKey === 'episode') return `<p class="truncate">${this.$strings.LabelEpisode}: ${html}</p>`
|
||||
if (this.matchKey === 'tags') return `<p class="truncate">${this.$strings.LabelTags}: ${html}</p>`
|
||||
if (this.matchKey === 'subtitle') return `<p class="truncate">${html}</p>`
|
||||
if (this.matchKey === 'authors') this.$getString('LabelByAuthor', [html])
|
||||
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
|
||||
if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
|
||||
if (this.matchKey === 'series') return `<p class="truncate">${this.$strings.LabelSeries}: ${html}</p>`
|
||||
if (this.matchKey === 'narrators') return `<p class="truncate">${this.$strings.LabelNarrator}: ${html}</p>`
|
||||
return `${html}`
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<div class="flex items-center px-1 overflow-hidden">
|
||||
<div class="w-8 flex items-center justify-center">
|
||||
<span v-if="isFinished" :class="taskIconStatus" class="material-icons text-base">{{ actionIcon }}</span>
|
||||
<span v-if="isFinished" :class="taskIconStatus" class="material-symbols text-base">{{ actionIcon }}</span>
|
||||
<widgets-loading-spinner v-else />
|
||||
</div>
|
||||
<div class="flex-grow px-2 taskRunningCardContent">
|
||||
<p class="truncate text-sm">{{ title }}</p>
|
||||
|
||||
<p class="truncate text-xs text-gray-300">{{ description }}</p>
|
||||
<p v-if="specialMessage" class="truncate text-xs text-gray-300">{{ specialMessage }}</p>
|
||||
|
||||
<p v-if="isFailed && failedMessage" class="text-xs truncate text-red-500">{{ failedMessage }}</p>
|
||||
<p v-else-if="!isFinished && cancelingScan" class="text-xs truncate">Canceling...</p>
|
||||
@@ -26,7 +27,16 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
cancelingScan: false
|
||||
cancelingScan: false,
|
||||
specialMessage: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
task: {
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.initTask()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -34,14 +44,17 @@ export default {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
title() {
|
||||
if (this.task.titleKey && this.$strings[this.task.titleKey]) {
|
||||
return this.$getString(this.task.titleKey, this.task.titleSubs)
|
||||
}
|
||||
return this.task.title || 'No Title'
|
||||
},
|
||||
description() {
|
||||
if (this.task.descriptionKey && this.$strings[this.task.descriptionKey]) {
|
||||
return this.$getString(this.task.descriptionKey, this.task.descriptionSubs)
|
||||
}
|
||||
return this.task.description || ''
|
||||
},
|
||||
details() {
|
||||
return this.task.details || 'Unknown'
|
||||
},
|
||||
isFinished() {
|
||||
return !!this.task.isFinished
|
||||
},
|
||||
@@ -52,6 +65,9 @@ export default {
|
||||
return this.isFinished && !this.isFailed
|
||||
},
|
||||
failedMessage() {
|
||||
if (this.task.errorKey && this.$strings[this.task.errorKey]) {
|
||||
return this.$getString(this.task.errorKey, this.task.errorSubs)
|
||||
}
|
||||
return this.task.error || ''
|
||||
},
|
||||
action() {
|
||||
@@ -87,6 +103,21 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initTask() {
|
||||
// special message for library scan tasks
|
||||
if (this.task?.data?.scanResults) {
|
||||
const scanResults = this.task.data.scanResults
|
||||
const strs = []
|
||||
if (scanResults.added) strs.push(this.$getString('MessageTaskScanItemsAdded', [scanResults.added]))
|
||||
if (scanResults.updated) strs.push(this.$getString('MessageTaskScanItemsUpdated', [scanResults.updated]))
|
||||
if (scanResults.missing) strs.push(this.$getString('MessageTaskScanItemsMissing', [scanResults.missing]))
|
||||
const changesDetected = strs.length > 0 ? strs.join(', ') : this.$strings.MessageTaskScanNoChangesNeeded
|
||||
const timeElapsed = scanResults.elapsed ? ` (${this.$elapsedPretty(scanResults.elapsed / 1000, false, true)})` : ''
|
||||
this.specialMessage = `${changesDetected}${timeElapsed}`
|
||||
} else {
|
||||
this.specialMessage = ''
|
||||
}
|
||||
},
|
||||
cancelScan() {
|
||||
const libraryId = this.task?.data?.libraryId
|
||||
if (!libraryId) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</div>
|
||||
|
||||
<div v-if="!processing && !uploadFailed && !uploadSuccess" class="absolute -top-3 -right-3 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-error cursor-pointer" @click="$emit('remove')">
|
||||
<span class="text-base text-white text-opacity-80 font-mono material-icons">close</span>
|
||||
<span class="text-base text-white text-opacity-80 font-mono material-symbols">close</span>
|
||||
</div>
|
||||
|
||||
<template v-if="!uploadSuccess && !uploadFailed">
|
||||
@@ -22,7 +22,7 @@
|
||||
<ui-text-input-with-label v-model.trim="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
|
||||
<ui-tooltip :text="$strings.LabelUploaderItemFetchMetadataHelp">
|
||||
<div class="ml-2 mb-1 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-primary cursor-pointer" @click="fetchMetadata">
|
||||
<span class="text-base text-white text-opacity-80 font-mono material-icons">sync</span>
|
||||
<span class="text-base text-white text-opacity-80 font-mono material-symbols">sync</span>
|
||||
</div>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
@@ -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,59 +14,52 @@
|
||||
</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">
|
||||
<div cy-id="playButton" v-show="showPlayButton" 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" @click.stop.prevent="play">
|
||||
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'em' }">play_circle_filled</span>
|
||||
<span class="material-symbols fill" :style="{ fontSize: playIconFontSize + 'em' }">play_arrow</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div cy-id="readButton" v-show="showReadButton" 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" @click.stop.prevent="clickReadEBook">
|
||||
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'em' }">auto_stories</span>
|
||||
<span class="material-symbols" :style="{ fontSize: playIconFontSize + 'em' }">auto_stories</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div cy-id="editButton" v-if="userCanUpdate" v-show="!isSelectionMode" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-150 top-0 right-0" :style="{ padding: 0.375 + 'em' }" @click.stop.prevent="editClick">
|
||||
<span class="material-icons" :style="{ fontSize: 1 + 'em' }">edit</span>
|
||||
<span class="material-symbols" :style="{ fontSize: 1 + 'em' }">edit</span>
|
||||
</div>
|
||||
|
||||
<!-- Radio button -->
|
||||
<div cy-id="selectedRadioButton" v-if="!isAuthorBookshelfView" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 + 'em', left: 0.375 + 'em' }" @click.stop.prevent="selectBtnClick">
|
||||
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 + 'em' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
|
||||
<span class="material-symbols" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 + 'em' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- More Menu Icon -->
|
||||
<div cy-id="moreButton" ref="moreIcon" v-show="!isSelectionMode && moreMenuItems.length" class="md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-150" :style="{ bottom: 0.375 + 'em', right: 0.375 + 'em' }" @click.stop.prevent="clickShowMore">
|
||||
<span class="material-icons" :style="{ fontSize: 1.2 + 'em' }">more_vert</span>
|
||||
<span class="material-symbols" :style="{ fontSize: 1.2 + 'em' }">more_vert</span>
|
||||
</div>
|
||||
|
||||
<div cy-id="ebookFormat" v-if="ebookFormat" class="absolute" :style="{ bottom: 0.375 + 'em', left: 0.375 + 'em' }">
|
||||
@@ -87,17 +80,17 @@
|
||||
<!-- Error widget -->
|
||||
<ui-tooltip cy-id="ErrorTooltip" v-if="showError" :text="errorText" class="absolute bottom-4e left-0 z-10">
|
||||
<div :style="{ height: 1.5 + 'em', width: 2.5 + 'em' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
|
||||
<span class="material-icons text-red-100 pr-1e" :style="{ fontSize: 0.875 + 'em' }">priority_high</span>
|
||||
<span class="material-symbols text-red-100 pr-1e" :style="{ fontSize: 0.875 + 'em' }">priority_high</span>
|
||||
</div>
|
||||
</ui-tooltip>
|
||||
|
||||
<!-- 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-icons" :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-icons" :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 -->
|
||||
@@ -132,6 +125,9 @@
|
||||
<widgets-explicit-indicator cy-id="explicitIndicator" v-if="isExplicit" />
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<ui-tooltip v-if="showSubtitles" :text="displaySubtitle" :disabled="!displaySubtitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
|
||||
<p cy-id="subtitle" class="truncate" ref="displaySubtitle" :style="{ fontSize: 0.6 + 'em' }">{{ displaySubtitle }}</p>
|
||||
</ui-tooltip>
|
||||
<p cy-id="line2" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displayLineTwo || ' ' }}</p>
|
||||
<p cy-id="line3" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displaySortLine }}</p>
|
||||
</div>
|
||||
@@ -171,6 +167,7 @@ export default {
|
||||
selected: false,
|
||||
isSelectionMode: false,
|
||||
displayTitleTruncated: false,
|
||||
displaySubtitleTruncated: false,
|
||||
showCoverBg: false
|
||||
}
|
||||
},
|
||||
@@ -197,23 +194,6 @@ export default {
|
||||
// This method returns immediately without waiting for the DOM to update
|
||||
return this.coverWidth
|
||||
},
|
||||
/*
|
||||
cardHeight() {
|
||||
// This method returns immediately without waiting for the DOM to update
|
||||
return this.coverHeight + this.detailsHeight
|
||||
},
|
||||
detailsHeight() {
|
||||
if (!this.isAlternativeBookshelfView) return 0
|
||||
const lineHeight = 1.5
|
||||
const remSize = 16
|
||||
const baseHeight = this.sizeMultiplier * lineHeight * remSize
|
||||
const titleHeight = 0.9 * baseHeight
|
||||
const line2Height = 0.8 * baseHeight
|
||||
const line3Height = this.displaySortLine ? 0.8 * baseHeight : 0
|
||||
const marginHeight = 8 * 2 * this.sizeMultiplier // py-2
|
||||
return titleHeight + line2Height + line3Height + marginHeight
|
||||
},
|
||||
*/
|
||||
sizeMultiplier() {
|
||||
return this.store.getters['user/getSizeMultiplier']
|
||||
},
|
||||
@@ -237,10 +217,7 @@ export default {
|
||||
return this._libraryItem.mediaType
|
||||
},
|
||||
isPodcast() {
|
||||
return this.mediaType === 'podcast'
|
||||
},
|
||||
isMusic() {
|
||||
return this.mediaType === 'music'
|
||||
return this.mediaType === 'podcast' || this.store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast'
|
||||
},
|
||||
isExplicit() {
|
||||
return this.mediaMetadata.explicit || false
|
||||
@@ -260,6 +237,7 @@ export default {
|
||||
return this.mediaMetadata.series
|
||||
},
|
||||
seriesName() {
|
||||
if (this.collapsedSeries?.name) return this.collapsedSeries.name
|
||||
return this.series?.name || null
|
||||
},
|
||||
seriesSequence() {
|
||||
@@ -339,10 +317,16 @@ export default {
|
||||
if (this.collapsedSeries) return ignorePrefix ? this.collapsedSeries.nameIgnorePrefix : this.collapsedSeries.name
|
||||
return ignorePrefix ? this.mediaMetadata.titleIgnorePrefix || '\u00A0' : this.title || '\u00A0'
|
||||
},
|
||||
displaySubtitle() {
|
||||
if (!this.libraryItem) return '\u00A0'
|
||||
if (this.collapsedSeries) return `${this.collapsedSeries.numBooks} ${this.$strings.LabelBooks}`
|
||||
if (this.mediaMetadata.subtitle) return this.mediaMetadata.subtitle
|
||||
if (this.mediaMetadata.seriesName) return this.mediaMetadata.seriesName
|
||||
return ''
|
||||
},
|
||||
displayLineTwo() {
|
||||
if (this.recentEpisode) return this.title
|
||||
if (this.isPodcast) return this.author
|
||||
if (this.isMusic) return this.artist
|
||||
if (this.collapsedSeries) return ''
|
||||
if (this.isAuthorBookshelfView) {
|
||||
return this.mediaMetadata.publishedYear || ''
|
||||
@@ -352,14 +336,14 @@ export default {
|
||||
},
|
||||
displaySortLine() {
|
||||
if (this.collapsedSeries) return null
|
||||
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)
|
||||
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)
|
||||
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt, this.dateFormat)
|
||||
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
||||
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
||||
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
|
||||
if (this.orderBy === 'mtimeMs') return this.$getString('LabelFileModifiedDate', [this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)])
|
||||
if (this.orderBy === 'birthtimeMs') return this.$getString('LabelFileBornDate', [this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)])
|
||||
if (this.orderBy === 'addedAt') return this.$getString('LabelAddedDate', [this.$formatDate(this._libraryItem.addedAt, this.dateFormat)])
|
||||
if (this.orderBy === 'media.duration') return this.$strings.LabelDuration + ': ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
||||
if (this.orderBy === 'size') return this.$strings.LabelSize + ': ' + this.$bytesPretty(this._libraryItem.size)
|
||||
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} ` + this.$strings.LabelEpisodes
|
||||
if (this.orderBy === 'media.metadata.publishedYear') {
|
||||
if (this.mediaMetadata.publishedYear) return 'Published ' + this.mediaMetadata.publishedYear
|
||||
if (this.mediaMetadata.publishedYear) return this.$getString('LabelPublishedDate', [this.mediaMetadata.publishedYear])
|
||||
return '\u00A0'
|
||||
}
|
||||
return null
|
||||
@@ -370,7 +354,6 @@ export default {
|
||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
|
||||
},
|
||||
userProgress() {
|
||||
if (this.isMusic) return null
|
||||
if (this.episodeProgress) return this.episodeProgress
|
||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||
},
|
||||
@@ -426,7 +409,7 @@ export default {
|
||||
return !this.isSelectionMode && !this.showPlayButton && this.ebookFormat
|
||||
},
|
||||
showPlayButton() {
|
||||
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode || this.isMusic)
|
||||
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode)
|
||||
},
|
||||
showSmallEBookIcon() {
|
||||
return !this.isSelectionMode && this.ebookFormat
|
||||
@@ -470,8 +453,6 @@ export default {
|
||||
return this.store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
moreMenuItems() {
|
||||
if (this.isMusic) return []
|
||||
|
||||
if (this.recentEpisode) {
|
||||
const items = [
|
||||
{
|
||||
@@ -644,6 +625,9 @@ export default {
|
||||
},
|
||||
mediaItemShare() {
|
||||
return this._libraryItem.mediaItemShare || null
|
||||
},
|
||||
showSubtitles() {
|
||||
return !this.isPodcast && this.store.getters['user/getUserSetting']('showSubtitles')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -685,6 +669,9 @@ export default {
|
||||
if (this.$refs.displayTitle) {
|
||||
this.displayTitleTruncated = this.$refs.displayTitle.scrollWidth > this.$refs.displayTitle.clientWidth
|
||||
}
|
||||
if (this.$refs.displaySubtitle) {
|
||||
this.displaySubtitleTruncated = this.$refs.displaySubtitle.scrollWidth > this.$refs.displaySubtitle.clientWidth
|
||||
}
|
||||
})
|
||||
},
|
||||
clickCard(e) {
|
||||
@@ -710,7 +697,7 @@ export default {
|
||||
toggleFinished(confirmed = false) {
|
||||
if (!this.itemIsFinished && this.userProgressPercent > 0 && !confirmed) {
|
||||
const payload = {
|
||||
message: `Are you sure you want to mark "${this.displayTitle}" as finished?`,
|
||||
message: this.$getString('MessageConfirmMarkItemFinished', [this.displayTitle]),
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.toggleFinished(true)
|
||||
@@ -755,18 +742,18 @@ export default {
|
||||
.then((data) => {
|
||||
var result = data.result
|
||||
if (!result) {
|
||||
this.$toast.error(`Re-Scan Failed for "${this.title}"`)
|
||||
this.$toast.error(this.$getString('ToastRescanFailed', [this.displayTitle]))
|
||||
} else if (result === 'UPDATED') {
|
||||
this.$toast.success(`Re-Scan complete item was updated`)
|
||||
this.$toast.success(this.$strings.ToastRescanUpdated)
|
||||
} else if (result === 'UPTODATE') {
|
||||
this.$toast.success(`Re-Scan complete item was up to date`)
|
||||
this.$toast.success(this.$strings.ToastRescanUpToDate)
|
||||
} else if (result === 'REMOVED') {
|
||||
this.$toast.error(`Re-Scan complete item was removed`)
|
||||
this.$toast.error(this.$strings.ToastRescanRemoved)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to scan library item', error)
|
||||
this.$toast.error('Failed to scan library item')
|
||||
this.$toast.error(this.$strings.ToastScanFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
@@ -823,7 +810,7 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove series from home', error)
|
||||
this.$toast.error('Failed to update user')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
@@ -841,7 +828,7 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to hide item from home', error)
|
||||
this.$toast.error('Failed to update user')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
@@ -856,7 +843,7 @@ export default {
|
||||
episodeId: this.recentEpisode.id,
|
||||
title: this.recentEpisode.title,
|
||||
subtitle: this.mediaMetadata.title,
|
||||
caption: this.recentEpisode.publishedAt ? `Published ${this.$formatDate(this.recentEpisode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||
caption: this.recentEpisode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(this.recentEpisode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
|
||||
duration: this.recentEpisode.audioFile.duration || null,
|
||||
coverPath: this.media.coverPath || null
|
||||
}
|
||||
@@ -906,11 +893,11 @@ export default {
|
||||
axios
|
||||
.$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`)
|
||||
.then(() => {
|
||||
this.$toast.success('Item deleted')
|
||||
this.$toast.success(this.$strings.ToastItemDeletedSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to delete item', error)
|
||||
this.$toast.error('Failed to delete item')
|
||||
this.$toast.error(this.$strings.ToastItemDeletedFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
@@ -1016,7 +1003,7 @@ export default {
|
||||
episodeId: episode.id,
|
||||
title: episode.title,
|
||||
subtitle: this.mediaMetadata.title,
|
||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||
caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
|
||||
duration: episode.audioFile.duration || null,
|
||||
coverPath: this.media.coverPath || null
|
||||
})
|
||||
|
||||
@@ -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">
|
||||
@@ -7,11 +7,11 @@
|
||||
</div>
|
||||
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
|
||||
<div class="absolute pointer-events-auto" :style="{ top: 0.5 + 'em', right: 0.5 + 'em' }" @click.stop.prevent="clickEdit">
|
||||
<span class="material-icons text-white text-opacity-75 hover:text-opacity-100" :style="{ fontSize: 1.25 + 'em' }">edit</span>
|
||||
<span class="material-symbols text-white text-opacity-75 hover:text-opacity-100" :style="{ fontSize: 1.25 + 'em' }">edit</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 + 'em', left: 0.5 + 'em', fontSize: 1.5 + 'em' }">rss_feed</span>
|
||||
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-symbols text-success" :style="{ top: 0.5 + 'em', left: 0.5 + 'em', fontSize: 1.5 + 'em' }">rss_feed</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6e h-6e rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
|
||||
@@ -57,23 +57,11 @@ export default {
|
||||
return this.store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
cardWidth() {
|
||||
return this.width || this.coverHeight * 2
|
||||
return this.width || (this.coverHeight / this.bookCoverAspectRatio) * 2
|
||||
},
|
||||
coverHeight() {
|
||||
return this.height * this.sizeMultiplier
|
||||
},
|
||||
cardHeight() {
|
||||
return this.coverHeight + this.bottomTextHeight
|
||||
},
|
||||
bottomTextHeight() {
|
||||
if (!this.isAlternativeBookshelfView) return 0 // bottom text appears on top of the divider
|
||||
const lineHeight = 1.5
|
||||
const remSize = 16
|
||||
const baseHeight = this.sizeMultiplier * lineHeight * remSize
|
||||
const titleHeight = this.labelFontSize * baseHeight
|
||||
const paddingHeight = 4 * 2 * this.sizeMultiplier // py-1
|
||||
return titleHeight + paddingHeight
|
||||
},
|
||||
labelFontSize() {
|
||||
if (this.width < 160) return 0.75
|
||||
return 0.9
|
||||
|
||||
@@ -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">
|
||||
@@ -7,7 +7,7 @@
|
||||
</div>
|
||||
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
|
||||
<div class="absolute pointer-events-auto" :style="{ top: 0.5 + 'em', right: 0.5 + 'em' }" @click.stop.prevent="clickEdit">
|
||||
<span class="material-icons text-white text-opacity-75 hover:text-opacity-100" :style="{ fontSize: 1.25 + 'em' }">edit</span>
|
||||
<span class="material-symbols text-white text-opacity-75 hover:text-opacity-100" :style="{ fontSize: 1.25 + 'em' }">edit</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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,16 +7,16 @@
|
||||
</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>
|
||||
|
||||
<span cy-id="rssFeedMarker" v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 + 'em', left: 0.5 + 'em', fontSize: 1.5 + 'em' }">rss_feed</span>
|
||||
<span cy-id="rssFeedMarker" v-if="!isHovering && rssFeed" class="absolute z-10 material-symbols text-success" :style="{ top: 0.5 + 'em', left: 0.5 + 'em', fontSize: 1.5 + 'em' }">rss_feed</span>
|
||||
</div>
|
||||
|
||||
<div cy-id="standardBottomText" v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6e h-6e rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
|
||||
@@ -65,7 +65,7 @@ export default {
|
||||
return this.store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
cardWidth() {
|
||||
return this.width || this.coverHeight * 2
|
||||
return this.width || (this.coverHeight / this.bookCoverAspectRatio) * 2
|
||||
},
|
||||
coverHeight() {
|
||||
return this.height * this.sizeMultiplier
|
||||
@@ -81,22 +81,22 @@ export default {
|
||||
return this.store.getters['user/getSizeMultiplier']
|
||||
},
|
||||
seriesId() {
|
||||
return this.series ? this.series.id : ''
|
||||
return this.series?.id || ''
|
||||
},
|
||||
title() {
|
||||
return this.series ? this.series.name : ''
|
||||
return this.series?.name || ''
|
||||
},
|
||||
nameIgnorePrefix() {
|
||||
return this.series ? this.series.nameIgnorePrefix : ''
|
||||
return this.series?.nameIgnorePrefix || ''
|
||||
},
|
||||
displayTitle() {
|
||||
if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title
|
||||
if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title || '\u00A0'
|
||||
return this.title || '\u00A0'
|
||||
},
|
||||
displaySortLine() {
|
||||
switch (this.orderBy) {
|
||||
case 'addedAt':
|
||||
return `${this.$strings.LabelAdded} ${this.$formatDate(this.addedAt, this.dateFormat)}`
|
||||
return this.$getString('LabelAddedDate', [this.$formatDate(this.addedAt, this.dateFormat)])
|
||||
case 'totalDuration':
|
||||
return `${this.$strings.LabelDuration} ${this.$elapsedPrettyExtended(this.totalDuration, false)}`
|
||||
case 'lastBookUpdated':
|
||||
@@ -110,13 +110,13 @@ export default {
|
||||
}
|
||||
},
|
||||
books() {
|
||||
return this.series ? this.series.books || [] : []
|
||||
return this.series?.books || []
|
||||
},
|
||||
addedAt() {
|
||||
return this.series ? this.series.addedAt : 0
|
||||
return this.series?.addedAt || 0
|
||||
},
|
||||
totalDuration() {
|
||||
return this.series ? this.series.totalDuration : 0
|
||||
return this.series?.totalDuration || 0
|
||||
},
|
||||
seriesBookProgress() {
|
||||
return this.books
|
||||
@@ -161,7 +161,7 @@ export default {
|
||||
return this.bookshelfView == constants.BookshelfView.DETAIL
|
||||
},
|
||||
rssFeed() {
|
||||
return this.series ? this.series.rssFeed : null
|
||||
return this.series?.rssFeed
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(name)}`">
|
||||
<div cy-id="card" :style="{ width: cardWidth + 'px', height: cardHeight + 'px', fontSize: sizeMultiplier + 'rem' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
||||
<div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-40">
|
||||
<span class="material-icons-outlined text-[10em]">record_voice_over</span>
|
||||
<span class="material-symbols text-[10em]"></span>
|
||||
</div>
|
||||
|
||||
<!-- Narrator name & num books overlay -->
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<div class="flex h-full px-1 overflow-hidden">
|
||||
<div class="w-10 h-10 flex items-center justify-center">
|
||||
<span class="material-icons text-2xl text-gray-200">record_voice_over</span>
|
||||
<span class="material-symbols text-2xl text-gray-200"></span>
|
||||
</div>
|
||||
<div class="flex-grow px-2 narratorSearchCardContent h-full">
|
||||
<p class="truncate text-sm">{{ narrator }}</p>
|
||||
<p class="text-xs text-gray-400">{{ $getString('LabelXBooks', [numBooks]) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -12,7 +13,8 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
narrator: String
|
||||
narrator: String,
|
||||
numBooks: Number
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
@@ -26,9 +28,9 @@ export default {
|
||||
<style scoped>
|
||||
.narratorSearchCardContent {
|
||||
width: calc(100% - 40px);
|
||||
height: 40px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
<p class="text-base md:text-lg font-semibold pr-4">{{ eventName }}</p>
|
||||
<div class="flex-grow" />
|
||||
|
||||
<ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" @click.stop="fireTestEventAndSucceed">Fire onTest Event</ui-btn>
|
||||
<ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" color="red-600" @click.stop="fireTestEventAndFail">Fire & Fail</ui-btn>
|
||||
<ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" @click.stop="fireTestEventAndSucceed">{{ this.$strings.ButtonFireOnTest }}</ui-btn>
|
||||
<ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" color="red-600" @click.stop="fireTestEventAndFail">{{ this.$strings.ButtonFireAndFail }}</ui-btn>
|
||||
<!-- <ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" @click.stop="rapidFireTestEvents">Rapid Fire</ui-btn> -->
|
||||
<ui-btn v-else-if="notification.enabled" :loading="sendingTest" small class="mr-2" @click.stop="sendTestClick">Test</ui-btn>
|
||||
<ui-btn v-else :loading="enabling" small color="success" class="mr-2" @click="enableNotification">Enable</ui-btn>
|
||||
<ui-btn v-else-if="notification.enabled" :loading="sendingTest" small class="mr-2" @click.stop="sendTestClick">{{ this.$strings.ButtonTest }}</ui-btn>
|
||||
<ui-btn v-else :loading="enabling" small color="success" class="mr-2" @click="enableNotification">{{ this.$strings.ButtonEnable }}</ui-btn>
|
||||
|
||||
<ui-icon-btn :size="7" icon-font-size="1.1rem" icon="edit" class="mr-2" @click="editNotification" />
|
||||
<ui-icon-btn bg-color="error" :size="7" icon-font-size="1.2rem" icon="delete" @click="deleteNotificationClick" />
|
||||
@@ -65,12 +65,12 @@ export default {
|
||||
this.$axios
|
||||
.$get(`/api/notifications/test?fail=${intentionallyFail ? 1 : 0}`)
|
||||
.then(() => {
|
||||
this.$toast.success('Triggered onTest Event')
|
||||
this.$toast.success(this.$strings.ToastNotificationTestTriggerSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
const errorMsg = error.response ? error.response.data : null
|
||||
this.$toast.error(`Failed: ${errorMsg}` || 'Failed to trigger onTest event')
|
||||
this.$toast.error(`Failed: ${errorMsg}` || this.$strings.ToastNotificationTestTriggerFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.testing = false
|
||||
@@ -91,7 +91,7 @@ export default {
|
||||
// End testing functions
|
||||
sendTestClick() {
|
||||
const payload = {
|
||||
message: `Trigger this notification with test data?`,
|
||||
message: this.$strings.MessageConfirmNotificationTestTrigger,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.sendTest()
|
||||
@@ -106,12 +106,12 @@ export default {
|
||||
this.$axios
|
||||
.$get(`/api/notifications/${this.notification.id}/test`)
|
||||
.then(() => {
|
||||
this.$toast.success('Triggered test notification')
|
||||
this.$toast.success(this.$strings.ToastNotificationTestTriggerSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
const errorMsg = error.response ? error.response.data : null
|
||||
this.$toast.error(`Failed: ${errorMsg}` || 'Failed to trigger test notification')
|
||||
this.$toast.error(`Failed: ${errorMsg}` || this.$strings.ToastNotificationTestTriggerFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.sendingTest = false
|
||||
@@ -127,11 +127,10 @@ export default {
|
||||
.$patch(`/api/notifications/${this.notification.id}`, payload)
|
||||
.then((updatedSettings) => {
|
||||
this.$emit('update', updatedSettings)
|
||||
this.$toast.success('Notification enabled')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update notification', error)
|
||||
this.$toast.error('Failed to update notification')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.enabling = false
|
||||
@@ -139,7 +138,7 @@ export default {
|
||||
},
|
||||
deleteNotificationClick() {
|
||||
const payload = {
|
||||
message: `Are you sure you want to delete this notification?`,
|
||||
message: this.$strings.MessageConfirmDeleteNotification,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.deleteNotification()
|
||||
@@ -155,11 +154,10 @@ export default {
|
||||
.$delete(`/api/notifications/${this.notification.id}`)
|
||||
.then((updatedSettings) => {
|
||||
this.$emit('update', updatedSettings)
|
||||
this.$toast.success('Deleted notification')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
this.$toast.error('Failed to delete notification')
|
||||
this.$toast.error(this.$strings.ToastNotificationDeleteFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.deleting = false
|
||||
@@ -171,4 +169,4 @@ export default {
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<div class="flex h-full px-1 overflow-hidden">
|
||||
<div class="w-10 h-10 flex items-center justify-center">
|
||||
<span class="material-icons text-2xl text-gray-200">local_offer</span>
|
||||
<span class="material-symbols text-2xl text-gray-200">local_offer</span>
|
||||
</div>
|
||||
<div class="flex-grow px-2 tagSearchCardContent h-full">
|
||||
<p class="truncate text-sm">{{ tag }}</p>
|
||||
<p class="text-xs text-gray-400">{{ $getString('LabelXItems', [numItems]) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -12,7 +13,8 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
tag: String
|
||||
tag: String,
|
||||
numItems: Number
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
@@ -26,9 +28,9 @@ export default {
|
||||
<style>
|
||||
.tagSearchCardContent {
|
||||
width: calc(100% - 40px);
|
||||
height: 40px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -27,38 +27,6 @@
|
||||
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=publishers.${$encode(publisher)}`" class="hover:underline">{{ publisher }}</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="musicAlbum" class="flex py-0.5">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">Album</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ musicAlbum }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="musicAlbumArtist" class="flex py-0.5">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">Album Artist</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ musicAlbumArtist }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="musicTrackPretty" class="flex py-0.5">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">Track</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ musicTrackPretty }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="musicDiscPretty" class="flex py-0.5">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">Disc</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ musicDiscPretty }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="podcastType" class="flex py-0.5">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span>
|
||||
@@ -97,7 +65,7 @@
|
||||
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=languages.${$encode(language)}`" class="hover:underline">{{ language }}</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
|
||||
<div v-if="tracks.length || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
||||
</div>
|
||||
@@ -134,10 +102,6 @@ export default {
|
||||
isPodcast() {
|
||||
return this.libraryItem.mediaType === 'podcast'
|
||||
},
|
||||
audioFile() {
|
||||
// Music track
|
||||
return this.media.audioFile
|
||||
},
|
||||
media() {
|
||||
return this.libraryItem.media || {}
|
||||
},
|
||||
@@ -168,25 +132,6 @@ export default {
|
||||
publisher() {
|
||||
return this.mediaMetadata.publisher || ''
|
||||
},
|
||||
musicArtists() {
|
||||
return this.mediaMetadata.artists || []
|
||||
},
|
||||
musicAlbum() {
|
||||
return this.mediaMetadata.album || ''
|
||||
},
|
||||
musicAlbumArtist() {
|
||||
return this.mediaMetadata.albumArtist || ''
|
||||
},
|
||||
musicTrackPretty() {
|
||||
if (!this.mediaMetadata.trackNumber) return null
|
||||
if (!this.mediaMetadata.trackTotal) return this.mediaMetadata.trackNumber
|
||||
return `${this.mediaMetadata.trackNumber} / ${this.mediaMetadata.trackTotal}`
|
||||
},
|
||||
musicDiscPretty() {
|
||||
if (!this.mediaMetadata.discNumber) return null
|
||||
if (!this.mediaMetadata.discTotal) return this.mediaMetadata.discNumber
|
||||
return `${this.mediaMetadata.discNumber} / ${this.mediaMetadata.discTotal}`
|
||||
},
|
||||
narrators() {
|
||||
return this.mediaMetadata.narrators || []
|
||||
},
|
||||
@@ -220,4 +165,4 @@ export default {
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</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-300" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
|
||||
<span class="material-icons" style="font-size: 1.1rem">close</span>
|
||||
<span class="material-symbols" style="font-size: 1.1rem">close</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<!-- selected checkmark icon -->
|
||||
<div v-if="item.value === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
|
||||
<span class="material-icons text-base text-yellow-400">check</span>
|
||||
<span class="material-symbols text-base text-yellow-400">check</span>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<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">
|
||||
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
|
||||
<span v-else class="material-icons" style="font-size: 1.2rem">close</span>
|
||||
</div>
|
||||
<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>
|
||||
</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">
|
||||
<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">
|
||||
<li v-if="isTyping" class="py-2 px-2">
|
||||
<p>{{ $strings.MessageThinking }}</p>
|
||||
@@ -25,7 +25,7 @@
|
||||
<template v-for="item in bookResults">
|
||||
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||
<nuxt-link :to="`/item/${item.libraryItem.id}`">
|
||||
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
|
||||
<cards-item-search-card :library-item="item.libraryItem" />
|
||||
</nuxt-link>
|
||||
</li>
|
||||
</template>
|
||||
@@ -34,7 +34,7 @@
|
||||
<template v-for="item in podcastResults">
|
||||
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||
<nuxt-link :to="`/item/${item.libraryItem.id}`">
|
||||
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
|
||||
<cards-item-search-card :library-item="item.libraryItem" />
|
||||
</nuxt-link>
|
||||
</li>
|
||||
</template>
|
||||
@@ -42,7 +42,7 @@
|
||||
<p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelAuthors }}</p>
|
||||
<template v-for="item in authorResults">
|
||||
<li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(item.id)}`">
|
||||
<nuxt-link :to="`/author/${item.id}`">
|
||||
<cards-author-search-card :author="item" />
|
||||
</nuxt-link>
|
||||
</li>
|
||||
@@ -59,9 +59,18 @@
|
||||
|
||||
<p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelTags }}</p>
|
||||
<template v-for="item in tagResults">
|
||||
<li :key="item.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||
<li :key="`tag.${item.name}`" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(item.name)}`">
|
||||
<cards-tag-search-card :tag="item.name" />
|
||||
<cards-tag-search-card :tag="item.name" :num-items="item.numItems" />
|
||||
</nuxt-link>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<p v-if="genreResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelGenres }}</p>
|
||||
<template v-for="item in genreResults">
|
||||
<li :key="`genre.${item.name}`" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=genres.${$encode(item.name)}`">
|
||||
<cards-genre-search-card :genre="item.name" :num-items="item.numItems" />
|
||||
</nuxt-link>
|
||||
</li>
|
||||
</template>
|
||||
@@ -70,7 +79,7 @@
|
||||
<template v-for="narrator in narratorResults">
|
||||
<li :key="narrator.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(narrator.name)}`">
|
||||
<cards-narrator-search-card :narrator="narrator.name" />
|
||||
<cards-narrator-search-card :narrator="narrator.name" :num-books="narrator.numBooks" />
|
||||
</nuxt-link>
|
||||
</li>
|
||||
</template>
|
||||
@@ -95,6 +104,7 @@ export default {
|
||||
authorResults: [],
|
||||
seriesResults: [],
|
||||
tagResults: [],
|
||||
genreResults: [],
|
||||
narratorResults: [],
|
||||
searchTimeout: null,
|
||||
lastSearch: null
|
||||
@@ -105,7 +115,7 @@ export default {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
totalResults() {
|
||||
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length + this.podcastResults.length + this.narratorResults.length
|
||||
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length + this.genreResults.length + this.podcastResults.length + this.narratorResults.length
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -116,7 +126,7 @@ export default {
|
||||
if (!this.search) return
|
||||
var search = this.search
|
||||
this.clearResults()
|
||||
this.$router.push(`/library/${this.currentLibraryId}/search?q=${search}`)
|
||||
this.$router.push(`/library/${this.currentLibraryId}/search?q=${encodeURIComponent(search)}`)
|
||||
},
|
||||
clearResults() {
|
||||
this.search = null
|
||||
@@ -126,6 +136,7 @@ export default {
|
||||
this.authorResults = []
|
||||
this.seriesResults = []
|
||||
this.tagResults = []
|
||||
this.genreResults = []
|
||||
this.narratorResults = []
|
||||
this.showMenu = false
|
||||
this.isFetching = false
|
||||
@@ -146,7 +157,7 @@ export default {
|
||||
clearTimeout(this.focusTimeout)
|
||||
this.focusTimeout = setTimeout(() => {
|
||||
this.showMenu = false
|
||||
}, 200)
|
||||
}, 100)
|
||||
},
|
||||
async runSearch(value) {
|
||||
this.lastSearch = value
|
||||
@@ -155,7 +166,7 @@ export default {
|
||||
}
|
||||
this.isFetching = true
|
||||
|
||||
const searchResults = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/search?q=${value}&limit=3`).catch((error) => {
|
||||
const searchResults = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/search?q=${encodeURIComponent(value)}&limit=3`).catch((error) => {
|
||||
console.error('Search error', error)
|
||||
return []
|
||||
})
|
||||
@@ -168,6 +179,7 @@ export default {
|
||||
this.authorResults = searchResults.authors || []
|
||||
this.seriesResults = searchResults.series || []
|
||||
this.tagResults = searchResults.tags || []
|
||||
this.genreResults = searchResults.genres || []
|
||||
this.narratorResults = searchResults.narrators || []
|
||||
|
||||
this.isFetching = false
|
||||
@@ -203,4 +215,4 @@ export default {
|
||||
.globalSearchMenu {
|
||||
max-height: calc(100vh - 75px);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,58 +1,60 @@
|
||||
<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">
|
||||
<span class="material-icons" style="font-size: 1.1rem">close</span>
|
||||
</div>
|
||||
</button>
|
||||
<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>
|
||||
</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-icons 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">
|
||||
<span class="material-icons text-base text-yellow-400">check</span>
|
||||
<span class="material-symbols text-base text-yellow-400">check</span>
|
||||
</div>
|
||||
</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-icons text-2xl">arrow_left</span>
|
||||
<span class="material-symbols text-2xl">arrow_left</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<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>
|
||||
<!-- selected checkmark icon -->
|
||||
<div v-if="`${sublist}.${item.value}` === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
|
||||
<span class="material-icons text-base text-yellow-400">check</span>
|
||||
<span class="material-symbols text-base text-yellow-400">check</span>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
@@ -98,9 +100,6 @@ export default {
|
||||
isPodcast() {
|
||||
return this.libraryMediaType === 'podcast'
|
||||
},
|
||||
isMusic() {
|
||||
return this.libraryMediaType === 'music'
|
||||
},
|
||||
seriesItems() {
|
||||
return [
|
||||
{
|
||||
@@ -192,6 +191,12 @@ export default {
|
||||
value: 'publishers',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelPublishedDecade,
|
||||
textPlural: this.$strings.LabelPublishedDecades,
|
||||
value: 'publishedDecades',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelLanguage,
|
||||
textPlural: this.$strings.LabelLanguages,
|
||||
@@ -274,35 +279,9 @@ export default {
|
||||
}
|
||||
]
|
||||
},
|
||||
musicItems() {
|
||||
return [
|
||||
{
|
||||
text: this.$strings.LabelAll,
|
||||
value: 'all'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelGenre,
|
||||
textPlural: this.$strings.LabelGenres,
|
||||
value: 'genres',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelTag,
|
||||
textPlural: this.$strings.LabelTags,
|
||||
value: 'tags',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.ButtonIssues,
|
||||
value: 'issues',
|
||||
sublist: false
|
||||
}
|
||||
]
|
||||
},
|
||||
selectItems() {
|
||||
if (this.isSeries) return this.seriesItems
|
||||
if (this.isPodcast) return this.podcastItems
|
||||
if (this.isMusic) return this.musicItems
|
||||
return this.bookItems
|
||||
},
|
||||
selectedItemSublist() {
|
||||
@@ -367,6 +346,9 @@ export default {
|
||||
publishers() {
|
||||
return this.filterData.publishers || []
|
||||
},
|
||||
publishedDecades() {
|
||||
return this.filterData.publishedDecades || []
|
||||
},
|
||||
progress() {
|
||||
return [
|
||||
{
|
||||
@@ -433,21 +415,17 @@ export default {
|
||||
id: 'isbn',
|
||||
name: 'ISBN'
|
||||
},
|
||||
{
|
||||
id: 'subtitle',
|
||||
name: this.$strings.LabelSubtitle
|
||||
},
|
||||
{
|
||||
id: 'authors',
|
||||
name: this.$strings.LabelAuthor
|
||||
},
|
||||
{
|
||||
id: 'publishedYear',
|
||||
name: this.$strings.LabelPublishYear
|
||||
id: 'chapters',
|
||||
name: this.$strings.LabelChapters
|
||||
},
|
||||
{
|
||||
id: 'series',
|
||||
name: this.$strings.LabelSeries
|
||||
id: 'cover',
|
||||
name: this.$strings.LabelCover
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
@@ -458,24 +436,32 @@ export default {
|
||||
name: this.$strings.LabelGenres
|
||||
},
|
||||
{
|
||||
id: 'tags',
|
||||
name: this.$strings.LabelTags
|
||||
id: 'language',
|
||||
name: this.$strings.LabelLanguage
|
||||
},
|
||||
{
|
||||
id: 'narrators',
|
||||
name: this.$strings.LabelNarrator
|
||||
},
|
||||
{
|
||||
id: 'publishedYear',
|
||||
name: this.$strings.LabelPublishYear
|
||||
},
|
||||
{
|
||||
id: 'publisher',
|
||||
name: this.$strings.LabelPublisher
|
||||
},
|
||||
{
|
||||
id: 'language',
|
||||
name: this.$strings.LabelLanguage
|
||||
id: 'series',
|
||||
name: this.$strings.LabelSeries
|
||||
},
|
||||
{
|
||||
id: 'cover',
|
||||
name: this.$strings.LabelCover
|
||||
id: 'subtitle',
|
||||
name: this.$strings.LabelSubtitle
|
||||
},
|
||||
{
|
||||
id: 'tags',
|
||||
name: this.$strings.LabelTags
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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-icons 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-icons 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>
|
||||
@@ -56,9 +56,6 @@ export default {
|
||||
isPodcast() {
|
||||
return this.libraryMediaType === 'podcast'
|
||||
},
|
||||
isMusic() {
|
||||
return this.libraryMediaType === 'music'
|
||||
},
|
||||
podcastItems() {
|
||||
return [
|
||||
{
|
||||
@@ -88,6 +85,10 @@ export default {
|
||||
{
|
||||
text: this.$strings.LabelFileModified,
|
||||
value: 'mtimeMs'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelRandomly,
|
||||
value: 'random'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -128,6 +129,10 @@ export default {
|
||||
{
|
||||
text: this.$strings.LabelFileModified,
|
||||
value: 'mtimeMs'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelRandomly,
|
||||
value: 'random'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -140,40 +145,10 @@ export default {
|
||||
}
|
||||
]
|
||||
},
|
||||
musicItems() {
|
||||
return [
|
||||
{
|
||||
text: this.$strings.LabelTitle,
|
||||
value: 'media.metadata.title'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelAddedAt,
|
||||
value: 'addedAt'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelSize,
|
||||
value: 'size'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelDuration,
|
||||
value: 'media.duration'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelFileBirthtime,
|
||||
value: 'birthtimeMs'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelFileModified,
|
||||
value: 'mtimeMs'
|
||||
}
|
||||
]
|
||||
},
|
||||
selectItems() {
|
||||
let items = null
|
||||
if (this.isPodcast) {
|
||||
items = this.podcastItems
|
||||
} else if (this.isMusic) {
|
||||
items = this.musicItems
|
||||
} else if (this.$store.getters['user/getUserSetting']('filterBy').startsWith('series.')) {
|
||||
items = this.seriesItems
|
||||
} else {
|
||||
@@ -215,4 +190,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<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="font-mono uppercase 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-20 z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }">
|
||||
<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' }">
|
||||
<div class="arrow-down" />
|
||||
</div>
|
||||
@@ -11,15 +11,15 @@
|
||||
<template v-for="rate in rates">
|
||||
<div :key="rate" class="h-full border-black-300 w-11 cursor-pointer border rounded-sm" :class="value === rate ? 'bg-black-100' : 'hover:bg-black hover:bg-opacity-10'" style="min-width: 44px; max-width: 44px" @click="set(rate)">
|
||||
<div class="w-full h-full flex justify-center items-center">
|
||||
<p class="text-xs text-center font-mono">{{ rate }}<span class="text-sm">x</span></p>
|
||||
<p class="text-xs text-center">{{ rate }}<span class="text-sm">x</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="w-full py-1 px-4">
|
||||
<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() {
|
||||
@@ -41,7 +45,7 @@ export default {
|
||||
currentPlaybackRate: 0,
|
||||
MIN_SPEED: 0.5,
|
||||
MAX_SPEED: 10,
|
||||
menuLeft: -92,
|
||||
menuLeft: -96,
|
||||
arrowLeft: 0
|
||||
}
|
||||
},
|
||||
@@ -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
|
||||
@@ -89,9 +100,9 @@ export default {
|
||||
if (boundingBox.left + 110 > window.innerWidth - 10) {
|
||||
this.menuLeft = window.innerWidth - 230 - boundingBox.left
|
||||
|
||||
this.arrowLeft = Math.abs(this.menuLeft) - 92
|
||||
this.arrowLeft = Math.abs(this.menuLeft) - 96
|
||||
} else {
|
||||
this.menuLeft = -92
|
||||
this.menuLeft = -96
|
||||
this.arrowLeft = 0
|
||||
}
|
||||
},
|
||||
@@ -109,4 +120,4 @@ export default {
|
||||
this.currentPlaybackRate = this.playbackRate
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -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-icons 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-icons 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,13 +1,13 @@
|
||||
<template>
|
||||
<div class="relative" v-click-outside="clickOutside" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<button :aria-label="$strings.LabelVolume" class="text-gray-300 hover:text-white" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon">
|
||||
<span class="material-icons text-2xl sm:text-3xl">{{ volumeIcon }}</span>
|
||||
<span class="material-symbols text-2xl sm:text-3xl">{{ volumeIcon }}</span>
|
||||
</button>
|
||||
<transition name="menux">
|
||||
<div v-show="isOpen" class="volumeMenu h-6 absolute bottom-2 w-28 px-2 bg-bg shadow-sm rounded-lg" style="left: -116px">
|
||||
<div ref="volumeTrack" class="h-1 w-full bg-gray-500 my-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack">
|
||||
<div class="bg-gray-100 h-full absolute left-0 top-0 pointer-events-none rounded-full" :style="{ width: volume * trackWidth + 'px' }" />
|
||||
<div class="w-2.5 h-2.5 bg-white shadow-sm rounded-full absolute pointer-events-none" :class="isDragging ? 'transform scale-125 origin-center' : ''" :style="{ left: cursorLeft + 'px', top: '-3px' }" />
|
||||
<div v-show="isOpen" class="volumeMenu h-28 absolute bottom-2 w-6 py-2 bg-bg shadow-sm rounded-lg" style="top: -116px">
|
||||
<div ref="volumeTrack" class="w-1 h-full bg-gray-500 mx-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack">
|
||||
<div class="bg-gray-100 w-full absolute left-0 bottom-0 pointer-events-none rounded-full" :style="{ height: volume * trackHeight + 'px' }" />
|
||||
<div class="w-2.5 h-2.5 bg-white shadow-sm rounded-full absolute pointer-events-none" :class="isDragging ? 'transform scale-125 origin-center' : ''" :style="{ bottom: cursorBottom + 'px', left: '-3px' }" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
@@ -24,10 +24,10 @@ export default {
|
||||
isOpen: false,
|
||||
isDragging: false,
|
||||
isHovering: false,
|
||||
posX: 0,
|
||||
posY: 0,
|
||||
lastValue: 0.5,
|
||||
isMute: false,
|
||||
trackWidth: 112 - 20,
|
||||
trackHeight: 112 - 20,
|
||||
openTimeout: null
|
||||
}
|
||||
},
|
||||
@@ -45,9 +45,9 @@ export default {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
cursorLeft() {
|
||||
var left = this.trackWidth * this.volume
|
||||
return left - 3
|
||||
cursorBottom() {
|
||||
var bottom = this.trackHeight * this.volume
|
||||
return bottom - 3
|
||||
},
|
||||
volumeIcon() {
|
||||
if (this.volume <= 0) return 'volume_mute'
|
||||
@@ -89,17 +89,10 @@ export default {
|
||||
}, 600)
|
||||
},
|
||||
mousemove(e) {
|
||||
var diff = this.posX - e.x
|
||||
this.posX = e.x
|
||||
var volShift = 0
|
||||
if (diff < 0) {
|
||||
// Volume up
|
||||
volShift = diff / this.trackWidth
|
||||
} else {
|
||||
// volume down
|
||||
volShift = diff / this.trackWidth
|
||||
}
|
||||
var newVol = this.volume - volShift
|
||||
var diff = this.posY - e.y
|
||||
this.posY = e.y
|
||||
var volShift = diff / this.trackHeight
|
||||
var newVol = this.volume + volShift
|
||||
newVol = Math.min(Math.max(0, newVol), 1)
|
||||
this.volume = newVol
|
||||
e.preventDefault()
|
||||
@@ -113,8 +106,8 @@ export default {
|
||||
},
|
||||
mousedownTrack(e) {
|
||||
this.isDragging = true
|
||||
this.posX = e.x
|
||||
var vol = e.offsetX / this.trackWidth
|
||||
this.posY = e.y
|
||||
var vol = 1 - e.offsetY / this.trackHeight
|
||||
vol = Math.min(Math.max(vol, 0), 1)
|
||||
this.volume = vol
|
||||
document.body.addEventListener('mousemove', this.mousemove)
|
||||
@@ -137,7 +130,7 @@ export default {
|
||||
this.clickVolumeIcon()
|
||||
},
|
||||
clickVolumeTrack(e) {
|
||||
var vol = e.offsetX / this.trackWidth
|
||||
var vol = 1 - e.offsetY / this.trackHeight
|
||||
vol = Math.min(Math.max(vol, 0), 1)
|
||||
this.volume = vol
|
||||
}
|
||||
@@ -147,7 +140,7 @@ export default {
|
||||
this.isMute = true
|
||||
}
|
||||
const storageVolume = localStorage.getItem('volume')
|
||||
if (storageVolume) {
|
||||
if (storageVolume && !isNaN(storageVolume)) {
|
||||
this.volume = parseFloat(storageVolume)
|
||||
}
|
||||
},
|
||||
@@ -157,4 +150,4 @@ export default {
|
||||
document.body.removeEventListener('mouseup', this.mouseup)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -56,24 +56,15 @@ export default {
|
||||
},
|
||||
imgSrc() {
|
||||
if (!this.imagePath) return null
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// Testing
|
||||
return `http://localhost:3333${this.$config.routerBasePath}/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
||||
}
|
||||
return `/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
||||
return `${this.$config.routerBasePath}/api/authors/${this.authorId}/image?ts=${this.updatedAt}`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
imageLoaded() {
|
||||
var aspectRatio = 1.25
|
||||
if (this.$refs.wrapper) {
|
||||
aspectRatio = this.$refs.wrapper.clientHeight / this.$refs.wrapper.clientWidth
|
||||
}
|
||||
if (this.$refs.img) {
|
||||
var { naturalWidth, naturalHeight } = this.$refs.img
|
||||
var imgAr = naturalHeight / naturalWidth
|
||||
var arDiff = Math.abs(imgAr - aspectRatio)
|
||||
if (arDiff > 0.15) {
|
||||
if (imgAr < 0.5 || imgAr > 2) {
|
||||
this.showCoverBg = true
|
||||
} else {
|
||||
this.showCoverBg = false
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<img ref="cover" :src="cover" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0" :class="showCoverBg ? 'object-contain' : 'object-fill'" />
|
||||
|
||||
<a v-if="!imageFailed && showOpenNewTab && isHovering" :href="cover" @click.stop target="_blank" class="absolute bg-primary flex items-center justify-center shadow-sm rounded-full hover:scale-110 transform duration-100" :style="{ top: sizeMultiplier * 0.5 + 'rem', right: sizeMultiplier * 0.5 + 'rem', width: 2.5 * sizeMultiplier + 'rem', height: 2.5 * sizeMultiplier + 'rem' }">
|
||||
<span class="material-icons" :style="{ fontSize: sizeMultiplier * 1.75 + 'rem' }">open_in_new</span>
|
||||
<span class="material-symbols" :style="{ fontSize: sizeMultiplier * 1.75 + 'rem' }">open_in_new</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -69,6 +69,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center my-2 max-w-md">
|
||||
<div class="w-1/2">
|
||||
<p id="ereader-permissions-toggle">{{ $strings.LabelPermissionsCreateEreader }}</p>
|
||||
</div>
|
||||
<div class="w-1/2">
|
||||
<ui-toggle-switch labeledBy="ereader-permissions-toggle" v-model="newUser.permissions.createEreader" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center my-2 max-w-md">
|
||||
<div class="w-1/2">
|
||||
<p id="explicit-content-permissions-toggle">{{ $strings.LabelPermissionsAccessExplicitContent }}</p>
|
||||
@@ -111,7 +120,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex pt-4 px-2">
|
||||
<ui-btn v-if="hasOpenIDLink" small :loading="unlinkingFromOpenID" color="primary" type="button" class="mr-2" @click.stop="unlinkOpenID">Unlink OpenID</ui-btn>
|
||||
<ui-btn v-if="hasOpenIDLink" small :loading="unlinkingFromOpenID" color="primary" type="button" class="mr-2" @click.stop="unlinkOpenID">{{ $strings.ButtonUnlinkOpenId }}</ui-btn>
|
||||
<ui-btn v-if="isEditingRoot" small class="flex items-center" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
@@ -212,19 +221,19 @@ export default {
|
||||
},
|
||||
unlinkOpenID() {
|
||||
const payload = {
|
||||
message: 'Are you sure you want to unlink this user from OpenID?',
|
||||
message: this.$strings.MessageConfirmUnlinkOpenId,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.unlinkingFromOpenID = true
|
||||
this.$axios
|
||||
.$patch(`/api/users/${this.account.id}/openid-unlink`)
|
||||
.then(() => {
|
||||
this.$toast.success('User unlinked from OpenID')
|
||||
this.$toast.success(this.$strings.ToastUnlinkOpenIdSuccess)
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to unlink user from OpenID', error)
|
||||
this.$toast.error('Failed to unlink user from OpenID')
|
||||
this.$toast.error(this.$strings.ToastUnlinkOpenIdFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.unlinkingFromOpenID = false
|
||||
@@ -265,15 +274,15 @@ export default {
|
||||
},
|
||||
submitForm() {
|
||||
if (!this.newUser.username) {
|
||||
this.$toast.error('Enter a username')
|
||||
this.$toast.error(this.$strings.ToastNewUserUsernameError)
|
||||
return
|
||||
}
|
||||
if (!this.newUser.permissions.accessAllLibraries && !this.newUser.librariesAccessible.length) {
|
||||
this.$toast.error('Must select at least one library')
|
||||
this.$toast.error(this.$strings.ToastNewUserLibraryError)
|
||||
return
|
||||
}
|
||||
if (!this.newUser.permissions.accessAllTags && !this.newUser.itemTagsSelected.length) {
|
||||
this.$toast.error('Must select at least one tag')
|
||||
this.$toast.error(this.$strings.ToastNewUserTagError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -296,7 +305,7 @@ export default {
|
||||
.then((data) => {
|
||||
this.processing = false
|
||||
if (data.error) {
|
||||
this.$toast.error(`${this.$strings.ToastAccountUpdateFailed}: ${data.error}`)
|
||||
this.$toast.error(`${this.$strings.ToastFailedToUpdate}: ${data.error}`)
|
||||
} else {
|
||||
console.log('Account updated', data.user)
|
||||
|
||||
@@ -313,12 +322,12 @@ export default {
|
||||
this.processing = false
|
||||
console.error('Failed to update account', error)
|
||||
var errMsg = error.response ? error.response.data || '' : ''
|
||||
this.$toast.error(errMsg || 'Failed to update account')
|
||||
this.$toast.error(errMsg || this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
},
|
||||
submitCreateAccount() {
|
||||
if (!this.newUser.password) {
|
||||
this.$toast.error('Must have a password, only root user can have an empty password')
|
||||
this.$toast.error(this.$strings.ToastNewUserPasswordError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -329,9 +338,9 @@ export default {
|
||||
.then((data) => {
|
||||
this.processing = false
|
||||
if (data.error) {
|
||||
this.$toast.error(`Failed to create account: ${data.error}`)
|
||||
this.$toast.error(this.$strings.ToastNewUserCreatedFailed + ': ' + data.error)
|
||||
} else {
|
||||
this.$toast.success('New account created')
|
||||
this.$toast.success(this.$strings.ToastNewUserCreatedSuccess)
|
||||
this.show = false
|
||||
}
|
||||
})
|
||||
@@ -351,9 +360,11 @@ export default {
|
||||
update: type === 'admin',
|
||||
delete: type === 'admin',
|
||||
upload: type === 'admin',
|
||||
accessExplicitContent: type === 'admin',
|
||||
accessAllLibraries: true,
|
||||
accessAllTags: true,
|
||||
selectedTagsNotAccessible: false
|
||||
selectedTagsNotAccessible: false,
|
||||
createEreader: type === 'admin'
|
||||
}
|
||||
},
|
||||
init() {
|
||||
@@ -385,7 +396,9 @@ export default {
|
||||
upload: false,
|
||||
accessAllLibraries: true,
|
||||
accessAllTags: true,
|
||||
selectedTagsNotAccessible: false
|
||||
accessExplicitContent: false,
|
||||
selectedTagsNotAccessible: false,
|
||||
createEreader: false
|
||||
},
|
||||
librariesAccessible: [],
|
||||
itemTagsSelected: []
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<modals-modal ref="modal" v-model="show" name="custom-metadata-provider" :width="600" :height="'unset'" :processing="processing">
|
||||
<template #outer>
|
||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||
<p class="text-3xl text-white truncate">Add custom metadata provider</p>
|
||||
<p class="text-3xl text-white truncate">{{ $strings.HeaderAddCustomMetadataProvider }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<form @submit.prevent="submitForm">
|
||||
@@ -20,7 +20,7 @@
|
||||
<ui-text-input-with-label v-model="newUrl" label="URL" />
|
||||
</div>
|
||||
<div class="w-full mb-2 p-1">
|
||||
<ui-text-input-with-label v-model="newAuthHeaderValue" :label="'Authorization Header Value'" type="password" />
|
||||
<ui-text-input-with-label v-model="newAuthHeaderValue" :label="$strings.LabelProviderAuthorizationValue" type="password" />
|
||||
</div>
|
||||
<div class="flex px-1 pt-4">
|
||||
<div class="flex-grow" />
|
||||
@@ -67,7 +67,7 @@ export default {
|
||||
methods: {
|
||||
submitForm() {
|
||||
if (!this.newName || !this.newUrl) {
|
||||
this.$toast.error('Must add name and url')
|
||||
this.$toast.error(this.$strings.ToastProviderNameAndUrlRequired)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -81,13 +81,13 @@ export default {
|
||||
})
|
||||
.then((data) => {
|
||||
this.$emit('added', data.provider)
|
||||
this.$toast.success('New provider added')
|
||||
this.$toast.success(this.$strings.ToastProviderCreatedSuccess)
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMsg = error.response?.data || 'Unknown error'
|
||||
console.error('Failed to add provider', error)
|
||||
this.$toast.error('Failed to add provider: ' + errorMsg)
|
||||
this.$toast.error(this.$strings.ToastProviderCreatedFailed + ': ' + errorMsg)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-base text-gray-200 truncate">{{ metadata.filename }}</p>
|
||||
<ui-btn v-if="ffprobeData" small class="ml-2" @click="ffprobeData = null">{{ $strings.ButtonReset }}</ui-btn>
|
||||
<ui-btn v-else-if="userIsAdminOrUp" small :loading="probingFile" class="ml-2" @click="getFFProbeData">Probe Audio File</ui-btn>
|
||||
<ui-btn v-else-if="userIsAdminOrUp" small :loading="probingFile" class="ml-2" @click="getFFProbeData">{{ $strings.ButtonProbeAudioFile }}</ui-btn>
|
||||
</div>
|
||||
|
||||
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
|
||||
@@ -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-icons">{{ 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
|
||||
}
|
||||
}
|
||||
@@ -159,14 +158,19 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to get ffprobe data', error)
|
||||
this.$toast.error('FFProbe failed')
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
})
|
||||
.finally(() => {
|
||||
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() {}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<widgets-cron-expression-builder ref="expressionBuilder" v-model="newCronExpression" @input="expressionUpdated" />
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<ui-btn :disabled="!isUpdated" @click="submit">{{ isUpdated ? $strings.ButtonSave : $strings.MessageNoUpdateNecessary }}</ui-btn>
|
||||
<ui-btn :disabled="!isUpdated" @click="submit">{{ isUpdated ? $strings.ButtonSave : $strings.MessageNoUpdatesWereNecessary }}</ui-btn>
|
||||
</div>
|
||||
</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 py-4">
|
||||
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
|
||||
<div class="flex px-8 items-center py-2">
|
||||
@@ -19,7 +19,7 @@
|
||||
<ui-tooltip :text="$strings.LabelUpdateCoverHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.LabelUpdateCover }}
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -28,7 +28,7 @@
|
||||
<ui-tooltip :text="$strings.LabelUpdateDetailsHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.LabelUpdateDetails }}
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
@@ -116,10 +115,10 @@ export default {
|
||||
libraryItemIds: this.selectedBookIds
|
||||
})
|
||||
.then(() => {
|
||||
this.$toast.info('Batch quick match of ' + this.selectedBookIds.length + ' books started!')
|
||||
this.$toast.info(this.$getString('ToastBatchQuickMatchStarted', [this.selectedBookIds.length]))
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toast.error('Batch quick match failed')
|
||||
this.$toast.error(this.$strings.ToastBatchQuickMatchFailed)
|
||||
console.error('Failed to batch quick match', error)
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
@@ -5,26 +5,28 @@
|
||||
<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-icons text-2xl -mt-px">add</span></ui-btn>
|
||||
<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>
|
||||
</form>
|
||||
</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
|
||||
@@ -94,7 +98,7 @@ export default {
|
||||
this.$toast.success(this.$strings.ToastBookmarkRemoveSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toast.error(this.$strings.ToastBookmarkRemoveFailed)
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
console.error(error)
|
||||
})
|
||||
this.show = false
|
||||
@@ -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.ToastBookmarkUpdateFailed)
|
||||
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>
|
||||
<modals-modal v-model="show" name="chapters" :width="600" :height="'unset'">
|
||||
<div id="chapter-modal-wrapper" ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div id="chapter-modal-wrapper" ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<template v-for="chap in chapters">
|
||||
<div :key="chap.id" :id="`chapter-row-${chap.id}`" class="flex items-center px-6 py-3 justify-start cursor-pointer hover:bg-bg relative" :class="chap.id === currentChapterId ? 'bg-yellow-400 bg-opacity-10' : chap.end / _playbackRate <= currentChapterStart ? 'bg-success bg-opacity-5' : 'bg-opacity-20'" @click="clickChapter(chap)">
|
||||
<div :key="chap.id" :id="`chapter-row-${chap.id}`" class="flex items-center px-6 py-3 justify-start cursor-pointer relative" :class="chap.id === currentChapterId ? 'bg-yellow-400/20 hover:bg-yellow-400/10' : chap.end / _playbackRate <= currentChapterStart ? 'bg-success/10 hover:bg-success/5' : 'hover:bg-primary/10'" @click="clickChapter(chap)">
|
||||
<p class="chapter-title truncate text-sm md:text-base">
|
||||
{{ chap.title }}
|
||||
</p>
|
||||
@@ -87,4 +87,4 @@ export default {
|
||||
max-width: calc(100% - 150px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<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">
|
||||
<span class="material-icons text-2xl md:text-4xl">close</span>
|
||||
</div>
|
||||
<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>
|
||||
</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>
|
||||
<div class="flex">
|
||||
<div class="flex-grow p-1 min-w-48 sm:min-w-64 md:min-w-80">
|
||||
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!isNewSeries" :label="$strings.LabelSeriesName" />
|
||||
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!isNewSeries" :label="$strings.LabelSeriesName" @input="seriesNameInputHandler" />
|
||||
</div>
|
||||
<div class="w-24 sm:w-28 md:w-40 p-1">
|
||||
<ui-text-input-with-label ref="sequenceInput" v-model="selectedSeries.sequence" :label="$strings.LabelSequence" />
|
||||
@@ -66,6 +66,11 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
seriesNameInputHandler() {
|
||||
if (this.$refs.sequenceInput) {
|
||||
this.$refs.sequenceInput.setFocus()
|
||||
}
|
||||
},
|
||||
setInputFocus() {
|
||||
if (this.isNewSeries) {
|
||||
// Focus on series input if new series
|
||||
@@ -134,4 +139,4 @@ export default {
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
|
||||
<div class="flex items-center">
|
||||
<ui-btn v-if="!isOpenSession && !isMediaItemShareSession" small color="error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn>
|
||||
<ui-btn v-else-if="!isMediaItemShareSession" small color="error" @click.stop="closeSessionClick">Close Open Session</ui-btn>
|
||||
<ui-btn v-else-if="!isMediaItemShareSession" small color="error" @click.stop="closeSessionClick">{{ $strings.ButtonCloseSession }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
@@ -206,14 +206,13 @@ export default {
|
||||
this.$axios
|
||||
.$post(`/api/session/${this._session.id}/close`)
|
||||
.then(() => {
|
||||
this.$toast.success('Session closed')
|
||||
this.show = false
|
||||
this.$emit('closedSession')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to close session', error)
|
||||
const errMsg = error.response?.data || ''
|
||||
this.$toast.error(errMsg || 'Failed to close open session')
|
||||
this.$toast.error(errMsg || this.$strings.ToastSessionCloseFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
|
||||
@@ -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-icons text-2xl landscape:text-2xl md:portrait:text-4xl lg:text-4xl">close</span>
|
||||
<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)'
|
||||
|
||||
87
client/components/modals/PlayerSettingsModal.vue
Normal file
87
client/components/modals/PlayerSettingsModal.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<modals-modal v-model="show" name="player-settings" :width="500" :height="'unset'">
|
||||
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden p-4" style="max-height: 80vh; min-height: 40vh">
|
||||
<h3 class="text-xl font-semibold mb-8">{{ $strings.HeaderPlayerSettings }}</h3>
|
||||
<div class="flex items-center mb-4">
|
||||
<ui-toggle-switch v-model="useChapterTrack" @input="setUseChapterTrack" />
|
||||
<div class="pl-4">
|
||||
<span>{{ $strings.LabelUseChapterTrack }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<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 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>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
useChapterTrack: false,
|
||||
jumpValues: [
|
||||
{ text: this.$getString('LabelTimeDurationXSeconds', ['10']), value: 10 },
|
||||
{ text: this.$getString('LabelTimeDurationXSeconds', ['15']), value: 15 },
|
||||
{ text: this.$getString('LabelTimeDurationXSeconds', ['30']), value: 30 },
|
||||
{ text: this.$getString('LabelTimeDurationXSeconds', ['60']), value: 60 },
|
||||
{ text: this.$getString('LabelTimeDurationXMinutes', ['2']), value: 120 },
|
||||
{ text: this.$getString('LabelTimeDurationXMinutes', ['5']), value: 300 }
|
||||
],
|
||||
jumpForwardAmount: 10,
|
||||
jumpBackwardAmount: 10,
|
||||
playbackRateIncrementDecrementValues: [0.1, 0.05],
|
||||
playbackRateIncrementDecrement: 0.1
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setUseChapterTrack() {
|
||||
this.$store.dispatch('user/updateUserSettings', { useChapterTrack: this.useChapterTrack })
|
||||
},
|
||||
setJumpForwardAmount(val) {
|
||||
this.jumpForwardAmount = val
|
||||
this.$store.dispatch('user/updateUserSettings', { jumpForwardAmount: val })
|
||||
},
|
||||
setJumpBackwardAmount(val) {
|
||||
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() {
|
||||
this.settingsUpdated()
|
||||
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -9,22 +9,23 @@
|
||||
<div class="absolute top-0 right-0 p-4">
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||
<a href="https://www.audiobookshelf.org/guides/media-item-shares" target="_blank" class="inline-flex">
|
||||
<span class="material-icons text-xl w-5 text-gray-200">help_outline</span>
|
||||
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<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: {
|
||||
@@ -112,11 +123,11 @@ export default {
|
||||
return this.$store.state.user.user
|
||||
},
|
||||
demoShareUrl() {
|
||||
return `${window.origin}/share/${this.newShareSlug}`
|
||||
return `${window.origin}${this.$config.routerBasePath}/share/${this.newShareSlug}`
|
||||
},
|
||||
currentShareUrl() {
|
||||
if (!this.currentShare) return ''
|
||||
return `${window.origin}/share/${this.currentShare.slug}`
|
||||
return `${window.origin}${this.$config.routerBasePath}/share/${this.currentShare.slug}`
|
||||
},
|
||||
currentShareTimeRemaining() {
|
||||
if (!this.currentShare) return 'Error'
|
||||
@@ -165,14 +176,15 @@ export default {
|
||||
},
|
||||
openShare() {
|
||||
if (!this.newShareSlug) {
|
||||
this.$toast.error('Slug is required')
|
||||
this.$toast.error(this.$strings.ToastSlugRequired)
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
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
|
||||
|
||||
@@ -6,34 +6,36 @@
|
||||
</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 v-if="!timerSet" class="w-full">
|
||||
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div class="w-full">
|
||||
<template v-for="time in sleepTimes">
|
||||
<div :key="time.text" class="flex items-center px-6 py-3 justify-center cursor-pointer hover:bg-bg relative" @click="setTime(time.seconds)">
|
||||
<p class="text-xl text-center">{{ time.text }}</p>
|
||||
<div :key="time.text" class="flex items-center px-6 py-3 justify-center cursor-pointer hover:bg-primary/25 relative" @click="setTime(time)">
|
||||
<p class="text-lg text-center">{{ time.text }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<form class="flex items-center justify-center px-6 py-3" @submit.prevent="submitCustomTime">
|
||||
<ui-text-input v-model="customTime" type="number" step="any" min="0.1" placeholder="Time in minutes" class="w-48" />
|
||||
<ui-btn color="success" type="submit" :padding-x="0" class="h-9 w-12 flex items-center justify-center ml-1">Set</ui-btn>
|
||||
<ui-text-input v-model="customTime" type="number" step="any" min="0.1" :placeholder="$strings.LabelTimeInMinutes" class="w-48" />
|
||||
<ui-btn color="success" type="submit" :padding-x="0" class="h-9 w-18 flex items-center justify-center ml-1">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
</form>
|
||||
</div>
|
||||
<div v-else class="w-full p-4">
|
||||
<div class="mb-4 flex items-center justify-center">
|
||||
<ui-btn :padding-x="2" small :disabled="remaining < 30 * 60" class="flex items-center mr-4" @click="decrement(30 * 60)">
|
||||
<span class="material-icons text-lg">remove</span>
|
||||
<span class="pl-1 text-base font-mono">30m</span>
|
||||
<div v-if="timerSet" class="w-full p-4">
|
||||
<div class="mb-4 h-px w-full bg-white/10" />
|
||||
|
||||
<div v-if="timerType === $constants.SleepTimerTypes.COUNTDOWN" class="mb-4 flex items-center justify-center space-x-4">
|
||||
<ui-btn :padding-x="2" small :disabled="remaining < 30 * 60" class="flex items-center h-9" @click="decrement(30 * 60)">
|
||||
<span class="material-symbols text-lg">remove</span>
|
||||
<span class="pl-1 text-sm">30m</span>
|
||||
</ui-btn>
|
||||
|
||||
<ui-icon-btn icon="remove" @click="decrement(60 * 5)" />
|
||||
<ui-icon-btn icon="remove" class="min-w-9" @click="decrement(60 * 5)" />
|
||||
|
||||
<p class="mx-6 text-2xl font-mono">{{ $secondsToTimestamp(remaining) }}</p>
|
||||
<p class="text-2xl font-mono">{{ $secondsToTimestamp(remaining) }}</p>
|
||||
|
||||
<ui-icon-btn icon="add" @click="increment(60 * 5)" />
|
||||
<ui-icon-btn icon="add" class="min-w-9" @click="increment(60 * 5)" />
|
||||
|
||||
<ui-btn :padding-x="2" small class="flex items-center ml-4" @click="increment(30 * 60)">
|
||||
<span class="material-icons text-lg">add</span>
|
||||
<span class="pl-1 text-base font-mono">30m</span>
|
||||
<ui-btn :padding-x="2" small class="flex items-center h-9" @click="increment(30 * 60)">
|
||||
<span class="material-symbols text-lg">add</span>
|
||||
<span class="pl-1 text-sm">30m</span>
|
||||
</ui-btn>
|
||||
</div>
|
||||
<ui-btn class="w-full" @click="$emit('cancel')">{{ $strings.ButtonCancel }}</ui-btn>
|
||||
@@ -47,52 +49,13 @@ export default {
|
||||
props: {
|
||||
value: Boolean,
|
||||
timerSet: Boolean,
|
||||
timerTime: Number,
|
||||
remaining: Number
|
||||
timerType: String,
|
||||
remaining: Number,
|
||||
hasChapters: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
customTime: null,
|
||||
sleepTimes: [
|
||||
{
|
||||
seconds: 60 * 5,
|
||||
text: '5 minutes'
|
||||
},
|
||||
{
|
||||
seconds: 60 * 15,
|
||||
text: '15 minutes'
|
||||
},
|
||||
{
|
||||
seconds: 60 * 20,
|
||||
text: '20 minutes'
|
||||
},
|
||||
{
|
||||
seconds: 60 * 30,
|
||||
text: '30 minutes'
|
||||
},
|
||||
{
|
||||
seconds: 60 * 45,
|
||||
text: '45 minutes'
|
||||
},
|
||||
{
|
||||
seconds: 60 * 60,
|
||||
text: '60 minutes'
|
||||
},
|
||||
{
|
||||
seconds: 60 * 90,
|
||||
text: '90 minutes'
|
||||
},
|
||||
{
|
||||
seconds: 60 * 120,
|
||||
text: '2 hours'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show(newVal) {
|
||||
if (newVal) {
|
||||
}
|
||||
customTime: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -103,6 +66,54 @@ export default {
|
||||
set(val) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
sleepTimes() {
|
||||
const times = [
|
||||
{
|
||||
seconds: 60 * 5,
|
||||
text: this.$getString('LabelTimeDurationXMinutes', ['5']),
|
||||
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||
},
|
||||
{
|
||||
seconds: 60 * 15,
|
||||
text: this.$getString('LabelTimeDurationXMinutes', ['15']),
|
||||
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||
},
|
||||
{
|
||||
seconds: 60 * 20,
|
||||
text: this.$getString('LabelTimeDurationXMinutes', ['20']),
|
||||
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||
},
|
||||
{
|
||||
seconds: 60 * 30,
|
||||
text: this.$getString('LabelTimeDurationXMinutes', ['30']),
|
||||
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||
},
|
||||
{
|
||||
seconds: 60 * 45,
|
||||
text: this.$getString('LabelTimeDurationXMinutes', ['45']),
|
||||
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||
},
|
||||
{
|
||||
seconds: 60 * 60,
|
||||
text: this.$getString('LabelTimeDurationXMinutes', ['60']),
|
||||
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||
},
|
||||
{
|
||||
seconds: 60 * 90,
|
||||
text: this.$getString('LabelTimeDurationXMinutes', ['90']),
|
||||
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||
},
|
||||
{
|
||||
seconds: 60 * 120,
|
||||
text: this.$getString('LabelTimeDurationXHours', ['2']),
|
||||
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||
}
|
||||
]
|
||||
if (this.hasChapters) {
|
||||
times.push({ seconds: -1, text: this.$strings.LabelEndOfChapter, timerType: this.$constants.SleepTimerTypes.CHAPTER })
|
||||
}
|
||||
return times
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -113,10 +124,14 @@ export default {
|
||||
}
|
||||
|
||||
const timeInSeconds = Math.round(Number(this.customTime) * 60)
|
||||
this.setTime(timeInSeconds)
|
||||
const time = {
|
||||
seconds: timeInSeconds,
|
||||
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||
}
|
||||
this.setTime(time)
|
||||
},
|
||||
setTime(seconds) {
|
||||
this.$emit('set', seconds)
|
||||
setTime(time) {
|
||||
this.$emit('set', time)
|
||||
},
|
||||
increment(amount) {
|
||||
this.$emit('increment', amount)
|
||||
@@ -130,4 +145,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
<div v-if="previewUpload" class="absolute top-0 left-0 w-full h-full z-10 bg-bg p-8">
|
||||
<p class="text-lg">Preview Cover</p>
|
||||
<span class="absolute top-4 right-4 material-icons text-2xl cursor-pointer" @click="resetCoverPreview">close</span>
|
||||
<span class="absolute top-4 right-4 material-symbols text-2xl cursor-pointer" @click="resetCoverPreview">close</span>
|
||||
<div class="flex justify-center py-4">
|
||||
<covers-preview-cover :src="previewUpload" :width="240" />
|
||||
</div>
|
||||
@@ -78,14 +78,13 @@ export default {
|
||||
if (data.error) {
|
||||
this.$toast.error(data.error)
|
||||
} else {
|
||||
this.$toast.success('Cover Uploaded')
|
||||
this.resetCoverPreview()
|
||||
}
|
||||
this.processingUpload = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
var errorMsg = error.response && error.response.data ? error.response.data : 'Unknown Error'
|
||||
var errorMsg = error.response && error.response.data ? error.response.data : this.$strings.ToastUnknownError
|
||||
this.$toast.error(errorMsg)
|
||||
this.processingUpload = false
|
||||
})
|
||||
@@ -95,7 +94,7 @@ export default {
|
||||
|
||||
var success = await this.$axios.$post(`/api/${this.entity}/${this.entityId}/cover`, { url: this.imageUrl }).catch((error) => {
|
||||
console.error('Failed to download cover from url', error)
|
||||
var errorMsg = error.response && error.response.data ? error.response.data : 'Unknown Error'
|
||||
var errorMsg = error.response && error.response.data ? error.response.data : this.$strings.ToastUnknownError
|
||||
this.$toast.error(errorMsg)
|
||||
return false
|
||||
})
|
||||
@@ -104,4 +103,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="w-full h-45 relative">
|
||||
<covers-author-image :author="authorCopy" />
|
||||
<div v-if="userCanDelete && !processing && author.imagePath" class="absolute top-0 left-0 w-full h-full opacity-0 hover:opacity-100">
|
||||
<span class="absolute top-2 right-2 material-icons text-error transform hover:scale-125 transition-transform cursor-pointer text-lg" @click="removeCover">delete</span>
|
||||
<span class="absolute top-2 right-2 material-symbols text-error transform hover:scale-125 transition-transform cursor-pointer text-lg" @click="removeCover">delete</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,12 +116,12 @@ export default {
|
||||
this.$axios
|
||||
.$delete(`/api/authors/${this.authorId}`)
|
||||
.then(() => {
|
||||
this.$toast.success('Author removed')
|
||||
this.$toast.success(this.$strings.ToastAuthorRemoveSuccess)
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove author', error)
|
||||
this.$toast.error('Failed to remove author')
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
@@ -141,14 +141,14 @@ export default {
|
||||
}
|
||||
})
|
||||
if (!Object.keys(updatePayload).length) {
|
||||
this.$toast.info(this.$strings.MessageNoUpdateNecessary)
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
return
|
||||
}
|
||||
this.processing = true
|
||||
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
const errorMsg = error.response ? error.response.data : null
|
||||
this.$toast.error(errorMsg || this.$strings.ToastAuthorUpdateFailed)
|
||||
this.$toast.error(errorMsg || this.$strings.ToastFailedToUpdate)
|
||||
return null
|
||||
})
|
||||
if (result) {
|
||||
@@ -158,7 +158,7 @@ export default {
|
||||
} else if (result.merged) {
|
||||
this.$toast.success(this.$strings.ToastAuthorUpdateMerged)
|
||||
this.show = false
|
||||
} else this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||
} else this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
}
|
||||
this.processing = false
|
||||
},
|
||||
@@ -174,7 +174,7 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
this.$toast.error(this.$strings.ToastAuthorImageRemoveFailed)
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
@@ -182,7 +182,7 @@ export default {
|
||||
},
|
||||
submitUploadCover() {
|
||||
if (!this.imageUrl?.startsWith('http:') && !this.imageUrl?.startsWith('https:')) {
|
||||
this.$toast.error('Invalid image url')
|
||||
this.$toast.error(this.$strings.ToastInvalidImageUrl)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -194,14 +194,14 @@ export default {
|
||||
.$post(`/api/authors/${this.authorId}/image`, updatePayload)
|
||||
.then((data) => {
|
||||
this.imageUrl = ''
|
||||
this.$toast.success('Author image updated')
|
||||
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||
|
||||
this.authorCopy.updatedAt = data.author.updatedAt
|
||||
this.authorCopy.imagePath = data.author.imagePath
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
this.$toast.error(error.response.data || 'Failed to remove author image')
|
||||
this.$toast.error(error.response.data || this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
@@ -209,7 +209,7 @@ export default {
|
||||
},
|
||||
async searchAuthor() {
|
||||
if (!this.authorCopy.name && !this.authorCopy.asin) {
|
||||
this.$toast.error('Must enter an author name')
|
||||
this.$toast.error(this.$strings.ToastNameRequired)
|
||||
return
|
||||
}
|
||||
this.processing = true
|
||||
@@ -228,17 +228,19 @@ export default {
|
||||
return null
|
||||
})
|
||||
if (!response) {
|
||||
this.$toast.error('Author not found')
|
||||
this.$toast.error(this.$strings.ToastAuthorSearchNotFound)
|
||||
} else if (response.updated) {
|
||||
if (response.author.imagePath) {
|
||||
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||
} else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
||||
} else {
|
||||
this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
||||
}
|
||||
|
||||
this.authorCopy = {
|
||||
...response.author
|
||||
}
|
||||
} else {
|
||||
this.$toast.info('No updates were made for Author')
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
}
|
||||
this.processing = false
|
||||
}
|
||||
|
||||
@@ -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,11 +10,11 @@
|
||||
<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-icons text-2xl -mt-px">forward</span></ui-btn>
|
||||
<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">
|
||||
<span class="material-icons text-3xl text-white text-opacity-70 hover:text-opacity-95 cursor-pointer" @click.stop.prevent="cancelEditing">close</span>
|
||||
<span class="material-symbols text-3xl text-white text-opacity-70 hover:text-opacity-95 cursor-pointer" @click.stop.prevent="cancelEditing">close</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -22,8 +22,8 @@
|
||||
<p v-else class="pl-2 pr-2 truncate">{{ bookmark.title }}</p>
|
||||
</div>
|
||||
<div v-if="!isEditing" class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
||||
<span class="material-icons text-xl mr-2 text-gray-200 hover:text-yellow-400" @click.stop="editClick">edit</span>
|
||||
<span class="material-icons text-xl text-gray-200 hover:text-error cursor-pointer" @click.stop="deleteClick">delete</span>
|
||||
<span class="material-symbols text-xl mr-2 text-gray-200 hover:text-yellow-400" @click.stop="editClick">edit</span>
|
||||
<span class="material-symbols text-xl text-gray-200 hover:text-error cursor-pointer" @click.stop="deleteClick">delete</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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,14 +2,19 @@
|
||||
<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">
|
||||
<p class="text-xl font-bold pb-4">
|
||||
Changelog <a :href="currentTagUrl" target="_blank" class="hover:underline">v{{ currentVersionNumber }}</a> ({{ currentVersionPubDate }})
|
||||
</p>
|
||||
<div class="custom-text" v-html="compiledMarkedown" />
|
||||
<template v-for="release in releasesToShow">
|
||||
<div :key="release.name">
|
||||
<p class="text-xl font-bold pb-4">
|
||||
Changelog <a :href="`https://github.com/advplyr/audiobookshelf/releases/tag/${release.name}`" target="_blank" class="hover:underline">{{ release.name }}</a> ({{ $formatDate(release.pubdate, dateFormat) }})
|
||||
</p>
|
||||
<div class="custom-text" v-html="getChangelog(release)" />
|
||||
</div>
|
||||
<div v-if="release !== releasesToShow[releasesToShow.length - 1]" :key="`${release.name}-divider`" class="border-b border-black-300 my-8" />
|
||||
</template>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
@@ -37,24 +42,15 @@ export default {
|
||||
dateFormat() {
|
||||
return this.$store.state.serverSettings.dateFormat
|
||||
},
|
||||
changelog() {
|
||||
return this.versionData?.currentVersionChangelog || 'No Changelog Available'
|
||||
},
|
||||
compiledMarkedown() {
|
||||
return marked.parse(this.changelog, { gfm: true, breaks: true })
|
||||
},
|
||||
currentVersionPubDate() {
|
||||
if (!this.versionData?.currentVersionPubDate) return 'Unknown release date'
|
||||
return `${this.$formatDate(this.versionData.currentVersionPubDate, this.dateFormat)}`
|
||||
},
|
||||
currentTagUrl() {
|
||||
return this.versionData?.currentTagUrl
|
||||
},
|
||||
currentVersionNumber() {
|
||||
return this.$config.version
|
||||
releasesToShow() {
|
||||
return this.versionData?.releasesToShow || []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getChangelog(release) {
|
||||
return marked.parse(release.changelog || 'No Changelog Available', { gfm: true, breaks: true })
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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,12 +149,11 @@ 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) => {
|
||||
console.error('Failed to remove books from collection', error)
|
||||
this.$toast.error(this.$strings.ToastCollectionItemsRemoveFailed)
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
this.processing = false
|
||||
})
|
||||
} else {
|
||||
@@ -152,12 +162,11 @@ 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) => {
|
||||
console.error('Failed to remove book from collection', error)
|
||||
this.$toast.error(this.$strings.ToastCollectionItemsRemoveFailed)
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
@@ -167,17 +176,16 @@ 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('Books added to collection')
|
||||
this.processing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to add books to collection', error)
|
||||
this.$toast.error('Failed to add books to collection')
|
||||
this.$toast.error(this.$strings.ToastCollectionItemsAddFailed)
|
||||
this.processing = false
|
||||
})
|
||||
} else {
|
||||
@@ -187,12 +195,11 @@ export default {
|
||||
.$post(`/api/collections/${collection.id}/book`, { id: this.selectedLibraryItemId })
|
||||
.then((updatedCollection) => {
|
||||
console.log(`Book added to collection`, updatedCollection)
|
||||
this.$toast.success('Book added to collection')
|
||||
this.processing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to add book to collection', error)
|
||||
this.$toast.error('Failed to add book to collection')
|
||||
this.$toast.error(this.$strings.ToastCollectionItemsAddFailed)
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
@@ -214,14 +221,13 @@ 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 = ''
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to create collection', error)
|
||||
var errMsg = error.response ? error.response.data || '' : ''
|
||||
this.$toast.error(`Failed to create collection: ${errMsg}`)
|
||||
this.$toast.error(this.$strings.ToastCollectionCreateFailed + ': ' + errMsg)
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
@@ -8,8 +8,8 @@
|
||||
<nuxt-link :to="`/collection/${collection.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ collection.name }}</nuxt-link>
|
||||
</div>
|
||||
<div class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
||||
<ui-btn v-if="!isBookIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-icons text-2xl pt-px">add</span></ui-btn>
|
||||
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-icons text-2xl pt-px">remove</span></ui-btn>
|
||||
<ui-btn v-if="!isBookIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-symbols text-2xl pt-px">add</span></ui-btn>
|
||||
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-symbols text-2xl pt-px">remove</span></ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<template v-else>
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="hover:bg-white hover:bg-opacity-10 cursor-pointer h-11 w-11 flex items-center justify-center rounded-full" @click="showImageUploader = false">
|
||||
<span class="material-icons text-4xl">arrow_back</span>
|
||||
<span class="material-symbols text-4xl">arrow_back</span>
|
||||
</div>
|
||||
<p class="ml-2 text-xl mb-1">Collection Cover Image</p>
|
||||
</div>
|
||||
@@ -106,7 +106,7 @@ export default {
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove collection', error)
|
||||
this.processing = false
|
||||
this.$toast.error(this.$strings.ToastCollectionRemoveFailed)
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -115,7 +115,7 @@ export default {
|
||||
return
|
||||
}
|
||||
if (!this.newCollectionName) {
|
||||
return this.$toast.error('Collection must have a name')
|
||||
return this.$toast.error(this.$strings.ToastNameRequired)
|
||||
}
|
||||
|
||||
this.processing = true
|
||||
@@ -135,7 +135,7 @@ export default {
|
||||
.catch((error) => {
|
||||
console.error('Failed to update collection', error)
|
||||
this.processing = false
|
||||
this.$toast.error(this.$strings.ToastCollectionUpdateFailed)
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -125,12 +125,12 @@ export default {
|
||||
this.$refs.ereaderEmailInput.blur()
|
||||
|
||||
if (!this.newDevice.name?.trim() || !this.newDevice.email?.trim()) {
|
||||
this.$toast.error('Name and email required')
|
||||
this.$toast.error(this.$strings.ToastNameEmailRequired)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.newDevice.availabilityOption === 'specificUsers' && !this.newDevice.users.length) {
|
||||
this.$toast.error('Must select at least one user')
|
||||
this.$toast.error(this.$strings.ToastSelectAtLeastOneUser)
|
||||
return
|
||||
}
|
||||
if (this.newDevice.availabilityOption !== 'specificUsers') {
|
||||
@@ -142,14 +142,14 @@ export default {
|
||||
|
||||
if (!this.ereaderDevice) {
|
||||
if (this.existingDevices.some((d) => d.name === this.newDevice.name)) {
|
||||
this.$toast.error('Ereader device with that name already exists')
|
||||
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
|
||||
return
|
||||
}
|
||||
|
||||
this.submitCreate()
|
||||
} else {
|
||||
if (this.ereaderDevice.name !== this.newDevice.name && this.existingDevices.some((d) => d.name === this.newDevice.name)) {
|
||||
this.$toast.error('Ereader device with that name already exists')
|
||||
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -174,12 +174,11 @@ export default {
|
||||
.$post(`/api/emails/ereader-devices`, payload)
|
||||
.then((data) => {
|
||||
this.$emit('update', data.ereaderDevices)
|
||||
this.$toast.success('Device updated')
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update device', error)
|
||||
this.$toast.error('Failed to update device')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
@@ -201,12 +200,11 @@ export default {
|
||||
.$post('/api/emails/ereader-devices', payload)
|
||||
.then((data) => {
|
||||
this.$emit('update', data.ereaderDevices || [])
|
||||
this.$toast.success('Device added')
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to add device', error)
|
||||
this.$toast.error('Failed to add device')
|
||||
this.$toast.error(this.$strings.ToastDeviceAddFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
|
||||
188
client/components/modals/emails/UserEReaderDeviceModal.vue
Normal file
188
client/components/modals/emails/UserEReaderDeviceModal.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<modals-modal ref="modal" v-model="show" name="ereader-device-edit" :width="800" :height="'unset'" :processing="processing">
|
||||
<template #outer>
|
||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<form @submit.prevent="submitForm">
|
||||
<div class="w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300">
|
||||
<div class="w-full px-3 py-5 md:p-12">
|
||||
<div class="flex items-center -mx-1 mb-4">
|
||||
<div class="w-full md:w-1/2 px-1">
|
||||
<ui-text-input-with-label ref="ereaderNameInput" v-model="newDevice.name" :disabled="processing" :label="$strings.LabelName" />
|
||||
</div>
|
||||
<div class="w-full md:w-1/2 px-1">
|
||||
<ui-text-input-with-label ref="ereaderEmailInput" v-model="newDevice.email" :disabled="processing" :label="$strings.LabelEmail" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center pt-4">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</modals-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: Boolean,
|
||||
existingDevices: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
ereaderDevice: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
newDevice: {
|
||||
name: '',
|
||||
email: '',
|
||||
availabilityOption: 'adminAndUp',
|
||||
users: []
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show: {
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
user() {
|
||||
return this.$store.state.user.user
|
||||
},
|
||||
title() {
|
||||
return !this.ereaderDevice ? 'Create Device' : 'Update Device'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submitForm() {
|
||||
this.$refs.ereaderNameInput.blur()
|
||||
this.$refs.ereaderEmailInput.blur()
|
||||
|
||||
if (!this.newDevice.name?.trim() || !this.newDevice.email?.trim()) {
|
||||
this.$toast.error(this.$strings.ToastNameEmailRequired)
|
||||
return
|
||||
}
|
||||
|
||||
this.newDevice.name = this.newDevice.name.trim()
|
||||
this.newDevice.email = this.newDevice.email.trim()
|
||||
|
||||
// Only catches duplicate names for the current user
|
||||
// Duplicates with other users caught on server side
|
||||
if (!this.ereaderDevice) {
|
||||
if (this.existingDevices.some((d) => d.name === this.newDevice.name)) {
|
||||
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
|
||||
return
|
||||
}
|
||||
|
||||
this.submitCreate()
|
||||
} else {
|
||||
if (this.ereaderDevice.name !== this.newDevice.name && this.existingDevices.some((d) => d.name === this.newDevice.name)) {
|
||||
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
|
||||
return
|
||||
}
|
||||
|
||||
this.submitUpdate()
|
||||
}
|
||||
},
|
||||
submitUpdate() {
|
||||
this.processing = true
|
||||
|
||||
const existingDevicesWithoutThisOne = this.existingDevices.filter((d) => d.name !== this.ereaderDevice.name)
|
||||
|
||||
const payload = {
|
||||
ereaderDevices: [
|
||||
...existingDevicesWithoutThisOne,
|
||||
{
|
||||
...this.newDevice
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
this.$axios
|
||||
.$post(`/api/me/ereader-devices`, payload)
|
||||
.then((data) => {
|
||||
this.$emit('update', data.ereaderDevices)
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update device', error)
|
||||
if (error.response?.data?.toLowerCase().includes('duplicate')) {
|
||||
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
|
||||
} else {
|
||||
this.$toast.error(this.$strings.ToastDeviceAddFailed)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
submitCreate() {
|
||||
this.processing = true
|
||||
|
||||
const payload = {
|
||||
ereaderDevices: [
|
||||
...this.existingDevices,
|
||||
{
|
||||
...this.newDevice
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
this.$axios
|
||||
.$post('/api/me/ereader-devices', payload)
|
||||
.then((data) => {
|
||||
this.$emit('update', data.ereaderDevices || [])
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to add device', error)
|
||||
if (error.response?.data?.toLowerCase().includes('duplicate')) {
|
||||
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
|
||||
} else {
|
||||
this.$toast.error(this.$strings.ToastDeviceAddFailed)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
init() {
|
||||
if (this.ereaderDevice) {
|
||||
this.newDevice.name = this.ereaderDevice.name
|
||||
this.newDevice.email = this.ereaderDevice.email
|
||||
this.newDevice.availabilityOption = this.ereaderDevice.availabilityOption || 'specificUsers'
|
||||
this.newDevice.users = this.ereaderDevice.users || [this.user.id]
|
||||
} else {
|
||||
this.newDevice.name = ''
|
||||
this.newDevice.email = ''
|
||||
this.newDevice.availabilityOption = 'specificUsers'
|
||||
this.newDevice.users = [this.user.id]
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -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-icons 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-icons 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>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
|
||||
<div v-if="userCanDelete" class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-sm" @click="removeCover">
|
||||
<ui-tooltip direction="top" :text="$strings.LabelRemoveCover">
|
||||
<span class="material-icons text-2xl">delete</span>
|
||||
<span class="material-symbols text-2xl">delete</span>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,7 +19,7 @@
|
||||
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 md:min-w-32">
|
||||
<ui-file-input ref="fileInput" @change="fileUploadSelected">
|
||||
<span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span>
|
||||
<span class="material-icons text-2xl inline-block md:!hidden">upload</span>
|
||||
<span class="material-symbols text-2xl inline-block md:!hidden">upload</span>
|
||||
</ui-file-input>
|
||||
</div>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
<div v-if="previewUpload" class="absolute top-0 left-0 w-full h-full z-10 bg-bg p-8">
|
||||
<p class="text-lg">{{ $strings.HeaderPreviewCover }}</p>
|
||||
<span class="absolute top-4 right-4 material-icons text-2xl cursor-pointer" @click="resetCoverPreview">close</span>
|
||||
<span class="absolute top-4 right-4 material-symbols text-2xl cursor-pointer" @click="resetCoverPreview">close</span>
|
||||
<div class="flex justify-center py-4">
|
||||
<covers-preview-cover :src="previewUpload" :width="240" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</div>
|
||||
@@ -194,7 +194,6 @@ export default {
|
||||
if (data.error) {
|
||||
this.$toast.error(data.error)
|
||||
} else {
|
||||
this.$toast.success('Cover Uploaded')
|
||||
this.resetCoverPreview()
|
||||
}
|
||||
this.processingUpload = false
|
||||
@@ -204,7 +203,7 @@ export default {
|
||||
if (error.response && error.response.data) {
|
||||
this.$toast.error(error.response.data)
|
||||
} else {
|
||||
this.$toast.error('Oops, something went wrong...')
|
||||
this.$toast.error(this.$strings.ToastUnknownError)
|
||||
}
|
||||
this.processingUpload = false
|
||||
})
|
||||
@@ -255,7 +254,7 @@ export default {
|
||||
},
|
||||
async updateCover(cover) {
|
||||
if (!cover.startsWith('http:') && !cover.startsWith('https:')) {
|
||||
this.$toast.error('Invalid URL')
|
||||
this.$toast.error(this.$strings.ToastInvalidUrl)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -264,11 +263,10 @@ export default {
|
||||
.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover })
|
||||
.then(() => {
|
||||
this.imageUrl = ''
|
||||
this.$toast.success('Update Successful')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update cover', error)
|
||||
this.$toast.error(error.response?.data || 'Failed to update cover')
|
||||
this.$toast.error(error.response?.data || this.$strings.ToastCoverUpdateFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.isProcessing = false
|
||||
@@ -308,12 +306,9 @@ export default {
|
||||
this.isProcessing = true
|
||||
this.$axios
|
||||
.$patch(`/api/items/${this.libraryItemId}/cover`, { cover: coverFile.metadata.path })
|
||||
.then(() => {
|
||||
this.$toast.success('Update Successful')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to set local cover', error)
|
||||
this.$toast.error(error.response?.data || 'Failed to set cover')
|
||||
this.$toast.error(error.response?.data || this.$strings.ToastCoverUpdateFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.isProcessing = false
|
||||
@@ -321,4 +316,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -92,7 +92,7 @@ export default {
|
||||
|
||||
var { title, author } = this.$refs.itemDetailsEdit.getTitleAndAuthorName()
|
||||
if (!title) {
|
||||
this.$toast.error('Must have a title for quick match')
|
||||
this.$toast.error(this.$strings.ToastTitleRequired)
|
||||
return
|
||||
}
|
||||
this.quickMatching = true
|
||||
@@ -108,9 +108,9 @@ export default {
|
||||
if (res.warning) {
|
||||
this.$toast.warning(res.warning)
|
||||
} else if (res.updated) {
|
||||
this.$toast.success('Item details updated')
|
||||
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||
} else {
|
||||
this.$toast.info('No updates were made')
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -128,18 +128,18 @@ export default {
|
||||
this.rescanning = false
|
||||
var result = data.result
|
||||
if (!result) {
|
||||
this.$toast.error(`Re-Scan Failed for "${this.title}"`)
|
||||
this.$toast.error(this.$getString('ToastRescanFailed', [this.title]))
|
||||
} else if (result === 'UPDATED') {
|
||||
this.$toast.success(`Re-Scan complete item was updated`)
|
||||
this.$toast.success(this.$strings.ToastRescanUpdated)
|
||||
} else if (result === 'UPTODATE') {
|
||||
this.$toast.success(`Re-Scan complete item was up to date`)
|
||||
this.$toast.success(this.$strings.ToastRescanUpToDate)
|
||||
} else if (result === 'REMOVED') {
|
||||
this.$toast.error(`Re-Scan complete item was removed`)
|
||||
this.$toast.error(this.$strings.ToastRescanRemoved)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to scan library item', error)
|
||||
this.$toast.error('Failed to scan library item')
|
||||
this.$toast.error(this.$strings.ToastScanFailed)
|
||||
this.rescanning = false
|
||||
})
|
||||
},
|
||||
@@ -156,7 +156,7 @@ export default {
|
||||
}
|
||||
var updatedDetails = this.$refs.itemDetailsEdit.getDetails()
|
||||
if (!updatedDetails.hasChanges) {
|
||||
this.$toast.info('No changes were made')
|
||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||
return false
|
||||
}
|
||||
return this.updateDetails(updatedDetails)
|
||||
@@ -170,7 +170,7 @@ export default {
|
||||
this.isProcessing = false
|
||||
if (updateResult) {
|
||||
if (updateResult.updated) {
|
||||
this.$toast.success('Item details updated')
|
||||
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||
return true
|
||||
} else {
|
||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||
@@ -217,4 +217,4 @@ export default {
|
||||
height: calc(100% - 80px);
|
||||
max-height: calc(100% - 80px);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<ui-text-input-with-label ref="maxEpisodesInput" v-model="maxEpisodesToDownload" :disabled="checkingNewEpisodes" type="number" :label="$strings.LabelLimit" class="w-16 mr-2" input-class="h-10">
|
||||
<div class="flex -mb-0.5">
|
||||
<p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': checkingNewEpisodes }">{{ $strings.LabelLimit }}</p>
|
||||
<ui-tooltip direction="top" text="Max # of episodes to download. Use 0 for unlimited.">
|
||||
<span class="material-icons text-base">info_outlined</span>
|
||||
<ui-tooltip direction="top" :text="$strings.LabelMaxEpisodesToDownload">
|
||||
<span class="material-symbols text-base">info</span>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</ui-text-input-with-label>
|
||||
@@ -99,7 +99,7 @@ export default {
|
||||
|
||||
if (this.maxEpisodesToDownload < 0) {
|
||||
this.maxEpisodesToDownload = 3
|
||||
this.$toast.error('Invalid max episodes to download')
|
||||
this.$toast.error(this.$strings.ToastInvalidMaxEpisodesToDownload)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -120,9 +124,9 @@ export default {
|
||||
.then((response) => {
|
||||
if (response.episodes && response.episodes.length) {
|
||||
console.log('New episodes', response.episodes.length)
|
||||
this.$toast.success(`${response.episodes.length} new episodes found!`)
|
||||
this.$toast.success(this.$getString('ToastNewEpisodesFound', [response.episodes.length]))
|
||||
} else {
|
||||
this.$toast.info('No new episodes found')
|
||||
this.$toast.info(this.$strings.ToastNoNewEpisodesFound)
|
||||
}
|
||||
this.checkingNewEpisodes = false
|
||||
})
|
||||
@@ -141,4 +145,4 @@ export default {
|
||||
this.setLastEpisodeCheckInput()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div v-if="selectedMatchOrig" class="absolute top-0 left-0 w-full bg-bg h-full px-2 py-6 md:p-8 max-h-full overflow-y-auto overflow-x-hidden">
|
||||
<div class="flex mb-4">
|
||||
<div class="w-8 h-8 rounded-full hover:bg-white hover:bg-opacity-10 flex items-center justify-center cursor-pointer" @click="clearSelectedMatch">
|
||||
<span class="material-icons text-3xl">arrow_back</span>
|
||||
<span class="material-symbols text-3xl">arrow_back</span>
|
||||
</div>
|
||||
<p class="text-xl pl-3">{{ $strings.HeaderUpdateDetails }}</p>
|
||||
</div>
|
||||
@@ -59,49 +59,63 @@
|
||||
<ui-checkbox v-model="selectedMatchUsage.title" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" :label="$strings.LabelTitle" />
|
||||
<p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.title || '' }}</p>
|
||||
<p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('title', mediaMetadata.title)">{{ mediaMetadata.title || '' }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.subtitle" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.subtitle" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" :label="$strings.LabelSubtitle" />
|
||||
<p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.subtitle || '' }}</p>
|
||||
<p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('subtitle', mediaMetadata.subtitle)">{{ mediaMetadata.subtitle }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.author" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.author" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" :label="$strings.LabelAuthor" />
|
||||
<p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.authorName || '' }}</p>
|
||||
<p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('author', mediaMetadata.authorName)">{{ mediaMetadata.authorName }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.narrator" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.narrator" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-multi-select v-model="selectedMatch.narrator" :items="narrators" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" />
|
||||
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.narratorName || '' }}</p>
|
||||
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('narrator', mediaMetadata.narrators)">{{ mediaMetadata.narratorName }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<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" />
|
||||
<p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</p>
|
||||
<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.descriptionPlain.substr(0, 100) + (mediaMetadata.descriptionPlain.length > 100 ? '...' : '') }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.publisher" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.publisher" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" :label="$strings.LabelPublisher" />
|
||||
<p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.publisher || '' }}</p>
|
||||
<p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publisher', mediaMetadata.publisher)">{{ mediaMetadata.publisher }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.publishedYear" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.publishedYear" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" :label="$strings.LabelPublishYear" />
|
||||
<p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.publishedYear || '' }}</p>
|
||||
<p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publishedYear', mediaMetadata.publishedYear)">{{ mediaMetadata.publishedYear }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,42 +123,54 @@
|
||||
<ui-checkbox v-model="selectedMatchUsage.series" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<widgets-series-input-widget v-model="selectedMatch.series" :disabled="!selectedMatchUsage.series" />
|
||||
<p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.seriesName || '' }}</p>
|
||||
<p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('series', mediaMetadata.series)">{{ mediaMetadata.seriesName }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.genres && selectedMatchOrig.genres.length" class="flex items-center py-2">
|
||||
<div v-if="selectedMatchOrig.genres?.length" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.genres" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-multi-select v-model="selectedMatch.genres" :items="genres" :disabled="!selectedMatchUsage.genres" :label="$strings.LabelGenres" />
|
||||
<p v-if="mediaMetadata.genres" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.genres.join(', ') }}</p>
|
||||
<p v-if="mediaMetadata.genres?.length" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('genres', mediaMetadata.genres)">{{ mediaMetadata.genres.join(', ') }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.tags" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.tags" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-multi-select v-model="selectedMatch.tags" :items="tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" />
|
||||
<p v-if="media.tags" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ media.tags.join(', ') }}</p>
|
||||
<p v-if="media.tags?.length" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('tags', media.tags)">{{ media.tags.join(', ') }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.language" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.language" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" :label="$strings.LabelLanguage" />
|
||||
<p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.language || '' }}</p>
|
||||
<p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('language', mediaMetadata.language)">{{ mediaMetadata.language }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.isbn" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.isbn" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" />
|
||||
<p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.isbn || '' }}</p>
|
||||
<p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('isbn', mediaMetadata.isbn)">{{ mediaMetadata.isbn }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.asin" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.asin" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" />
|
||||
<p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.asin || '' }}</p>
|
||||
<p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('asin', mediaMetadata.asin)">{{ mediaMetadata.asin }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -152,28 +178,36 @@
|
||||
<ui-checkbox v-model="selectedMatchUsage.itunesId" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.itunesId" type="number" :disabled="!selectedMatchUsage.itunesId" label="iTunes ID" />
|
||||
<p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.itunesId || '' }}</p>
|
||||
<p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesId', mediaMetadata.itunesId)">{{ mediaMetadata.itunesId }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.feedUrl" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.feedUrl" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.feedUrl" :disabled="!selectedMatchUsage.feedUrl" label="RSS Feed URL" />
|
||||
<p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.feedUrl || '' }}</p>
|
||||
<p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('feedUrl', mediaMetadata.feedUrl)">{{ mediaMetadata.feedUrl }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.itunesPageUrl" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.itunesPageUrl" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.itunesPageUrl" :disabled="!selectedMatchUsage.itunesPageUrl" label="iTunes Page URL" />
|
||||
<p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.itunesPageUrl || '' }}</p>
|
||||
<p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesPageUrl', mediaMetadata.itunesPageUrl)">{{ mediaMetadata.itunesPageUrl }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.releaseDate" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.releaseDate" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" :label="$strings.LabelReleaseDate" />
|
||||
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.releaseDate || '' }}</p>
|
||||
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('releaseDate', mediaMetadata.releaseDate)">{{ mediaMetadata.releaseDate }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.explicit != null" class="flex items-center pb-2" :class="{ 'pt-2': mediaMetadata.explicit == null }">
|
||||
@@ -281,7 +315,7 @@ export default {
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
filterData() {
|
||||
return this.$store.state.libraries.filterData
|
||||
return this.$store.state.libraries.filterData || {}
|
||||
},
|
||||
providers() {
|
||||
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
|
||||
@@ -321,6 +355,13 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setMatchFieldValue(field, value) {
|
||||
if (Array.isArray(value)) {
|
||||
this.selectedMatch[field] = [...value]
|
||||
} else {
|
||||
this.selectedMatch[field] = value
|
||||
}
|
||||
},
|
||||
selectAllToggled(val) {
|
||||
for (const key in this.selectedMatchUsage) {
|
||||
this.selectedMatchUsage[key] = val
|
||||
@@ -356,7 +397,7 @@ export default {
|
||||
},
|
||||
submitSearch() {
|
||||
if (!this.searchTitle) {
|
||||
this.$toast.warning('Search title is required')
|
||||
this.$toast.warning(this.$strings.ToastTitleRequired)
|
||||
return
|
||||
}
|
||||
this.persistProvider()
|
||||
@@ -577,12 +618,12 @@ export default {
|
||||
if (updateResult.updated) {
|
||||
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||
} else {
|
||||
this.$toast.info(this.$strings.ToastItemDetailsUpdateUnneeded)
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
}
|
||||
this.clearSelectedMatch()
|
||||
this.$emit('selectTab', 'details')
|
||||
} else {
|
||||
this.$toast.error(this.$strings.ToastItemDetailsUpdateFailed)
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
}
|
||||
} else {
|
||||
this.clearSelectedMatch()
|
||||
|
||||
@@ -2,29 +2,29 @@
|
||||
<div class="w-full h-full relative">
|
||||
<div id="scheduleWrapper" class="w-full overflow-y-auto px-2 py-4 md:px-6 md:py-6">
|
||||
<template v-if="!feedUrl">
|
||||
<widgets-alert type="warning" class="text-base mb-4">No RSS feed URL is set for this podcast</widgets-alert>
|
||||
<widgets-alert type="warning" class="text-base mb-4">{{ $strings.ToastPodcastNoRssFeed }}</widgets-alert>
|
||||
</template>
|
||||
<template v-if="feedUrl || autoDownloadEpisodes">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<p class="text-base md:text-xl font-semibold">Schedule Automatic Episode Downloads</p>
|
||||
<ui-checkbox v-model="enableAutoDownloadEpisodes" label="Enable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" />
|
||||
<p class="text-base md:text-xl font-semibold">{{ $strings.HeaderScheduleEpisodeDownloads }}</p>
|
||||
<ui-checkbox v-model="enableAutoDownloadEpisodes" :label="$strings.LabelEnable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" />
|
||||
</div>
|
||||
|
||||
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
|
||||
<ui-text-input ref="maxEpisodesInput" type="number" v-model="newMaxEpisodesToKeep" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updatedMaxEpisodesToKeep" />
|
||||
<ui-tooltip text="Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes. <br>This will only delete 1 episode per new download.">
|
||||
<ui-tooltip :text="$strings.LabelMaxEpisodesToKeepHelp">
|
||||
<p class="pl-4 text-base">
|
||||
Max episodes to keep
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
{{ $strings.LabelMaxEpisodesToKeep }}
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
|
||||
<ui-text-input ref="maxEpisodesToDownloadInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" />
|
||||
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded.">
|
||||
<ui-tooltip :text="$strings.LabelUseZeroForUnlimited">
|
||||
<p class="pl-4 text-base">
|
||||
Max new episodes to download per check
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
{{ $strings.LabelMaxEpisodesToDownloadPerCheck }}
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -36,7 +36,7 @@
|
||||
<div v-if="feedUrl || autoDownloadEpisodes" class="absolute bottom-0 left-0 w-full py-2 md:py-4 bg-bg border-t border-white border-opacity-5">
|
||||
<div class="flex items-center px-2 md:px-4">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn @click="save" :disabled="!isUpdated" :color="isUpdated ? 'success' : 'primary'" class="mx-2">{{ isUpdated ? 'Save' : 'No update necessary' }}</ui-btn>
|
||||
<ui-btn @click="save" :disabled="!isUpdated" :color="isUpdated ? 'success' : 'primary'" class="mx-2">{{ isUpdated ? $strings.ButtonSave : $strings.MessageNoUpdatesWereNecessary }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -163,7 +163,7 @@ export default {
|
||||
this.isProcessing = false
|
||||
if (updateResult) {
|
||||
if (updateResult.updated) {
|
||||
this.$toast.success('Item details updated')
|
||||
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||
return true
|
||||
} else {
|
||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div>
|
||||
<ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=m4b`" class="flex items-center"
|
||||
>{{ $strings.ButtonOpenManager }}
|
||||
<span class="material-icons text-lg ml-2">launch</span>
|
||||
<span class="material-symbols text-lg ml-2">launch</span>
|
||||
</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,21 +30,21 @@
|
||||
<div>
|
||||
<ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=embed`" class="flex items-center"
|
||||
>{{ $strings.ButtonOpenManager }}
|
||||
<span class="material-icons text-lg ml-2">launch</span>
|
||||
<span class="material-symbols text-lg ml-2">launch</span>
|
||||
</ui-btn>
|
||||
|
||||
<ui-btn v-if="!isMetadataEmbedQueued && !isEmbedTaskRunning" class="w-full mt-4" small @click.stop="quickEmbed">Quick Embed</ui-btn>
|
||||
<ui-btn v-if="!isMetadataEmbedQueued && !isEmbedTaskRunning" class="w-full mt-4" small @click.stop="quickEmbed">{{ $strings.ButtonQuickEmbed }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- queued alert -->
|
||||
<widgets-alert v-if="isMetadataEmbedQueued" type="warning" class="mt-4">
|
||||
<p class="text-lg">Queued for metadata embed ({{ queuedEmbedLIds.length }} in queue)</p>
|
||||
<p class="text-lg">{{ $getString('MessageQuickEmbedQueue', [queuedEmbedLIds.length]) }}</p>
|
||||
</widgets-alert>
|
||||
|
||||
<!-- processing alert -->
|
||||
<widgets-alert v-if="isEmbedTaskRunning" type="warning" class="mt-4">
|
||||
<p class="text-lg">Currently embedding metadata</p>
|
||||
<p class="text-lg">{{ $strings.MessageQuickEmbedInProgress }}</p>
|
||||
</widgets-alert>
|
||||
</div>
|
||||
|
||||
@@ -113,7 +113,7 @@ export default {
|
||||
methods: {
|
||||
quickEmbed() {
|
||||
const payload = {
|
||||
message: 'Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?',
|
||||
message: this.$strings.MessageConfirmQuickEmbed,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.$axios
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
<div class="folders-container overflow-y-auto w-full py-2 mb-2">
|
||||
<p class="px-1 text-sm font-semibold">{{ $strings.LabelFolders }}</p>
|
||||
<div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2">
|
||||
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<span class="material-symbols fill bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<ui-editable-text ref="folderInput" v-model="folder.fullPath" :readonly="!!folder.id" type="text" class="w-full" @blur="existingFolderInputBlurred(folder)" />
|
||||
<span v-show="folders.length > 1" class="material-icons text-2xl ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span>
|
||||
<span v-show="folders.length > 1" class="material-symbols text-2xl ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span>
|
||||
</div>
|
||||
<div class="flex py-1 px-2 items-center w-full">
|
||||
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<span class="material-symbols fill bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<ui-editable-text ref="newFolderInput" v-model="newFolderPath" :placeholder="$strings.PlaceholderNewFolderPath" type="text" class="w-full" @blur="newFolderInputBlurred" />
|
||||
</div>
|
||||
|
||||
@@ -169,4 +169,4 @@ export default {
|
||||
max-height: calc(80vh - 292px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -111,7 +111,6 @@ export default {
|
||||
},
|
||||
updateLibrary(library) {
|
||||
this.mapLibraryToCopy(library)
|
||||
console.log('Updated library', this.libraryCopy)
|
||||
},
|
||||
getNewLibraryData() {
|
||||
return {
|
||||
@@ -128,7 +127,9 @@ export default {
|
||||
autoScanCronExpression: null,
|
||||
hideSingleBookSeries: false,
|
||||
onlyShowLaterBooksInContinueSeries: false,
|
||||
metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
|
||||
metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'],
|
||||
markAsFinishedPercentComplete: null,
|
||||
markAsFinishedTimeRemaining: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -156,11 +157,11 @@ export default {
|
||||
},
|
||||
validate() {
|
||||
if (!this.libraryCopy.name) {
|
||||
this.$toast.error('Library must have a name')
|
||||
this.$toast.error(this.$strings.ToastNameRequired)
|
||||
return false
|
||||
}
|
||||
if (!this.libraryCopy.folders.length) {
|
||||
this.$toast.error('Library must have at least 1 path')
|
||||
this.$toast.error(this.$strings.ToastMustHaveAtLeastOnePath)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -205,7 +206,7 @@ export default {
|
||||
submitUpdateLibrary() {
|
||||
var newLibraryPayload = this.getLibraryUpdatePayload()
|
||||
if (!Object.keys(newLibraryPayload).length) {
|
||||
this.$toast.info('No updates are necessary')
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -222,7 +223,7 @@ export default {
|
||||
if (error.response && error.response.data) {
|
||||
this.$toast.error(error.response.data)
|
||||
} else {
|
||||
this.$toast.error(this.$strings.ToastLibraryUpdateFailed)
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
}
|
||||
this.processing = false
|
||||
})
|
||||
@@ -236,7 +237,6 @@ export default {
|
||||
this.show = false
|
||||
this.$toast.success(this.$getString('ToastLibraryCreateSuccess', [res.name]))
|
||||
if (!this.$store.state.libraries.currentLibraryId) {
|
||||
console.log('Setting initially library id', res.id)
|
||||
// First library added
|
||||
this.$store.dispatch('libraries/fetch', res.id)
|
||||
}
|
||||
@@ -264,4 +264,4 @@ export default {
|
||||
.tab.tab-selected {
|
||||
height: 41px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="w-full h-full bg-bg absolute top-0 left-0 px-4 py-4 z-10">
|
||||
<div class="flex items-center py-1 mb-2">
|
||||
<span class="material-icons text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span>
|
||||
<span class="material-symbols text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span>
|
||||
<p class="px-4 text-xl">{{ $strings.HeaderChooseAFolder }}</p>
|
||||
</div>
|
||||
<div v-if="rootDirs.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2">
|
||||
@@ -10,18 +10,18 @@
|
||||
<div v-if="rootDirs.length" class="relative flex bg-primary bg-opacity-50 p-4 folder-container">
|
||||
<div class="w-1/2 border-r border-bg h-full overflow-y-auto">
|
||||
<div v-if="level > 0" class="w-full p-1 cursor-pointer flex items-center hover:bg-white/10" @click="goBack">
|
||||
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<span class="material-symbols fill bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<p class="text-base font-mono px-2">..</p>
|
||||
</div>
|
||||
<div v-for="dir in _directories" :key="dir.path" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" :class="dir.className" @click="selectDir(dir)">
|
||||
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<span class="material-symbols fill bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
|
||||
<span v-if="dir.path === selectedPath" class="material-icons" style="font-size: 1.1rem">arrow_right</span>
|
||||
<span v-if="dir.path === selectedPath" class="material-symbols" style="font-size: 1.1rem">arrow_right</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/2 h-full overflow-y-auto">
|
||||
<div v-for="dir in _subdirs" :key="dir.path" :class="dir.className" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" @click="selectSubDir(dir)">
|
||||
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<span class="material-symbols fill bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,7 +162,7 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to get filesystem paths', error)
|
||||
this.$toast.error('Failed to get filesystem paths')
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
return []
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -199,4 +199,4 @@ export default {
|
||||
height: calc(100% - 130px);
|
||||
min-height: calc(100% - 130px);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<p class="text-sm text-gray-300 pr-2">{{ $strings.LabelMetadataOrderOfPrecedenceDescription }}</p>
|
||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex">
|
||||
<a href="https://www.audiobookshelf.org/guides/book-scanner" target="_blank" class="inline-flex">
|
||||
<span class="material-icons text-xl w-5">help_outline</span>
|
||||
<span class="material-symbols text-xl w-5">help_outline</span>
|
||||
</a>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -17,7 +17,7 @@
|
||||
<draggable v-model="metadataSourceMapped" v-bind="dragOptions" class="list-group" draggable=".item" handle=".drag-handle" tag="ul" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
||||
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
||||
<li v-for="(source, index) in metadataSourceMapped" :key="source.id" :class="source.include ? 'item' : 'opacity-50'" class="w-full px-2 flex items-center relative border border-white/10">
|
||||
<span class="material-icons drag-handle text-xl text-gray-400 hover:text-gray-50 mr-2 md:mr-4">reorder</span>
|
||||
<span class="material-symbols drag-handle text-xl text-gray-400 hover:text-gray-50 mr-2 md:mr-4">reorder</span>
|
||||
<div class="text-center py-1 w-8 min-w-8">
|
||||
{{ source.include ? getSourceIndex(source.id) : '' }}
|
||||
</div>
|
||||
|
||||
@@ -1,78 +1,94 @@
|
||||
<template>
|
||||
<div class="w-full h-full px-1 md:px-4 py-1 mb-4">
|
||||
<div class="flex items-center py-3">
|
||||
<ui-toggle-switch v-model="useSquareBookCovers" @input="formUpdated" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsSquareBookCoversHelp">
|
||||
<p class="pl-4 text-base">
|
||||
{{ $strings.LabelSettingsSquareBookCovers }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<div class="py-3">
|
||||
<div class="flex items-center">
|
||||
<ui-toggle-switch v-if="!globalWatcherDisabled" v-model="enableWatcher" @input="formUpdated" />
|
||||
<ui-toggle-switch v-else disabled :value="false" />
|
||||
<p class="pl-4 text-base">{{ $strings.LabelSettingsEnableWatcherForLibrary }}</p>
|
||||
</div>
|
||||
<p v-if="globalWatcherDisabled" class="text-xs text-warning">*{{ $strings.MessageWatcherIsDisabledGlobally }}</p>
|
||||
</div>
|
||||
<div v-if="isBookLibrary" class="flex items-center py-3">
|
||||
<ui-toggle-switch v-model="audiobooksOnly" @input="formUpdated" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsAudiobooksOnlyHelp">
|
||||
<p class="pl-4 text-base">
|
||||
{{ $strings.LabelSettingsAudiobooksOnly }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<div v-if="isBookLibrary" class="py-3">
|
||||
<div class="flex items-center">
|
||||
<ui-toggle-switch v-model="skipMatchingMediaWithAsin" @input="formUpdated" />
|
||||
<p class="pl-4 text-base">{{ $strings.LabelSettingsSkipMatchingBooksWithASIN }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isBookLibrary" class="py-3">
|
||||
<div class="flex items-center">
|
||||
<ui-toggle-switch v-model="skipMatchingMediaWithIsbn" @input="formUpdated" />
|
||||
<p class="pl-4 text-base">{{ $strings.LabelSettingsSkipMatchingBooksWithISBN }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isBookLibrary" class="py-3">
|
||||
<div class="flex items-center">
|
||||
<ui-toggle-switch v-model="hideSingleBookSeries" @input="formUpdated" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsHideSingleBookSeriesHelp">
|
||||
<p class="pl-4 text-base">
|
||||
{{ $strings.LabelSettingsHideSingleBookSeries }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<div class="flex flex-wrap">
|
||||
<div class="flex items-center p-2 w-full md:w-1/2">
|
||||
<ui-toggle-switch v-model="useSquareBookCovers" size="sm" @input="formUpdated" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsSquareBookCoversHelp">
|
||||
<p class="pl-4 text-sm">
|
||||
{{ $strings.LabelSettingsSquareBookCovers }}
|
||||
<span class="material-symbols icon-text text-sm">info</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isBookLibrary" class="py-3">
|
||||
<div class="flex items-center">
|
||||
<ui-toggle-switch v-model="onlyShowLaterBooksInContinueSeries" @input="formUpdated" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp">
|
||||
<p class="pl-4 text-base">
|
||||
{{ $strings.LabelSettingsOnlyShowLaterBooksInContinueSeries }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
<div class="p-2 w-full md:w-1/2">
|
||||
<div class="flex items-center">
|
||||
<ui-toggle-switch v-if="!globalWatcherDisabled" v-model="enableWatcher" size="sm" @input="formUpdated" />
|
||||
<ui-toggle-switch v-else disabled size="sm" :value="false" />
|
||||
<p class="pl-4 text-sm">{{ $strings.LabelSettingsEnableWatcherForLibrary }}</p>
|
||||
</div>
|
||||
<p v-if="globalWatcherDisabled" class="text-xs text-warning">*{{ $strings.MessageWatcherIsDisabledGlobally }}</p>
|
||||
</div>
|
||||
<div v-if="isBookLibrary" class="flex items-center p-2 w-full md:w-1/2">
|
||||
<ui-toggle-switch v-model="audiobooksOnly" size="sm" @input="formUpdated" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsAudiobooksOnlyHelp">
|
||||
<p class="pl-4 text-sm">
|
||||
{{ $strings.LabelSettingsAudiobooksOnly }}
|
||||
<span class="material-symbols icon-text text-sm">info</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isBookLibrary" class="py-3">
|
||||
<div class="flex items-center">
|
||||
<ui-toggle-switch v-model="epubsAllowScriptedContent" @input="formUpdated" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsEpubsAllowScriptedContentHelp">
|
||||
<p class="pl-4 text-base">
|
||||
{{ $strings.LabelSettingsEpubsAllowScriptedContent }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
<div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
|
||||
<div class="flex items-center">
|
||||
<ui-toggle-switch v-model="skipMatchingMediaWithAsin" size="sm" @input="formUpdated" />
|
||||
<p class="pl-4 text-sm">{{ $strings.LabelSettingsSkipMatchingBooksWithASIN }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
|
||||
<div class="flex items-center">
|
||||
<ui-toggle-switch v-model="skipMatchingMediaWithIsbn" size="sm" @input="formUpdated" />
|
||||
<p class="pl-4 text-sm">{{ $strings.LabelSettingsSkipMatchingBooksWithISBN }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
|
||||
<div class="flex items-center">
|
||||
<ui-toggle-switch v-model="hideSingleBookSeries" size="sm" @input="formUpdated" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsHideSingleBookSeriesHelp">
|
||||
<p class="pl-4 text-sm">
|
||||
{{ $strings.LabelSettingsHideSingleBookSeries }}
|
||||
<span class="material-symbols icon-text text-sm">info</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
|
||||
<div class="flex items-center">
|
||||
<ui-toggle-switch v-model="onlyShowLaterBooksInContinueSeries" size="sm" @input="formUpdated" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp">
|
||||
<p class="pl-4 text-sm">
|
||||
{{ $strings.LabelSettingsOnlyShowLaterBooksInContinueSeries }}
|
||||
<span class="material-symbols icon-text text-sm">info</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
|
||||
<div class="flex items-center">
|
||||
<ui-toggle-switch v-model="epubsAllowScriptedContent" size="sm" @input="formUpdated" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsEpubsAllowScriptedContentHelp">
|
||||
<p class="pl-4 text-sm">
|
||||
{{ $strings.LabelSettingsEpubsAllowScriptedContent }}
|
||||
<span class="material-symbols icon-text text-sm">info</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isPodcastLibrary" class="p-2 w-full md:w-1/2">
|
||||
<ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-72" menu-max-height="200px" @input="formUpdated" />
|
||||
</div>
|
||||
<div class="p-2 w-full flex items-center space-x-2 flex-wrap">
|
||||
<div>
|
||||
<ui-dropdown v-model="markAsFinishedWhen" :items="maskAsFinishedWhenItems" :label="$strings.LabelSettingsLibraryMarkAsFinishedWhen" small class="w-72 min-w-72 text-sm" menu-max-height="200px" @input="markAsFinishedWhenChanged" />
|
||||
</div>
|
||||
<div class="w-16">
|
||||
<div>
|
||||
<label class="px-1 text-sm font-semibold"></label>
|
||||
<div class="relative">
|
||||
<ui-text-input v-model="markAsFinishedValue" type="number" label="" no-spinner custom-input-class="pr-5" @input="markAsFinishedChanged" />
|
||||
<div class="absolute top-0 bottom-0 right-4 flex items-center">{{ markAsFinishedWhen === 'timeRemaining' ? '' : '%' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isPodcastLibrary" class="py-3">
|
||||
<ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-72" menu-max-height="200px" @input="formUpdated" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -97,7 +113,9 @@ export default {
|
||||
epubsAllowScriptedContent: false,
|
||||
hideSingleBookSeries: false,
|
||||
onlyShowLaterBooksInContinueSeries: false,
|
||||
podcastSearchRegion: 'us'
|
||||
podcastSearchRegion: 'us',
|
||||
markAsFinishedWhen: 'timeRemaining',
|
||||
markAsFinishedValue: 10
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -119,10 +137,34 @@ export default {
|
||||
providers() {
|
||||
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
|
||||
return this.$store.state.scanners.providers
|
||||
},
|
||||
maskAsFinishedWhenItems() {
|
||||
return [
|
||||
{
|
||||
text: this.$strings.LabelSettingsLibraryMarkAsFinishedTimeRemaining,
|
||||
value: 'timeRemaining'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelSettingsLibraryMarkAsFinishedPercentComplete,
|
||||
value: 'percentComplete'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
markAsFinishedWhenChanged(val) {
|
||||
if (val === 'percentComplete' && this.markAsFinishedValue > 100) {
|
||||
this.markAsFinishedValue = 100
|
||||
}
|
||||
this.formUpdated()
|
||||
},
|
||||
markAsFinishedChanged(val) {
|
||||
this.formUpdated()
|
||||
},
|
||||
getLibraryData() {
|
||||
let markAsFinishedTimeRemaining = this.markAsFinishedWhen === 'timeRemaining' ? Number(this.markAsFinishedValue) : null
|
||||
let markAsFinishedPercentComplete = this.markAsFinishedWhen === 'percentComplete' ? Number(this.markAsFinishedValue) : null
|
||||
|
||||
return {
|
||||
settings: {
|
||||
coverAspectRatio: this.useSquareBookCovers ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD,
|
||||
@@ -133,7 +175,9 @@ export default {
|
||||
epubsAllowScriptedContent: !!this.epubsAllowScriptedContent,
|
||||
hideSingleBookSeries: !!this.hideSingleBookSeries,
|
||||
onlyShowLaterBooksInContinueSeries: !!this.onlyShowLaterBooksInContinueSeries,
|
||||
podcastSearchRegion: this.podcastSearchRegion
|
||||
podcastSearchRegion: this.podcastSearchRegion,
|
||||
markAsFinishedTimeRemaining: markAsFinishedTimeRemaining,
|
||||
markAsFinishedPercentComplete: markAsFinishedPercentComplete
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -150,6 +194,11 @@ export default {
|
||||
this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries
|
||||
this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries
|
||||
this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us'
|
||||
this.markAsFinishedWhen = this.librarySettings.markAsFinishedTimeRemaining ? 'timeRemaining' : 'percentComplete'
|
||||
if (!this.librarySettings.markAsFinishedTimeRemaining && !this.librarySettings.markAsFinishedPercentComplete) {
|
||||
this.markAsFinishedWhen = 'timeRemaining'
|
||||
}
|
||||
this.markAsFinishedValue = this.librarySettings.markAsFinishedTimeRemaining || this.librarySettings.markAsFinishedPercentComplete || 10
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<div class="w-full border border-black-200 p-4 my-8">
|
||||
<div class="flex flex-wrap items-center">
|
||||
<div>
|
||||
<p class="text-lg">Remove metadata files in library item folders</p>
|
||||
<p class="max-w-sm text-sm pt-2 text-gray-300">Remove all metadata.json or metadata.abs files in your {{ mediaType }} folders</p>
|
||||
<p class="text-lg">{{ $strings.LabelRemoveMetadataFile }}</p>
|
||||
<p class="max-w-sm text-sm pt-2 text-gray-300">{{ $getString('LabelRemoveMetadataFileHelp', [mediaType]) }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div>
|
||||
<ui-btn class="mb-4 block" @click.stop="removeAllMetadataClick('json')">Remove all metadata.json</ui-btn>
|
||||
<ui-btn @click.stop="removeAllMetadataClick('abs')">Remove all metadata.abs</ui-btn>
|
||||
<ui-btn class="mb-4 block" @click.stop="removeAllMetadataClick('json')">{{ $strings.LabelRemoveAllMetadataJson }}</ui-btn>
|
||||
<ui-btn @click.stop="removeAllMetadataClick('abs')">{{ $strings.LabelRemoveAllMetadataAbs }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,7 +43,7 @@ export default {
|
||||
methods: {
|
||||
removeAllMetadataClick(ext) {
|
||||
const payload = {
|
||||
message: `Are you sure you want to remove all metadata.${ext} files in your library item folders?`,
|
||||
message: this.$getString('MessageConfirmRemoveMetadataFiles', [ext]),
|
||||
persistent: true,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
@@ -60,16 +60,16 @@ export default {
|
||||
.$post(`/api/libraries/${this.libraryId}/remove-metadata?ext=${ext}`)
|
||||
.then((data) => {
|
||||
if (!data.found) {
|
||||
this.$toast.info(`No metadata.${ext} files were found in library`)
|
||||
this.$toast.info(this.$getString('ToastMetadataFilesRemovedNoneFound', [ext]))
|
||||
} else if (!data.removed) {
|
||||
this.$toast.success(`No metadata.${ext} files removed`)
|
||||
this.$toast.success(this.$getString('ToastMetadataFilesRemovedNoneRemoved', [ext]))
|
||||
} else {
|
||||
this.$toast.success(`Successfully removed ${data.removed} metadata.${ext} files`)
|
||||
this.$toast.success(this.$getString('ToastMetadataFilesRemovedSuccess', [data.removed, ext]))
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove metadata files', error)
|
||||
this.$toast.error('Failed to remove metadata files')
|
||||
this.$toast.error(this.$getString('ToastMetadataFilesRemovedError', [ext]))
|
||||
})
|
||||
.finally(() => {
|
||||
this.$emit('update:processing', false)
|
||||
@@ -78,4 +78,4 @@ export default {
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -77,7 +77,13 @@ export default {
|
||||
return this.notificationData.events || []
|
||||
},
|
||||
eventOptions() {
|
||||
return this.notificationEvents.map((e) => ({ value: e.name, text: e.name, subtext: e.description }))
|
||||
return this.notificationEvents.map((e) => {
|
||||
return {
|
||||
value: e.name,
|
||||
text: e.name,
|
||||
subtext: this.$strings[e.descriptionKey] || e.description
|
||||
}
|
||||
})
|
||||
},
|
||||
selectedEventData() {
|
||||
return this.notificationEvents.find((e) => e.name === this.newNotification.eventName)
|
||||
@@ -86,7 +92,7 @@ export default {
|
||||
return this.selectedEventData && this.selectedEventData.requiresLibrary
|
||||
},
|
||||
title() {
|
||||
return this.isNew ? 'Create Notification' : 'Update Notification'
|
||||
return this.isNew ? this.$strings.HeaderNotificationCreate : this.$strings.HeaderNotificationUpdate
|
||||
},
|
||||
availableVariables() {
|
||||
return this.selectedEventData ? this.selectedEventData.variables || null : null
|
||||
@@ -104,9 +110,9 @@ export default {
|
||||
},
|
||||
submitForm() {
|
||||
this.$refs.urlsInput?.forceBlur()
|
||||
|
||||
|
||||
if (!this.newNotification.urls.length) {
|
||||
this.$toast.error('Must enter an Apprise URL')
|
||||
this.$toast.error(this.$strings.ToastAppriseUrlRequired)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -127,12 +133,12 @@ export default {
|
||||
.$patch(`/api/notifications/${payload.id}`, payload)
|
||||
.then((updatedSettings) => {
|
||||
this.$emit('update', updatedSettings)
|
||||
this.$toast.success('Notification updated')
|
||||
this.$toast.success(this.$strings.ToastNotificationUpdateSuccess)
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update notification', error)
|
||||
this.$toast.error('Failed to update notification')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
@@ -149,12 +155,11 @@ export default {
|
||||
.$post('/api/notifications', payload)
|
||||
.then((updatedSettings) => {
|
||||
this.$emit('update', updatedSettings)
|
||||
this.$toast.success('Notification created')
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to create notification', error)
|
||||
this.$toast.error('Failed to create notification')
|
||||
this.$toast.error(this.$strings.ToastNotificationCreateFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
<p v-if="isOpenInPlayer" class="text-sm text-right text-gray-400">{{ $strings.ButtonPlaying }}</p>
|
||||
<div v-else-if="isHovering" class="flex items-center justify-end -mx-1">
|
||||
<button class="outline-none mx-1 flex items-center" @click.stop="playClick">
|
||||
<span class="material-icons text-2xl text-success">play_arrow</span>
|
||||
<span class="material-symbols fill text-2xl text-success">play_arrow</span>
|
||||
</button>
|
||||
<button class="outline-none mx-1 flex items-center" @click.stop="removeClick">
|
||||
<span class="material-icons text-2xl text-error">close</span>
|
||||
<span class="material-symbols text-2xl text-error">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-else class="text-gray-400 text-sm text-right">{{ durationPretty }}</p>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="flex-grow" />
|
||||
<ui-checkbox v-model="playerQueueAutoPlay" label="Auto Play" medium checkbox-bg="primary" border-color="gray-600" label-class="pl-2 mb-px" />
|
||||
</div>
|
||||
<modals-player-queue-item-row v-for="(item, index) in playerQueueItems" :key="index" :item="item" :index="index" @play="playItem" @remove="removeItem" />
|
||||
<modals-player-queue-item-row v-for="(item, index) in playerQueueItems" :key="index" :item="item" :index="index" @play="playItem(index)" @remove="removeItem" />
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
@@ -22,8 +22,7 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: Boolean,
|
||||
libraryItemId: String
|
||||
value: Boolean
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
@@ -50,11 +49,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
playItem(item) {
|
||||
this.$eventBus.$emit('play-item', {
|
||||
libraryItemId: item.libraryItemId,
|
||||
episodeId: item.episodeId || null,
|
||||
queueItems: this.playerQueueItems
|
||||
playItem(index) {
|
||||
this.$eventBus.$emit('play-queue-item', {
|
||||
index
|
||||
})
|
||||
this.show = false
|
||||
},
|
||||
@@ -63,4 +60,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -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,12 +140,11 @@ export default {
|
||||
.$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects })
|
||||
.then((updatedPlaylist) => {
|
||||
console.log(`Items removed from playlist`, updatedPlaylist)
|
||||
this.$toast.success('Playlist item(s) removed')
|
||||
this.processing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove items from playlist', error)
|
||||
this.$toast.error('Failed to remove playlist item(s)')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
@@ -148,12 +157,11 @@ export default {
|
||||
.$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects })
|
||||
.then((updatedPlaylist) => {
|
||||
console.log(`Items added to playlist`, updatedPlaylist)
|
||||
this.$toast.success('Items added to playlist')
|
||||
this.processing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to add items to playlist', error)
|
||||
this.$toast.error('Failed to add items to playlist')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
@@ -174,14 +182,13 @@ export default {
|
||||
.$post('/api/playlists', newPlaylist)
|
||||
.then((data) => {
|
||||
console.log('New playlist created', data)
|
||||
this.$toast.success(`Playlist "${data.name}" created`)
|
||||
this.processing = false
|
||||
this.newPlaylistName = ''
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to create playlist', error)
|
||||
var errMsg = error.response ? error.response.data || '' : ''
|
||||
this.$toast.error(`Failed to create playlist: ${errMsg}`)
|
||||
this.$toast.error(this.$strings.ToastPlaylistCreateFailed + ': ' + errMsg)
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export default {
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove playlist', error)
|
||||
this.processing = false
|
||||
this.$toast.error(this.$strings.ToastPlaylistRemoveFailed)
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -95,7 +95,7 @@ export default {
|
||||
return
|
||||
}
|
||||
if (!this.newPlaylistName) {
|
||||
return this.$toast.error('Playlist must have a name')
|
||||
return this.$toast.error(this.$strings.ToastNameRequired)
|
||||
}
|
||||
|
||||
this.processing = true
|
||||
@@ -115,7 +115,7 @@ export default {
|
||||
.catch((error) => {
|
||||
console.error('Failed to update playlist', error)
|
||||
this.processing = false
|
||||
this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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" />
|
||||
@@ -8,8 +8,8 @@
|
||||
<nuxt-link :to="`/playlist/${playlist.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ playlist.name }}</nuxt-link>
|
||||
</div>
|
||||
<div class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
||||
<ui-btn v-if="!isItemIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-icons text-2xl pt-px">add</span></ui-btn>
|
||||
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-icons text-2xl pt-px">remove</span></ui-btn>
|
||||
<ui-btn v-if="!isItemIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-symbols text-2xl pt-px">add</span></ui-btn>
|
||||
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-symbols text-2xl pt-px">remove</span></ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
</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-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevEpisode" @mousedown.prevent>arrow_back_ios</div>
|
||||
<div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevEpisode" @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-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextEpisode" @mousedown.prevent>arrow_forward_ios</div>
|
||||
<div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextEpisode" @mousedown.prevent>arrow_forward_ios</div>
|
||||
</div>
|
||||
|
||||
<div ref="wrapper" class="p-4 w-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
@click="toggleSelectEpisode(episode)"
|
||||
>
|
||||
<div class="absolute top-0 left-0 h-full flex items-center p-2">
|
||||
<span v-if="getIsEpisodeDownloaded(episode)" class="material-icons text-success text-xl">download_done</span>
|
||||
<span v-if="getIsEpisodeDownloaded(episode)" class="material-symbols text-success text-xl">download_done</span>
|
||||
<ui-checkbox v-else v-model="selectedEpisodes[episode.cleanUrl]" small checkbox-bg="primary" border-color="gray-600" />
|
||||
</div>
|
||||
<div class="px-8 py-2">
|
||||
|
||||
@@ -156,7 +156,12 @@ export default {
|
||||
return this.selectedFolder.fullPath
|
||||
},
|
||||
podcastTypes() {
|
||||
return this.$store.state.globals.podcastTypes || []
|
||||
return this.$store.state.globals.podcastTypes.map((e) => {
|
||||
return {
|
||||
text: this.$strings[e.descriptionKey] || e.text,
|
||||
value: e.value
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -16,11 +16,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-lg font-semibold mb-2">{{ $strings.HeaderPodcastsToAdd }}</p>
|
||||
<p class="text-lg font-semibold mb-1">{{ $strings.HeaderPodcastsToAdd }}</p>
|
||||
<p class="text-sm text-gray-300 mb-4">{{ $strings.MessageOpmlPreviewNote }}</p>
|
||||
|
||||
<div class="w-full overflow-y-auto" style="max-height: 50vh">
|
||||
<template v-for="(feed, index) in feedMetadata">
|
||||
<cards-podcast-feed-summary-card :key="index" :feed="feed" :library-folder-path="selectedFolderPath" class="my-1" />
|
||||
<template v-for="(feed, index) in feeds">
|
||||
<div :key="index" class="py-1 flex items-center">
|
||||
<p class="text-lg font-semibold">{{ index + 1 }}.</p>
|
||||
<div class="pl-2">
|
||||
<p v-if="feed.title" class="text-sm font-semibold">{{ feed.title }}</p>
|
||||
<p class="text-xs text-gray-400">{{ feed.feedUrl }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,9 +52,7 @@ export default {
|
||||
return {
|
||||
processing: false,
|
||||
selectedFolderId: null,
|
||||
fullPath: null,
|
||||
autoDownloadEpisodes: false,
|
||||
feedMetadata: []
|
||||
autoDownloadEpisodes: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -96,73 +101,36 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toFeedMetadata(feed) {
|
||||
const metadata = feed.metadata
|
||||
return {
|
||||
title: metadata.title,
|
||||
author: metadata.author,
|
||||
description: metadata.description,
|
||||
releaseDate: '',
|
||||
genres: [...metadata.categories],
|
||||
feedUrl: metadata.feedUrl,
|
||||
imageUrl: metadata.image,
|
||||
itunesPageUrl: '',
|
||||
itunesId: '',
|
||||
itunesArtistId: '',
|
||||
language: '',
|
||||
numEpisodes: feed.numEpisodes
|
||||
}
|
||||
},
|
||||
init() {
|
||||
this.feedMetadata = this.feeds.map(this.toFeedMetadata)
|
||||
|
||||
if (this.folderItems[0]) {
|
||||
this.selectedFolderId = this.folderItems[0].value
|
||||
}
|
||||
},
|
||||
async submit() {
|
||||
this.processing = true
|
||||
const newFeedPayloads = this.feedMetadata.map((metadata) => {
|
||||
return {
|
||||
path: `${this.selectedFolderPath}/${this.$sanitizeFilename(metadata.title)}`,
|
||||
folderId: this.selectedFolderId,
|
||||
libraryId: this.currentLibrary.id,
|
||||
media: {
|
||||
metadata: {
|
||||
...metadata
|
||||
},
|
||||
autoDownloadEpisodes: this.autoDownloadEpisodes
|
||||
}
|
||||
}
|
||||
})
|
||||
console.log('New feed payloads', newFeedPayloads)
|
||||
|
||||
for (const podcastPayload of newFeedPayloads) {
|
||||
await this.$axios
|
||||
.$post('/api/podcasts', podcastPayload)
|
||||
.then(() => {
|
||||
this.$toast.success(`${podcastPayload.media.metadata.title}: ${this.$strings.ToastPodcastCreateSuccess}`)
|
||||
})
|
||||
.catch((error) => {
|
||||
var errorMsg = error.response && error.response.data ? error.response.data : this.$strings.ToastPodcastCreateFailed
|
||||
console.error('Failed to create podcast', podcastPayload, error)
|
||||
this.$toast.error(`${podcastPayload.media.metadata.title}: ${errorMsg}`)
|
||||
})
|
||||
const payload = {
|
||||
feeds: this.feeds.map((f) => f.feedUrl),
|
||||
folderId: this.selectedFolderId,
|
||||
libraryId: this.currentLibrary.id,
|
||||
autoDownloadEpisodes: this.autoDownloadEpisodes
|
||||
}
|
||||
this.processing = false
|
||||
this.show = false
|
||||
this.$axios
|
||||
.$post('/api/podcasts/opml/create', payload)
|
||||
.then(() => {
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMsg = error.response?.data || this.$strings.ToastPodcastCreateFailed
|
||||
console.error('Failed to create podcast', payload, error)
|
||||
this.$toast.error(errorMsg)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#podcast-wrapper {
|
||||
min-height: 400px;
|
||||
max-height: 80vh;
|
||||
}
|
||||
#episodes-scroll {
|
||||
max-height: calc(80vh - 200px);
|
||||
}
|
||||
</style>
|
||||
@@ -16,8 +16,25 @@
|
||||
</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" />
|
||||
|
||||
<div class="flex items-center">
|
||||
<div class="flex-grow">
|
||||
<p class="font-semibold text-xs mb-1">{{ $strings.LabelFilename }}</p>
|
||||
<p class="mb-2 text-xs">
|
||||
{{ audioFileFilename }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<p class="font-semibold text-xs mb-1">{{ $strings.LabelSize }}</p>
|
||||
<p class="mb-2 text-xs">
|
||||
{{ audioFileSize }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
@@ -54,7 +71,7 @@ export default {
|
||||
return this.episode.description || ''
|
||||
},
|
||||
media() {
|
||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||
return this.libraryItem?.media || {}
|
||||
},
|
||||
mediaMetadata() {
|
||||
return this.media.metadata || {}
|
||||
@@ -65,6 +82,14 @@ export default {
|
||||
podcastAuthor() {
|
||||
return this.mediaMetadata.author
|
||||
},
|
||||
audioFileFilename() {
|
||||
return this.episode.audioFile?.metadata?.filename || ''
|
||||
},
|
||||
audioFileSize() {
|
||||
const size = this.episode.audioFile?.metadata?.size || 0
|
||||
|
||||
return this.$bytesPretty(size)
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
@@ -33,11 +33,11 @@
|
||||
</div>
|
||||
<div v-if="enclosureUrl" class="pb-4 pt-6">
|
||||
<ui-text-input-with-label :value="enclosureUrl" readonly class="text-xs">
|
||||
<label class="px-1 text-xs text-gray-200 font-semibold">Episode URL from RSS feed</label>
|
||||
<label class="px-1 text-xs text-gray-200 font-semibold">{{ $strings.LabelEpisodeUrlFromRssFeed }}</label>
|
||||
</ui-text-input-with-label>
|
||||
</div>
|
||||
<div v-else class="py-4">
|
||||
<p class="text-xs text-gray-300 font-semibold">Episode not linked to RSS feed episode</p>
|
||||
<p class="text-xs text-gray-300 font-semibold">{{ $strings.LabelEpisodeNotLinkedToRssFeed }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -97,7 +97,12 @@ export default {
|
||||
return this.enclosure.url
|
||||
},
|
||||
episodeTypes() {
|
||||
return this.$store.state.globals.episodeTypes || []
|
||||
return this.$store.state.globals.episodeTypes.map((e) => {
|
||||
return {
|
||||
text: this.$strings[e.descriptionKey] || e.text,
|
||||
value: e.value
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -140,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('No changes were made')
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
return false
|
||||
}
|
||||
|
||||
return this.updateDetails(updatedDetails)
|
||||
},
|
||||
async updateDetails(updatedDetails) {
|
||||
@@ -152,19 +164,16 @@ export default {
|
||||
const updateResult = await this.$axios.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatedDetails).catch((error) => {
|
||||
console.error('Failed update episode', error)
|
||||
this.isProcessing = false
|
||||
this.$toast.error(error?.response?.data || 'Failed to update episode')
|
||||
this.$toast.error(error?.response?.data || this.$strings.ToastFailedToUpdate)
|
||||
return false
|
||||
})
|
||||
|
||||
this.isProcessing = false
|
||||
if (updateResult) {
|
||||
if (updateResult) {
|
||||
this.$toast.success('Podcast episode updated')
|
||||
return true
|
||||
} else {
|
||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||
}
|
||||
this.$toast.success(this.$strings.ToastItemUpdateSuccess)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user