mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-03 17:30:39 +02:00
Compare commits
762 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b816c0e7c4 | |||
| a8b92819d1 | |||
| 54a4b09592 | |||
| f13283b950 | |||
| 78994b3589 | |||
| 6745efc4d6 | |||
| 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 | |||
| 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 | |||
| 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 |
@@ -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. -->
|
||||||
@@ -1,11 +1,25 @@
|
|||||||
name: "CodeQL"
|
name: 'CodeQL'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
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:
|
pull_request:
|
||||||
# The branches below must be a subset of the branches above
|
# 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:
|
schedule:
|
||||||
- cron: '16 5 * * 4'
|
- cron: '16 5 * * 4'
|
||||||
|
|
||||||
@@ -21,45 +35,44 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
language: [ 'javascript' ]
|
language: ['javascript']
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||||
# Use only 'java' to analyze code written in Java, Kotlin or both
|
# Use only 'java' to analyze code written in Java, Kotlin or both
|
||||||
# Use only 'javascript' to analyze code written in JavaScript, TypeScript 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
|
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# 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.
|
# 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.
|
# 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
|
# 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
|
# 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).
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
- name: Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@v2
|
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 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.
|
# - run: |
|
||||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
# echo "Run, Build Application using script"
|
||||||
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
# - run: |
|
- name: Perform CodeQL Analysis
|
||||||
# echo "Run, Build Application using script"
|
uses: github/codeql-action/analyze@v2
|
||||||
# ./location_of_script_within_repo/buildscript.sh
|
with:
|
||||||
|
category: '/language:${{matrix.language}}'
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v2
|
|
||||||
with:
|
|
||||||
category: "/language:${{matrix.language}}"
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ jobs:
|
|||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
tags: ${{ github.event.inputs.tags || steps.meta.outputs.tags }}
|
tags: ${{ github.event.inputs.tags || steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches-ignore:
|
branches-ignore:
|
||||||
- 'dependabot/**' # Don't run dependabot branches, as they are already covered by pull requests
|
- '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:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
/podcasts/
|
/podcasts/
|
||||||
/media/
|
/media/
|
||||||
/metadata/
|
/metadata/
|
||||||
|
/plugins/
|
||||||
/client/.nuxt/
|
/client/.nuxt/
|
||||||
/client/dist/
|
/client/dist/
|
||||||
/dist/
|
/dist/
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
/ffmpeg*
|
/ffmpeg*
|
||||||
/ffprobe*
|
/ffprobe*
|
||||||
/unicode*
|
/unicode*
|
||||||
|
/libnusqlite3*
|
||||||
|
|
||||||
sw.*
|
sw.*
|
||||||
.DS_STORE
|
.DS_STORE
|
||||||
|
|||||||
+29
-9
@@ -11,25 +11,45 @@ FROM node:20-alpine
|
|||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add --no-cache --update \
|
apk add --no-cache --update \
|
||||||
curl \
|
curl \
|
||||||
tzdata \
|
tzdata \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
make \
|
make \
|
||||||
gcompat \
|
python3 \
|
||||||
python3 \
|
g++ \
|
||||||
g++ \
|
tini \
|
||||||
tini
|
unzip
|
||||||
|
|
||||||
COPY --from=build /client/dist /client/dist
|
COPY --from=build /client/dist /client/dist
|
||||||
COPY index.js package* /
|
COPY index.js package* /
|
||||||
COPY server server
|
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 npm ci --only=production
|
||||||
|
|
||||||
RUN apk del make python3 g++
|
RUN apk del make python3 g++
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
|
ENV PORT=80
|
||||||
|
ENV CONFIG_PATH="/config"
|
||||||
|
ENV METADATA_PATH="/metadata"
|
||||||
|
ENV SOURCE="docker"
|
||||||
|
|
||||||
ENTRYPOINT ["tini", "--"]
|
ENTRYPOINT ["tini", "--"]
|
||||||
CMD ["node", "index.js"]
|
CMD ["node", "index.js"]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
@import './absicons.css';
|
@import './absicons.css';
|
||||||
|
|
||||||
:root {
|
: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%);
|
--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%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,4 +247,4 @@ Bookshelf Label
|
|||||||
|
|
||||||
.abs-btn:disabled::before {
|
.abs-btn:disabled::before {
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-16 bg-primary relative">
|
<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">
|
<div class="flex h-full items-center">
|
||||||
<nuxt-link to="/">
|
<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" />
|
<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" />
|
||||||
@@ -264,7 +264,6 @@ export default {
|
|||||||
libraryItems.forEach((item) => {
|
libraryItems.forEach((item) => {
|
||||||
let subtitle = ''
|
let subtitle = ''
|
||||||
if (item.mediaType === 'book') subtitle = item.media.metadata.authors.map((au) => au.name).join(', ')
|
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({
|
queueItems.push({
|
||||||
libraryItemId: item.id,
|
libraryItemId: item.id,
|
||||||
libraryId: item.libraryId,
|
libraryId: item.libraryId,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24e">
|
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24e">
|
||||||
<template v-for="(shelf, index) in supportedShelves">
|
<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)">
|
<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>
|
</widgets-item-slider>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -347,6 +347,13 @@ export default {
|
|||||||
libraryItemsAdded(libraryItems) {
|
libraryItemsAdded(libraryItems) {
|
||||||
console.log('libraryItems added', 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')
|
const recentlyAddedShelf = this.shelves.find((shelf) => shelf.id === 'recently-added')
|
||||||
if (!recentlyAddedShelf) return
|
if (!recentlyAddedShelf) return
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="shelf.type === 'authors'" class="flex items-center">
|
<div v-if="shelf.type === 'authors'" class="flex items-center">
|
||||||
<template v-for="entity in shelf.entities">
|
<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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="shelf.type === 'narrators'" class="flex items-center">
|
<div v-if="shelf.type === 'narrators'" class="flex items-center">
|
||||||
@@ -37,18 +37,18 @@
|
|||||||
<div class="relative">
|
<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="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` }">
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="bookshelfDividerCategorized h-6e w-full absolute top-0 left-0 right-0 z-20"></div>
|
<div class="bookshelfDividerCategorized h-6e w-full absolute top-0 left-0 right-0 z-20"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="canScrollLeft && !isScrolling" class="hidden sm:flex absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollLeft">
|
<button v-show="canScrollLeft && !isScrolling" :aria-label="$strings.ButtonScrollLeft" class="hidden sm:flex absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollLeft">
|
||||||
<span class="material-symbols text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_left</span>
|
<span class="material-symbols text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_left</span>
|
||||||
</div>
|
</button>
|
||||||
<div v-show="canScrollRight && !isScrolling" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollRight">
|
<button 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>
|
<span class="material-symbols text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_right</span>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
|
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
|
||||||
<span v-else class="material-symbols text-lg"></span>
|
<span v-else class="material-symbols text-lg"></span>
|
||||||
</nuxt-link>
|
</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>
|
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
|
||||||
<svg v-else class="w-5 h-5" viewBox="0 0 24 24">
|
<svg v-else class="w-5 h-5" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
@@ -42,15 +42,18 @@
|
|||||||
<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'">
|
<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>
|
<p class="text-sm">{{ $strings.ButtonAdd }}</p>
|
||||||
</nuxt-link>
|
</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>
|
||||||
<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 -->
|
<!-- Series books page -->
|
||||||
<template v-if="selectedSeries">
|
<template v-if="selectedSeries">
|
||||||
<p class="pl-2 text-base md:text-lg">
|
<p class="pl-2 text-base md:text-lg">
|
||||||
{{ seriesName }}
|
{{ seriesName }}
|
||||||
</p>
|
</p>
|
||||||
<div class="w-6 h-6 rounded-full bg-black bg-opacity-30 flex items-center justify-center ml-3">
|
<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>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
@@ -62,8 +65,8 @@
|
|||||||
<ui-context-menu-dropdown v-if="!isBatchSelecting && seriesContextMenuItems.length" :items="seriesContextMenuItems" class="mx-px" @action="seriesContextMenuAction" />
|
<ui-context-menu-dropdown v-if="!isBatchSelecting && seriesContextMenuItems.length" :items="seriesContextMenuItems" class="mx-px" @action="seriesContextMenuAction" />
|
||||||
</template>
|
</template>
|
||||||
<!-- library & collections page -->
|
<!-- library & collections page -->
|
||||||
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
|
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome && !isAuthorsPage">
|
||||||
<p class="hidden md:block">{{ numShowing }} {{ entityName }}</p>
|
<p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p>
|
||||||
|
|
||||||
<div class="flex-grow hidden sm:inline-block" />
|
<div class="flex-grow hidden sm:inline-block" />
|
||||||
|
|
||||||
@@ -80,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" />
|
<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 -->
|
<!-- 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" />
|
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
|
||||||
</template>
|
</template>
|
||||||
@@ -92,12 +95,14 @@
|
|||||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
|
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
|
||||||
</template>
|
</template>
|
||||||
<!-- authors page -->
|
<!-- authors page -->
|
||||||
<template v-else-if="page === 'authors'">
|
<template v-else-if="isAuthorsPage">
|
||||||
<div class="flex-grow" />
|
<p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p>
|
||||||
<ui-btn v-if="userCanUpdate && authors?.length && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
|
|
||||||
|
<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 -->
|
<!-- author sort select -->
|
||||||
<controls-sort-select v-if="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>
|
</template>
|
||||||
<!-- home page -->
|
<!-- home page -->
|
||||||
<template v-else-if="isHome">
|
<template v-else-if="isHome">
|
||||||
@@ -117,11 +122,7 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => null
|
default: () => null
|
||||||
},
|
},
|
||||||
searchQuery: String,
|
searchQuery: String
|
||||||
authors: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -246,9 +247,6 @@ export default {
|
|||||||
isPodcastLibrary() {
|
isPodcastLibrary() {
|
||||||
return this.currentLibraryMediaType === 'podcast'
|
return this.currentLibraryMediaType === 'podcast'
|
||||||
},
|
},
|
||||||
isMusicLibrary() {
|
|
||||||
return this.currentLibraryMediaType === 'music'
|
|
||||||
},
|
|
||||||
isLibraryPage() {
|
isLibraryPage() {
|
||||||
return this.page === ''
|
return this.page === ''
|
||||||
},
|
},
|
||||||
@@ -270,8 +268,11 @@ export default {
|
|||||||
isPodcastLatestPage() {
|
isPodcastLatestPage() {
|
||||||
return this.$route.name === 'library-library-podcast-latest'
|
return this.$route.name === 'library-library-podcast-latest'
|
||||||
},
|
},
|
||||||
|
isPodcastDownloadQueuePage() {
|
||||||
|
return this.$route.name === 'library-library-podcast-download-queue'
|
||||||
|
},
|
||||||
isAuthorsPage() {
|
isAuthorsPage() {
|
||||||
return this.$route.name === 'library-library-authors'
|
return this.page === 'authors'
|
||||||
},
|
},
|
||||||
isAlbumsPage() {
|
isAlbumsPage() {
|
||||||
return this.page === 'albums'
|
return this.page === 'albums'
|
||||||
@@ -281,13 +282,13 @@ export default {
|
|||||||
},
|
},
|
||||||
entityName() {
|
entityName() {
|
||||||
if (this.isAlbumsPage) return 'Albums'
|
if (this.isAlbumsPage) return 'Albums'
|
||||||
if (this.isMusicLibrary) return 'Tracks'
|
|
||||||
|
|
||||||
if (this.isPodcastLibrary) return this.$strings.LabelPodcasts
|
if (this.isPodcastLibrary) return this.$strings.LabelPodcasts
|
||||||
if (!this.page) return this.$strings.LabelBooks
|
if (!this.page) return this.$strings.LabelBooks
|
||||||
if (this.isSeriesPage) return this.$strings.LabelSeries
|
if (this.isSeriesPage) return this.$strings.LabelSeries
|
||||||
if (this.isCollectionsPage) return this.$strings.LabelCollections
|
if (this.isCollectionsPage) return this.$strings.LabelCollections
|
||||||
if (this.isPlaylistsPage) return this.$strings.LabelPlaylists
|
if (this.isPlaylistsPage) return this.$strings.LabelPlaylists
|
||||||
|
if (this.isAuthorsPage) return this.$strings.LabelAuthors
|
||||||
return ''
|
return ''
|
||||||
},
|
},
|
||||||
seriesId() {
|
seriesId() {
|
||||||
@@ -477,42 +478,54 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to re-add series to continue listening', error)
|
console.error('Failed to re-add series to continue listening', error)
|
||||||
this.$toast.error(this.$strings.ToastItemUpdateFailed)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processingSeries = false
|
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() {
|
async matchAllAuthors() {
|
||||||
this.processingAuthors = true
|
this.processingAuthors = true
|
||||||
|
|
||||||
for (const author of this.authors) {
|
try {
|
||||||
const payload = {}
|
const authors = await this.fetchAllAuthors()
|
||||||
if (author.asin) payload.asin = author.asin
|
|
||||||
else payload.q = author.name
|
|
||||||
|
|
||||||
payload.region = 'us'
|
for (const author of authors) {
|
||||||
if (this.libraryProvider.startsWith('audible.')) {
|
const payload = {}
|
||||||
payload.region = this.libraryProvider.split('.').pop() || 'us'
|
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)
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
this.$eventBus.$emit(`searching-author-${author.id}`, true)
|
console.error('Failed to match all authors', error)
|
||||||
|
this.$toast.error(this.$strings.ToastMatchAllAuthorsFailed)
|
||||||
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.processingAuthors = false
|
this.processingAuthors = false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div role="toolbar" aria-orientation="vertical" aria-label="Config Sidebar">
|
||||||
<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="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">
|
<div v-show="isMobilePortrait" class="flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer">
|
||||||
<span class="material-symbols text-2xl">arrow_back</span>
|
<span class="material-symbols text-2xl">arrow_back</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<p class="text-xs text-gray-300 italic">{{ Source }}</p>
|
<p class="text-xs text-gray-300 italic">{{ Source }}</p>
|
||||||
</div>
|
</div>
|
||||||
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ $config.version }}</a>
|
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ versionData.latestVersion }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
<div id="bookshelf" ref="bookshelf" class="w-full overflow-y-auto" :style="{ fontSize: sizeMultiplier + 'rem' }">
|
<div id="bookshelf" ref="bookshelf" class="w-full overflow-y-auto" :style="{ fontSize: sizeMultiplier + 'rem' }">
|
||||||
<template v-for="shelf in totalShelves">
|
<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' }">
|
<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 v-if="!isAlternativeBookshelfView" class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-20 h-6e" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -65,7 +69,13 @@ export default {
|
|||||||
tempIsScanning: false,
|
tempIsScanning: false,
|
||||||
cardWidth: 0,
|
cardWidth: 0,
|
||||||
cardHeight: 0,
|
cardHeight: 0,
|
||||||
resizeObserver: null
|
coverHeight: 0,
|
||||||
|
resizeObserver: null,
|
||||||
|
lastScrollTop: 0,
|
||||||
|
lastTimestamp: 0,
|
||||||
|
postScrollTimeout: null,
|
||||||
|
currFirstEntityIndex: -1,
|
||||||
|
currLastEntityIndex: -1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -91,6 +101,7 @@ export default {
|
|||||||
if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries
|
if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries
|
||||||
if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollections
|
if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollections
|
||||||
if (this.page === 'playlists') return this.$strings.MessageNoUserPlaylists
|
if (this.page === 'playlists') return this.$strings.MessageNoUserPlaylists
|
||||||
|
if (this.page === 'authors') return this.$strings.MessageNoAuthors
|
||||||
if (this.hasFilter) {
|
if (this.hasFilter) {
|
||||||
if (this.filterName === 'Issues') return this.$strings.MessageNoIssues
|
if (this.filterName === 'Issues') return this.$strings.MessageNoIssues
|
||||||
else if (this.filterName === 'Feed-open') return this.$strings.MessageBookshelfNoRSSFeeds
|
else if (this.filterName === 'Feed-open') return this.$strings.MessageBookshelfNoRSSFeeds
|
||||||
@@ -111,6 +122,12 @@ export default {
|
|||||||
seriesFilterBy() {
|
seriesFilterBy() {
|
||||||
return this.$store.getters['user/getUserSetting']('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() {
|
orderBy() {
|
||||||
return this.$store.getters['user/getUserSetting']('orderBy')
|
return this.$store.getters['user/getUserSetting']('orderBy')
|
||||||
},
|
},
|
||||||
@@ -164,9 +181,6 @@ export default {
|
|||||||
bookWidth() {
|
bookWidth() {
|
||||||
return this.cardWidth
|
return this.cardWidth
|
||||||
},
|
},
|
||||||
bookHeight() {
|
|
||||||
return this.cardHeight
|
|
||||||
},
|
|
||||||
shelfPadding() {
|
shelfPadding() {
|
||||||
if (this.bookshelfWidth < 640) return 32 * this.sizeMultiplier
|
if (this.bookshelfWidth < 640) return 32 * this.sizeMultiplier
|
||||||
return 64 * this.sizeMultiplier
|
return 64 * this.sizeMultiplier
|
||||||
@@ -177,9 +191,6 @@ export default {
|
|||||||
entityWidth() {
|
entityWidth() {
|
||||||
return this.cardWidth
|
return this.cardWidth
|
||||||
},
|
},
|
||||||
entityHeight() {
|
|
||||||
return this.cardHeight
|
|
||||||
},
|
|
||||||
shelfPaddingHeight() {
|
shelfPaddingHeight() {
|
||||||
return 16
|
return 16
|
||||||
},
|
},
|
||||||
@@ -217,6 +228,8 @@ export default {
|
|||||||
this.$store.commit('globals/setEditCollection', entity)
|
this.$store.commit('globals/setEditCollection', entity)
|
||||||
} else if (this.entityName === 'playlists') {
|
} else if (this.entityName === 'playlists') {
|
||||||
this.$store.commit('globals/setEditPlaylist', entity)
|
this.$store.commit('globals/setEditPlaylist', entity)
|
||||||
|
} else if (this.entityName === 'authors') {
|
||||||
|
this.$store.commit('globals/showEditAuthorModal', entity)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clearSelectedEntities() {
|
clearSelectedEntities() {
|
||||||
@@ -345,50 +358,53 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
loadPage(page) {
|
loadPage(page) {
|
||||||
this.pagesLoaded[page] = true
|
if (!this.pagesLoaded[page]) this.pagesLoaded[page] = this.fetchEntites(page)
|
||||||
this.fetchEntites(page)
|
return this.pagesLoaded[page]
|
||||||
},
|
},
|
||||||
showHideBookPlaceholder(index, show) {
|
showHideBookPlaceholder(index, show) {
|
||||||
var el = document.getElementById(`book-${index}-placeholder`)
|
var el = document.getElementById(`book-${index}-placeholder`)
|
||||||
if (el) el.style.display = show ? 'flex' : 'none'
|
if (el) el.style.display = show ? 'flex' : 'none'
|
||||||
},
|
},
|
||||||
mountEntites(fromIndex, toIndex) {
|
mountEntities(fromIndex, toIndex) {
|
||||||
for (let i = fromIndex; i < toIndex; i++) {
|
for (let i = fromIndex; i < toIndex; i++) {
|
||||||
if (!this.entityIndexesMounted.includes(i)) {
|
if (!this.entityIndexesMounted.includes(i)) {
|
||||||
this.cardsHelpers.mountEntityCard(i)
|
this.cardsHelpers.mountEntityCard(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleScroll(scrollTop) {
|
getVisibleIndices(scrollTop) {
|
||||||
this.currScrollTop = scrollTop
|
const firstShelfIndex = Math.floor(scrollTop / this.shelfHeight)
|
||||||
var firstShelfIndex = Math.floor(scrollTop / this.shelfHeight)
|
const lastShelfIndex = Math.min(Math.ceil((scrollTop + this.bookshelfHeight) / this.shelfHeight), this.totalShelves - 1)
|
||||||
var lastShelfIndex = Math.ceil((scrollTop + this.bookshelfHeight) / this.shelfHeight)
|
const firstEntityIndex = firstShelfIndex * this.entitiesPerShelf
|
||||||
lastShelfIndex = Math.min(this.totalShelves - 1, lastShelfIndex)
|
const lastEntityIndex = Math.min(lastShelfIndex * this.entitiesPerShelf + this.entitiesPerShelf, this.totalEntities)
|
||||||
|
return { firstEntityIndex, lastEntityIndex }
|
||||||
var firstBookIndex = firstShelfIndex * this.entitiesPerShelf
|
},
|
||||||
var lastBookIndex = lastShelfIndex * this.entitiesPerShelf + this.entitiesPerShelf
|
postScroll() {
|
||||||
lastBookIndex = Math.min(this.totalEntities, lastBookIndex)
|
const { firstEntityIndex, lastEntityIndex } = this.getVisibleIndices(this.currScrollTop)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.entityIndexesMounted = this.entityIndexesMounted.filter((_index) => {
|
this.entityIndexesMounted = this.entityIndexesMounted.filter((_index) => {
|
||||||
if (_index < firstBookIndex || _index >= lastBookIndex) {
|
if (_index < firstEntityIndex || _index >= lastEntityIndex) {
|
||||||
var el = document.getElementById(`book-card-${_index}`)
|
var el = this.entityComponentRefs[_index]
|
||||||
if (el) el.remove()
|
if (el && el.$el) el.$el.remove()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
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() {
|
async resetEntities() {
|
||||||
if (this.isFetchingEntities) {
|
if (this.isFetchingEntities) {
|
||||||
@@ -396,8 +412,6 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.destroyEntityComponents()
|
this.destroyEntityComponents()
|
||||||
this.entityIndexesMounted = []
|
|
||||||
this.entityComponentRefs = {}
|
|
||||||
this.pagesLoaded = {}
|
this.pagesLoaded = {}
|
||||||
this.entities = []
|
this.entities = []
|
||||||
this.totalShelves = 0
|
this.totalShelves = 0
|
||||||
@@ -407,40 +421,21 @@ export default {
|
|||||||
this.initialized = false
|
this.initialized = false
|
||||||
|
|
||||||
this.initSizeData()
|
this.initSizeData()
|
||||||
this.pagesLoaded[0] = true
|
await this.loadPage(0)
|
||||||
await this.fetchEntites(0)
|
|
||||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
||||||
this.mountEntites(0, lastBookIndex)
|
this.mountEntities(0, lastBookIndex)
|
||||||
},
|
},
|
||||||
remountEntities() {
|
async rebuild() {
|
||||||
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() {
|
|
||||||
this.initSizeData()
|
this.initSizeData()
|
||||||
|
|
||||||
var lastBookIndex = Math.min(this.totalEntities, this.booksPerFetch)
|
var lastBookIndex = Math.min(this.totalEntities, this.booksPerFetch)
|
||||||
this.entityIndexesMounted = []
|
this.destroyEntityComponents()
|
||||||
for (let i = 0; i < lastBookIndex; i++) {
|
await this.loadPage(0)
|
||||||
this.entityIndexesMounted.push(i)
|
|
||||||
if (!this.entities[i]) {
|
|
||||||
const page = Math.floor(i / this.booksPerFetch)
|
|
||||||
this.loadPage(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var bookshelfEl = document.getElementById('bookshelf')
|
var bookshelfEl = document.getElementById('bookshelf')
|
||||||
if (bookshelfEl) {
|
if (bookshelfEl) {
|
||||||
bookshelfEl.scrollTop = 0
|
bookshelfEl.scrollTop = 0
|
||||||
}
|
}
|
||||||
|
this.mountEntities(0, lastBookIndex)
|
||||||
this.$nextTick(this.remountEntities)
|
|
||||||
},
|
},
|
||||||
buildSearchParams() {
|
buildSearchParams() {
|
||||||
if (this.page === 'search' || this.page === 'collections') {
|
if (this.page === 'search' || this.page === 'collections') {
|
||||||
@@ -457,6 +452,9 @@ export default {
|
|||||||
if (this.collapseBookSeries) {
|
if (this.collapseBookSeries) {
|
||||||
searchParams.set('collapseseries', 1)
|
searchParams.set('collapseseries', 1)
|
||||||
}
|
}
|
||||||
|
} else if (this.page === 'authors') {
|
||||||
|
searchParams.set('sort', this.authorSortBy)
|
||||||
|
searchParams.set('desc', this.authorSortDesc ? 1 : 0)
|
||||||
} else {
|
} else {
|
||||||
if (this.filterBy && this.filterBy !== 'all') {
|
if (this.filterBy && this.filterBy !== 'all') {
|
||||||
searchParams.set('filter', this.filterBy)
|
searchParams.set('filter', this.filterBy)
|
||||||
@@ -501,12 +499,29 @@ export default {
|
|||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
this.resetEntities()
|
this.resetEntities()
|
||||||
} else if (settings.bookshelfCoverSize !== this.currentBookWidth) {
|
} 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) {
|
scroll(e) {
|
||||||
if (!e || !e.target) return
|
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)
|
this.handleScroll(scrollTop)
|
||||||
},
|
},
|
||||||
libraryItemAdded(libraryItem) {
|
libraryItemAdded(libraryItem) {
|
||||||
@@ -601,6 +616,34 @@ export default {
|
|||||||
this.executeRebuild()
|
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) {
|
shareOpen(mediaItemShare) {
|
||||||
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
||||||
var indexOf = this.entities.findIndex((ent) => ent?.media?.id === mediaItemShare.mediaItemId)
|
var indexOf = this.entities.findIndex((ent) => ent?.media?.id === mediaItemShare.mediaItemId)
|
||||||
@@ -627,13 +670,14 @@ export default {
|
|||||||
},
|
},
|
||||||
updatePagesLoaded() {
|
updatePagesLoaded() {
|
||||||
let numPages = Math.ceil(this.totalEntities / this.booksPerFetch)
|
let numPages = Math.ceil(this.totalEntities / this.booksPerFetch)
|
||||||
|
this.pagesLoaded = {}
|
||||||
for (let page = 0; page < numPages; page++) {
|
for (let page = 0; page < numPages; page++) {
|
||||||
let numEntities = Math.min(this.totalEntities - page * this.booksPerFetch, this.booksPerFetch)
|
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++) {
|
for (let i = 0; i < numEntities; i++) {
|
||||||
const index = page * this.booksPerFetch + i
|
const index = page * this.booksPerFetch + i
|
||||||
if (!this.entities[index]) {
|
if (!this.entities[index]) {
|
||||||
this.pagesLoaded[page] = false
|
if (this.pagesLoaded[page]) delete this.pagesLoaded[page]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -648,7 +692,6 @@ export default {
|
|||||||
var entitiesPerShelfBefore = this.entitiesPerShelf
|
var entitiesPerShelfBefore = this.entitiesPerShelf
|
||||||
|
|
||||||
var { clientHeight, clientWidth } = bookshelf
|
var { clientHeight, clientWidth } = bookshelf
|
||||||
// console.log('Init bookshelf width', clientWidth, 'window width', window.innerWidth)
|
|
||||||
this.mountWindowWidth = window.innerWidth
|
this.mountWindowWidth = window.innerWidth
|
||||||
this.bookshelfHeight = clientHeight
|
this.bookshelfHeight = clientHeight
|
||||||
this.bookshelfWidth = clientWidth
|
this.bookshelfWidth = clientWidth
|
||||||
@@ -673,10 +716,9 @@ export default {
|
|||||||
this.initSizeData(bookshelf)
|
this.initSizeData(bookshelf)
|
||||||
this.checkUpdateSearchParams()
|
this.checkUpdateSearchParams()
|
||||||
|
|
||||||
this.pagesLoaded[0] = true
|
await this.loadPage(0)
|
||||||
await this.fetchEntites(0)
|
|
||||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
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
|
// Set last scroll position for this bookshelf page
|
||||||
if (this.$store.state.lastBookshelfScrollData[this.page] && window.bookshelf) {
|
if (this.$store.state.lastBookshelfScrollData[this.page] && window.bookshelf) {
|
||||||
@@ -707,7 +749,7 @@ export default {
|
|||||||
var bookshelf = document.getElementById('bookshelf')
|
var bookshelf = document.getElementById('bookshelf')
|
||||||
if (bookshelf) {
|
if (bookshelf) {
|
||||||
this.init(bookshelf)
|
this.init(bookshelf)
|
||||||
bookshelf.addEventListener('scroll', this.scroll)
|
bookshelf.addEventListener('scroll', this.scroll, { passive: true })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -727,6 +769,9 @@ export default {
|
|||||||
this.$root.socket.on('playlist_added', this.playlistAdded)
|
this.$root.socket.on('playlist_added', this.playlistAdded)
|
||||||
this.$root.socket.on('playlist_updated', this.playlistUpdated)
|
this.$root.socket.on('playlist_updated', this.playlistUpdated)
|
||||||
this.$root.socket.on('playlist_removed', this.playlistRemoved)
|
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_open', this.shareOpen)
|
||||||
this.$root.socket.on('share_closed', this.shareClosed)
|
this.$root.socket.on('share_closed', this.shareClosed)
|
||||||
} else {
|
} else {
|
||||||
@@ -756,6 +801,9 @@ export default {
|
|||||||
this.$root.socket.off('playlist_added', this.playlistAdded)
|
this.$root.socket.off('playlist_added', this.playlistAdded)
|
||||||
this.$root.socket.off('playlist_updated', this.playlistUpdated)
|
this.$root.socket.off('playlist_updated', this.playlistUpdated)
|
||||||
this.$root.socket.off('playlist_removed', this.playlistRemoved)
|
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_open', this.shareOpen)
|
||||||
this.$root.socket.off('share_closed', this.shareClosed)
|
this.$root.socket.off('share_closed', this.shareClosed)
|
||||||
} else {
|
} else {
|
||||||
@@ -764,10 +812,14 @@ export default {
|
|||||||
},
|
},
|
||||||
destroyEntityComponents() {
|
destroyEntityComponents() {
|
||||||
for (const key in this.entityComponentRefs) {
|
for (const key in this.entityComponentRefs) {
|
||||||
if (this.entityComponentRefs[key] && this.entityComponentRefs[key].destroy) {
|
const ref = this.entityComponentRefs[key]
|
||||||
this.entityComponentRefs[key].destroy()
|
if (ref && ref.destroy) {
|
||||||
|
if (ref.$el) ref.$el.remove()
|
||||||
|
ref.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.entityComponentRefs = {}
|
||||||
|
this.entityIndexesMounted = []
|
||||||
},
|
},
|
||||||
scan() {
|
scan() {
|
||||||
this.tempIsScanning = true
|
this.tempIsScanning = true
|
||||||
@@ -780,6 +832,14 @@ export default {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.tempIsScanning = false
|
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() {
|
async mounted() {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
<template>
|
<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 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">
|
<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" />
|
<covers-book-cover expand-on-click :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
||||||
</div>
|
</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="min-w-0 w-full">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
|
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
|
||||||
@@ -12,10 +11,9 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<widgets-explicit-indicator v-if="isExplicit" />
|
<widgets-explicit-indicator v-if="isExplicit" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5">
|
<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>
|
<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-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">
|
<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>
|
<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>
|
</div>
|
||||||
@@ -55,16 +53,13 @@
|
|||||||
@showBookmarks="showBookmarks"
|
@showBookmarks="showBookmarks"
|
||||||
@showSleepTimer="showSleepTimerModal = true"
|
@showSleepTimer="showSleepTimerModal = true"
|
||||||
@showPlayerQueueItems="showPlayerQueueItemsModal = true"
|
@showPlayerQueueItems="showPlayerQueueItemsModal = true"
|
||||||
@showPlayerSettings="showPlayerSettingsModal = true"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :library-item-id="libraryItemId" @select="selectBookmark" />
|
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :playback-rate="currentPlaybackRate" :library-item-id="libraryItemId" @select="selectBookmark" />
|
||||||
|
|
||||||
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-type="sleepTimerType" :remaining="sleepTimerRemaining" :has-chapters="!!chapters.length" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
|
<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" />
|
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" />
|
||||||
|
|
||||||
<modals-player-settings-modal v-model="showPlayerSettingsModal" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -83,7 +78,6 @@ export default {
|
|||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
showSleepTimerModal: false,
|
showSleepTimerModal: false,
|
||||||
showPlayerQueueItemsModal: false,
|
showPlayerQueueItemsModal: false,
|
||||||
showPlayerSettingsModal: false,
|
|
||||||
sleepTimerSet: false,
|
sleepTimerSet: false,
|
||||||
sleepTimerRemaining: 0,
|
sleepTimerRemaining: 0,
|
||||||
sleepTimerType: null,
|
sleepTimerType: null,
|
||||||
@@ -140,9 +134,6 @@ export default {
|
|||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.streamLibraryItem?.mediaType === 'podcast'
|
return this.streamLibraryItem?.mediaType === 'podcast'
|
||||||
},
|
},
|
||||||
isMusic() {
|
|
||||||
return this.streamLibraryItem?.mediaType === 'music'
|
|
||||||
},
|
|
||||||
isExplicit() {
|
isExplicit() {
|
||||||
return !!this.mediaMetadata.explicit
|
return !!this.mediaMetadata.explicit
|
||||||
},
|
},
|
||||||
@@ -172,11 +163,7 @@ export default {
|
|||||||
},
|
},
|
||||||
podcastAuthor() {
|
podcastAuthor() {
|
||||||
if (!this.isPodcast) return null
|
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() {
|
hasNextItemInQueue() {
|
||||||
return this.currentPlayerQueueIndex < this.playerQueueItems.length - 1
|
return this.currentPlayerQueueIndex < this.playerQueueItems.length - 1
|
||||||
@@ -260,7 +247,7 @@ export default {
|
|||||||
sleepTimerEnd() {
|
sleepTimerEnd() {
|
||||||
this.clearSleepTimer()
|
this.clearSleepTimer()
|
||||||
this.playerHandler.pause()
|
this.playerHandler.pause()
|
||||||
this.$toast.info('Sleep Timer Done.. zZzzZz')
|
this.$toast.info(this.$strings.ToastSleepTimerDone)
|
||||||
},
|
},
|
||||||
cancelSleepTimer() {
|
cancelSleepTimer() {
|
||||||
this.showSleepTimerModal = false
|
this.showSleepTimerModal = false
|
||||||
@@ -387,19 +374,27 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API
|
||||||
if ('mediaSession' in navigator) {
|
if ('mediaSession' in navigator) {
|
||||||
var coverImageSrc = this.$store.getters['globals/getLibraryItemCoverSrc'](this.streamLibraryItem, '/Logo.png', true)
|
const chapterInfo = []
|
||||||
const artwork = [
|
if (this.chapters.length) {
|
||||||
{
|
this.chapters.forEach((chapter) => {
|
||||||
src: coverImageSrc
|
chapterInfo.push({
|
||||||
}
|
title: chapter.title,
|
||||||
]
|
startTime: chapter.start
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
navigator.mediaSession.metadata = new MediaMetadata({
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
title: this.title,
|
title: this.title,
|
||||||
artist: this.playerHandler.displayAuthor || this.mediaMetadata.authorName || 'Unknown',
|
artist: this.playerHandler.displayAuthor || this.mediaMetadata.authorName || 'Unknown',
|
||||||
album: this.mediaMetadata.seriesName || '',
|
album: this.mediaMetadata.seriesName || '',
|
||||||
artwork
|
artwork: [
|
||||||
|
{
|
||||||
|
src: this.$store.getters['globals/getLibraryItemCoverSrc'](this.streamLibraryItem, '/Logo.png', true)
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
console.log('Set media session metadata', navigator.mediaSession.metadata)
|
console.log('Set media session metadata', navigator.mediaSession.metadata)
|
||||||
|
|
||||||
@@ -534,7 +529,7 @@ export default {
|
|||||||
},
|
},
|
||||||
showFailedProgressSyncs() {
|
showFailedProgressSyncs() {
|
||||||
if (!isNaN(this.syncFailedToast)) this.$toast.dismiss(this.syncFailedToast)
|
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) {
|
sessionClosedEvent(sessionId) {
|
||||||
if (this.playerHandler.currentSessionId === sessionId) {
|
if (this.playerHandler.currentSessionId === sessionId) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<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 -->
|
<!-- 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 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'">
|
<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">
|
<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" />
|
<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" />
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</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">
|
<svg class="w-6 h-6" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@@ -95,14 +95,6 @@
|
|||||||
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</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-symbols 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'">
|
<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-symbols text-2xl"></span>
|
<span class="material-symbols text-2xl"></span>
|
||||||
|
|
||||||
@@ -172,9 +164,6 @@ export default {
|
|||||||
isPodcastLibrary() {
|
isPodcastLibrary() {
|
||||||
return this.currentLibraryMediaType === 'podcast'
|
return this.currentLibraryMediaType === 'podcast'
|
||||||
},
|
},
|
||||||
isMusicLibrary() {
|
|
||||||
return this.currentLibraryMediaType === 'music'
|
|
||||||
},
|
|
||||||
isPodcastDownloadQueuePage() {
|
isPodcastDownloadQueuePage() {
|
||||||
return this.$route.name === 'library-library-podcast-download-queue'
|
return this.$route.name === 'library-library-podcast-download-queue'
|
||||||
},
|
},
|
||||||
@@ -184,9 +173,6 @@ export default {
|
|||||||
isPodcastLatestPage() {
|
isPodcastLatestPage() {
|
||||||
return this.$route.name === 'library-library-podcast-latest'
|
return this.$route.name === 'library-library-podcast-latest'
|
||||||
},
|
},
|
||||||
isMusicAlbumsPage() {
|
|
||||||
return this.paramId === 'albums'
|
|
||||||
},
|
|
||||||
homePage() {
|
homePage() {
|
||||||
return this.$route.name === 'library-library'
|
return this.$route.name === 'library-library'
|
||||||
},
|
},
|
||||||
@@ -194,7 +180,7 @@ export default {
|
|||||||
return this.$route.name === 'library-library-series-id' || this.paramId === 'series'
|
return this.$route.name === 'library-library-series-id' || this.paramId === 'series'
|
||||||
},
|
},
|
||||||
isAuthorsPage() {
|
isAuthorsPage() {
|
||||||
return this.$route.name === 'library-library-authors'
|
return this.libraryBookshelfPage && this.paramId === 'authors'
|
||||||
},
|
},
|
||||||
isNarratorsPage() {
|
isNarratorsPage() {
|
||||||
return this.$route.name === 'library-library-narrators'
|
return this.$route.name === 'library-library-narrators'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :style="{ minWidth: cardWidth + 'px', maxWidth: cardWidth + 'px' }">
|
<div class="pb-3e" :style="{ minWidth: cardWidth + 'px', maxWidth: cardWidth + 'px' }">
|
||||||
<nuxt-link :to="`/author/${author.id}`">
|
<nuxt-link :to="`/author/${author?.id}`">
|
||||||
<div cy-id="card" @mouseover="mouseover" @mouseleave="mouseleave">
|
<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">
|
<div cy-id="imageArea" :style="{ height: cardHeight + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
||||||
<!-- Image or placeholder -->
|
<!-- Image or placeholder -->
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
author: {
|
authorMount: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
@@ -57,7 +57,8 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
searching: false,
|
searching: false,
|
||||||
isHovering: false
|
isHovering: false,
|
||||||
|
author: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -67,35 +68,41 @@ export default {
|
|||||||
cardHeight() {
|
cardHeight() {
|
||||||
return this.height * this.sizeMultiplier
|
return this.height * this.sizeMultiplier
|
||||||
},
|
},
|
||||||
|
coverHeight() {
|
||||||
|
return this.cardHeight
|
||||||
|
},
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.store.getters['user/getToken']
|
||||||
},
|
},
|
||||||
_author() {
|
_author() {
|
||||||
return this.author || {}
|
return this.author || {}
|
||||||
},
|
},
|
||||||
authorId() {
|
authorId() {
|
||||||
return this._author.id
|
return this._author?.id || ''
|
||||||
},
|
},
|
||||||
name() {
|
name() {
|
||||||
return this._author.name || ''
|
return this._author?.name || ''
|
||||||
},
|
},
|
||||||
asin() {
|
asin() {
|
||||||
return this._author.asin || ''
|
return this._author?.asin || ''
|
||||||
},
|
},
|
||||||
numBooks() {
|
numBooks() {
|
||||||
return this._author.numBooks || 0
|
return this._author?.numBooks || 0
|
||||||
|
},
|
||||||
|
store() {
|
||||||
|
return this.$store || this.$nuxt.$store
|
||||||
},
|
},
|
||||||
userCanUpdate() {
|
userCanUpdate() {
|
||||||
return this.$store.getters['user/getUserCanUpdate']
|
return this.store.getters['user/getUserCanUpdate']
|
||||||
},
|
},
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
libraryProvider() {
|
libraryProvider() {
|
||||||
return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
|
return this.store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
return this.$store.getters['user/getSizeMultiplier']
|
return this.store.getters['user/getSizeMultiplier']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -121,24 +128,54 @@ export default {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
if (!response) {
|
if (!response) {
|
||||||
this.$toast.error(`Author ${this.name} not found`)
|
this.$toast.error(this.$getString('ToastAuthorNotFound', [this.name]))
|
||||||
} else if (response.updated) {
|
} else if (response.updated) {
|
||||||
if (response.author.imagePath) this.$toast.success(`Author ${response.author.name} was updated`)
|
if (response.author.imagePath) {
|
||||||
else this.$toast.success(`Author ${response.author.name} was updated (no image found)`)
|
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||||
|
} else {
|
||||||
|
this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info(`No updates were made for Author ${response.author.name}`)
|
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
}
|
}
|
||||||
this.searching = false
|
this.searching = false
|
||||||
},
|
},
|
||||||
setSearching(isSearching) {
|
setSearching(isSearching) {
|
||||||
this.searching = 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() {
|
mounted() {
|
||||||
this.$eventBus.$on(`searching-author-${this.authorId}`, this.setSearching)
|
if (this.authorMount) this.setEntity(this.authorMount)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$eventBus.$off(`searching-author-${this.authorId}`, this.setSearching)
|
this.removeListeners()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<p class="truncate text-sm">{{ title }}</p>
|
<p class="truncate text-sm">{{ title }}</p>
|
||||||
|
|
||||||
<p class="truncate text-xs text-gray-300">{{ description }}</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-if="isFailed && failedMessage" class="text-xs truncate text-red-500">{{ failedMessage }}</p>
|
||||||
<p v-else-if="!isFinished && cancelingScan" class="text-xs truncate">Canceling...</p>
|
<p v-else-if="!isFinished && cancelingScan" class="text-xs truncate">Canceling...</p>
|
||||||
@@ -26,7 +27,16 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
cancelingScan: false
|
cancelingScan: false,
|
||||||
|
specialMessage: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
task: {
|
||||||
|
immediate: true,
|
||||||
|
handler() {
|
||||||
|
this.initTask()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -34,14 +44,17 @@ export default {
|
|||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
title() {
|
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'
|
return this.task.title || 'No Title'
|
||||||
},
|
},
|
||||||
description() {
|
description() {
|
||||||
|
if (this.task.descriptionKey && this.$strings[this.task.descriptionKey]) {
|
||||||
|
return this.$getString(this.task.descriptionKey, this.task.descriptionSubs)
|
||||||
|
}
|
||||||
return this.task.description || ''
|
return this.task.description || ''
|
||||||
},
|
},
|
||||||
details() {
|
|
||||||
return this.task.details || 'Unknown'
|
|
||||||
},
|
|
||||||
isFinished() {
|
isFinished() {
|
||||||
return !!this.task.isFinished
|
return !!this.task.isFinished
|
||||||
},
|
},
|
||||||
@@ -52,6 +65,9 @@ export default {
|
|||||||
return this.isFinished && !this.isFailed
|
return this.isFinished && !this.isFailed
|
||||||
},
|
},
|
||||||
failedMessage() {
|
failedMessage() {
|
||||||
|
if (this.task.errorKey && this.$strings[this.task.errorKey]) {
|
||||||
|
return this.$getString(this.task.errorKey, this.task.errorSubs)
|
||||||
|
}
|
||||||
return this.task.error || ''
|
return this.task.error || ''
|
||||||
},
|
},
|
||||||
action() {
|
action() {
|
||||||
@@ -87,6 +103,21 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
cancelScan() {
|
||||||
const libraryId = this.task?.data?.libraryId
|
const libraryId = this.task?.data?.libraryId
|
||||||
if (!libraryId) {
|
if (!libraryId) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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 ' }">
|
<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 -->
|
<!-- 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">
|
<div cy-id="coverBg" v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
||||||
@@ -14,21 +14,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
<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>
|
<p :style="{ fontSize: 0.8 + 'em' }" class="text-gray-300 text-center">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cover Image -->
|
<!-- 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 -->
|
<!-- 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 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>
|
<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>
|
</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' }">
|
<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>
|
<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>
|
</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">
|
<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">
|
||||||
@@ -93,11 +93,11 @@
|
|||||||
|
|
||||||
<!-- rss feed icon -->
|
<!-- 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' }">
|
<div cy-id="rssFeed" v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" :style="{ padding: 0.375 + 'em' }">
|
||||||
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">rss_feed</span>
|
<span class="material-symbols" aria-hidden="true" :style="{ fontSize: 1.5 + 'em' }">rss_feed</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- media item shared icon -->
|
<!-- 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' }">
|
<div cy-id="mediaItemShare" v-if="mediaItemShare && !isSelectionMode && !isHovering" class="absolute text-success left-0 z-10" :style="{ padding: 0.375 + 'em', top: rssFeed ? '2em' : '0px' }">
|
||||||
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">public</span>
|
<span class="material-symbols" aria-hidden="true" :style="{ fontSize: 1.5 + 'em' }">public</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Series sequence -->
|
<!-- Series sequence -->
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
|
|
||||||
<!-- Podcast Num Episodes -->
|
<!-- 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' }">
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Podcast Num Episodes -->
|
<!-- Podcast Num Episodes -->
|
||||||
@@ -226,9 +226,6 @@ export default {
|
|||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.mediaType === 'podcast' || this.store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast'
|
return this.mediaType === 'podcast' || this.store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast'
|
||||||
},
|
},
|
||||||
isMusic() {
|
|
||||||
return this.mediaType === 'music'
|
|
||||||
},
|
|
||||||
isExplicit() {
|
isExplicit() {
|
||||||
return this.mediaMetadata.explicit || false
|
return this.mediaMetadata.explicit || false
|
||||||
},
|
},
|
||||||
@@ -328,7 +325,7 @@ export default {
|
|||||||
},
|
},
|
||||||
displaySubtitle() {
|
displaySubtitle() {
|
||||||
if (!this.libraryItem) return '\u00A0'
|
if (!this.libraryItem) return '\u00A0'
|
||||||
if (this.collapsedSeries) return this.collapsedSeries.numBooks === 1 ? '1 book' : `${this.collapsedSeries.numBooks} books`
|
if (this.collapsedSeries) return `${this.collapsedSeries.numBooks} ${this.$strings.LabelBooks}`
|
||||||
if (this.mediaMetadata.subtitle) return this.mediaMetadata.subtitle
|
if (this.mediaMetadata.subtitle) return this.mediaMetadata.subtitle
|
||||||
if (this.mediaMetadata.seriesName) return this.mediaMetadata.seriesName
|
if (this.mediaMetadata.seriesName) return this.mediaMetadata.seriesName
|
||||||
return ''
|
return ''
|
||||||
@@ -336,7 +333,6 @@ export default {
|
|||||||
displayLineTwo() {
|
displayLineTwo() {
|
||||||
if (this.recentEpisode) return this.title
|
if (this.recentEpisode) return this.title
|
||||||
if (this.isPodcast) return this.author
|
if (this.isPodcast) return this.author
|
||||||
if (this.isMusic) return this.artist
|
|
||||||
if (this.collapsedSeries) return ''
|
if (this.collapsedSeries) return ''
|
||||||
if (this.isAuthorBookshelfView) {
|
if (this.isAuthorBookshelfView) {
|
||||||
return this.mediaMetadata.publishedYear || ''
|
return this.mediaMetadata.publishedYear || ''
|
||||||
@@ -364,7 +360,6 @@ export default {
|
|||||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
|
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
|
||||||
},
|
},
|
||||||
userProgress() {
|
userProgress() {
|
||||||
if (this.isMusic) return null
|
|
||||||
if (this.episodeProgress) return this.episodeProgress
|
if (this.episodeProgress) return this.episodeProgress
|
||||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
@@ -420,7 +415,7 @@ export default {
|
|||||||
return !this.isSelectionMode && !this.showPlayButton && this.ebookFormat
|
return !this.isSelectionMode && !this.showPlayButton && this.ebookFormat
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
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() {
|
showSmallEBookIcon() {
|
||||||
return !this.isSelectionMode && this.ebookFormat
|
return !this.isSelectionMode && this.ebookFormat
|
||||||
@@ -464,8 +459,6 @@ export default {
|
|||||||
return this.store.getters['user/getIsAdminOrUp']
|
return this.store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
moreMenuItems() {
|
moreMenuItems() {
|
||||||
if (this.isMusic) return []
|
|
||||||
|
|
||||||
if (this.recentEpisode) {
|
if (this.recentEpisode) {
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
@@ -823,7 +816,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove series from home', error)
|
console.error('Failed to remove series from home', error)
|
||||||
this.$toast.error(this.$strings.ToastFailedToUpdateUser)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
@@ -841,7 +834,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to hide item from home', error)
|
console.error('Failed to hide item from home', error)
|
||||||
this.$toast.error(this.$strings.ToastFailedToUpdateUser)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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="relative" :style="{ height: coverHeight + 'px' }">
|
||||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
<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">
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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="relative" :style="{ height: coverHeight + 'px' }">
|
||||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
<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">
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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 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="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">
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
||||||
@@ -7,12 +7,12 @@
|
|||||||
</div>
|
</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">
|
<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>
|
||||||
|
|
||||||
<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" :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>
|
<p :style="{ fontSize: 1.2 + 'em' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update notification', error)
|
console.error('Failed to update notification', error)
|
||||||
this.$toast.error(this.$strings.ToastNotificationUpdateFailed)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.enabling = false
|
this.enabling = false
|
||||||
|
|||||||
@@ -27,38 +27,6 @@
|
|||||||
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=publishers.${$encode(publisher)}`" class="hover:underline">{{ publisher }}</nuxt-link>
|
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=publishers.${$encode(publisher)}`" class="hover:underline">{{ publisher }}</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</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 v-if="podcastType" class="flex py-0.5">
|
||||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
<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>
|
<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>
|
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=languages.${$encode(language)}`" class="hover:underline">{{ language }}</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -134,10 +102,6 @@ export default {
|
|||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.libraryItem.mediaType === 'podcast'
|
return this.libraryItem.mediaType === 'podcast'
|
||||||
},
|
},
|
||||||
audioFile() {
|
|
||||||
// Music track
|
|
||||||
return this.media.audioFile
|
|
||||||
},
|
|
||||||
media() {
|
media() {
|
||||||
return this.libraryItem.media || {}
|
return this.libraryItem.media || {}
|
||||||
},
|
},
|
||||||
@@ -168,25 +132,6 @@ export default {
|
|||||||
publisher() {
|
publisher() {
|
||||||
return this.mediaMetadata.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() {
|
narrators() {
|
||||||
return this.mediaMetadata.narrators || []
|
return this.mediaMetadata.narrators || []
|
||||||
},
|
},
|
||||||
@@ -220,4 +165,4 @@ export default {
|
|||||||
methods: {},
|
methods: {},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="w-full relative sm:w-80">
|
<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" />
|
<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>
|
</form>
|
||||||
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
|
<button :aria-hidden="!search" class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
|
||||||
<span v-if="!search" class="material-symbols" style="font-size: 1.2rem"></span>
|
<span v-if="!search" class="material-symbols" style="font-size: 1.2rem"></span>
|
||||||
<span v-else class="material-symbols" style="font-size: 1.2rem">close</span>
|
<span v-else class="material-symbols" style="font-size: 1.2rem">close</span>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</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">
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<li v-if="isTyping" class="py-2 px-2">
|
<li v-if="isTyping" class="py-2 px-2">
|
||||||
<p>{{ $strings.MessageThinking }}</p>
|
<p>{{ $strings.MessageThinking }}</p>
|
||||||
@@ -157,7 +157,7 @@ export default {
|
|||||||
clearTimeout(this.focusTimeout)
|
clearTimeout(this.focusTimeout)
|
||||||
this.focusTimeout = setTimeout(() => {
|
this.focusTimeout = setTimeout(() => {
|
||||||
this.showMenu = false
|
this.showMenu = false
|
||||||
}, 200)
|
}, 100)
|
||||||
},
|
},
|
||||||
async runSearch(value) {
|
async runSearch(value) {
|
||||||
this.lastSearch = value
|
this.lastSearch = value
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
<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">
|
<div class="relative h-7">
|
||||||
<span class="flex items-center justify-between">
|
<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="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
<span class="flex items-center justify-between">
|
||||||
</span>
|
<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">
|
<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">
|
<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" />
|
<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>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<div v-else class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-200" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
|
<button v-else :aria-label="$strings.ButtonClearFilter" class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-200" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
|
||||||
<span class="material-symbols" style="font-size: 1.1rem">close</span>
|
<span class="material-symbols" style="font-size: 1.1rem">close</span>
|
||||||
</div>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
<div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm libraryFilterMenu">
|
<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">
|
<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">
|
<div class="flex items-center justify-between">
|
||||||
<span class="font-normal ml-3 block truncate text-sm">{{ item.text }}</span>
|
<span class="font-normal ml-3 block truncate text-sm">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
|
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
|
||||||
<span class="material-symbols text-2xl">arrow_right</span>
|
<span class="material-symbols text-2xl" :aria-label="$strings.LabelMore">arrow_right</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- selected checkmark icon -->
|
<!-- selected checkmark icon -->
|
||||||
<div v-if="item.value === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
|
<div v-if="item.value === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
|
||||||
@@ -31,8 +33,8 @@
|
|||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
<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">
|
||||||
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-white/5" role="option" @click="sublist = null">
|
<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">
|
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
|
||||||
<span class="material-symbols text-2xl">arrow_left</span>
|
<span class="material-symbols text-2xl">arrow_left</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,13 +42,13 @@
|
|||||||
<span class="font-normal block truncate">{{ $strings.ButtonBack }}</span>
|
<span class="font-normal block truncate">{{ $strings.ButtonBack }}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</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">
|
<div class="flex items-center justify-center">
|
||||||
<span class="font-normal block truncate py-2">{{ $getString('LabelLibraryFilterSublistEmpty', [selectedSublistText]) }}</span>
|
<span class="font-normal block truncate py-2">{{ $getString('LabelLibraryFilterSublistEmpty', [selectedSublistText]) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<template v-for="item in sublistItems">
|
<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">
|
<div class="flex items-center">
|
||||||
<span class="font-normal truncate py-2 text-xs">{{ item.text }}</span>
|
<span class="font-normal truncate py-2 text-xs">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,9 +100,6 @@ export default {
|
|||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.libraryMediaType === 'podcast'
|
return this.libraryMediaType === 'podcast'
|
||||||
},
|
},
|
||||||
isMusic() {
|
|
||||||
return this.libraryMediaType === 'music'
|
|
||||||
},
|
|
||||||
seriesItems() {
|
seriesItems() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -192,6 +191,12 @@ export default {
|
|||||||
value: 'publishers',
|
value: 'publishers',
|
||||||
sublist: true
|
sublist: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelPublishedDecade,
|
||||||
|
textPlural: this.$strings.LabelPublishedDecades,
|
||||||
|
value: 'publishedDecades',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.LabelLanguage,
|
text: this.$strings.LabelLanguage,
|
||||||
textPlural: this.$strings.LabelLanguages,
|
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() {
|
selectItems() {
|
||||||
if (this.isSeries) return this.seriesItems
|
if (this.isSeries) return this.seriesItems
|
||||||
if (this.isPodcast) return this.podcastItems
|
if (this.isPodcast) return this.podcastItems
|
||||||
if (this.isMusic) return this.musicItems
|
|
||||||
return this.bookItems
|
return this.bookItems
|
||||||
},
|
},
|
||||||
selectedItemSublist() {
|
selectedItemSublist() {
|
||||||
@@ -367,6 +346,9 @@ export default {
|
|||||||
publishers() {
|
publishers() {
|
||||||
return this.filterData.publishers || []
|
return this.filterData.publishers || []
|
||||||
},
|
},
|
||||||
|
publishedDecades() {
|
||||||
|
return this.filterData.publishedDecades || []
|
||||||
|
},
|
||||||
progress() {
|
progress() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -433,21 +415,17 @@ export default {
|
|||||||
id: 'isbn',
|
id: 'isbn',
|
||||||
name: 'ISBN'
|
name: 'ISBN'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'subtitle',
|
|
||||||
name: this.$strings.LabelSubtitle
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'authors',
|
id: 'authors',
|
||||||
name: this.$strings.LabelAuthor
|
name: this.$strings.LabelAuthor
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'publishedYear',
|
id: 'chapters',
|
||||||
name: this.$strings.LabelPublishYear
|
name: this.$strings.LabelChapters
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'series',
|
id: 'cover',
|
||||||
name: this.$strings.LabelSeries
|
name: this.$strings.LabelCover
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'description',
|
id: 'description',
|
||||||
@@ -458,24 +436,32 @@ export default {
|
|||||||
name: this.$strings.LabelGenres
|
name: this.$strings.LabelGenres
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'tags',
|
id: 'language',
|
||||||
name: this.$strings.LabelTags
|
name: this.$strings.LabelLanguage
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'narrators',
|
id: 'narrators',
|
||||||
name: this.$strings.LabelNarrator
|
name: this.$strings.LabelNarrator
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'publishedYear',
|
||||||
|
name: this.$strings.LabelPublishYear
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'publisher',
|
id: 'publisher',
|
||||||
name: this.$strings.LabelPublisher
|
name: this.$strings.LabelPublisher
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'language',
|
id: 'series',
|
||||||
name: this.$strings.LabelLanguage
|
name: this.$strings.LabelSeries
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'cover',
|
id: 'subtitle',
|
||||||
name: this.$strings.LabelCover
|
name: this.$strings.LabelSubtitle
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tags',
|
||||||
|
name: this.$strings.LabelTags
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
<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="flex items-center justify-between">
|
||||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||||
<span class="material-symbols text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
<span class="material-symbols text-lg text-yellow-400" :aria-label="descending ? $strings.LabelSortDescending : $strings.LabelSortAscending">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</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">
|
<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">
|
<div class="flex items-center">
|
||||||
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
|
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
||||||
<span class="material-symbols text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
<span class="material-symbols text-xl" :aria-label="descending ? $strings.LabelSortDescending : $strings.LabelSortAscending">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
@@ -56,9 +56,6 @@ export default {
|
|||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.libraryMediaType === 'podcast'
|
return this.libraryMediaType === 'podcast'
|
||||||
},
|
},
|
||||||
isMusic() {
|
|
||||||
return this.libraryMediaType === 'music'
|
|
||||||
},
|
|
||||||
podcastItems() {
|
podcastItems() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -148,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() {
|
selectItems() {
|
||||||
let items = null
|
let items = null
|
||||||
if (this.isPodcast) {
|
if (this.isPodcast) {
|
||||||
items = this.podcastItems
|
items = this.podcastItems
|
||||||
} else if (this.isMusic) {
|
|
||||||
items = this.musicItems
|
|
||||||
} else if (this.$store.getters['user/getUserSetting']('filterBy').startsWith('series.')) {
|
} else if (this.$store.getters['user/getUserSetting']('filterBy').startsWith('series.')) {
|
||||||
items = this.seriesItems
|
items = this.seriesItems
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="relative ml-4 sm:ml-8" v-click-outside="clickOutside">
|
<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)">
|
<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">{{ playbackRate.toFixed(1) }}<span class="text-base">x</span></span>
|
||||||
</div>
|
</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="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }">
|
||||||
<div class="arrow-down" />
|
<div class="arrow-down" />
|
||||||
</div>
|
</div>
|
||||||
@@ -11,12 +11,12 @@
|
|||||||
<template v-for="rate in rates">
|
<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 :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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full py-1 px-4">
|
<div class="w-full py-1 px-1">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" />
|
<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">{{ playbackRate }}<span class="text-2xl">x</span></p>
|
||||||
@@ -41,7 +41,7 @@ export default {
|
|||||||
currentPlaybackRate: 0,
|
currentPlaybackRate: 0,
|
||||||
MIN_SPEED: 0.5,
|
MIN_SPEED: 0.5,
|
||||||
MAX_SPEED: 10,
|
MAX_SPEED: 10,
|
||||||
menuLeft: -92,
|
menuLeft: -96,
|
||||||
arrowLeft: 0
|
arrowLeft: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -89,9 +89,9 @@ export default {
|
|||||||
if (boundingBox.left + 110 > window.innerWidth - 10) {
|
if (boundingBox.left + 110 > window.innerWidth - 10) {
|
||||||
this.menuLeft = window.innerWidth - 230 - boundingBox.left
|
this.menuLeft = window.innerWidth - 230 - boundingBox.left
|
||||||
|
|
||||||
this.arrowLeft = Math.abs(this.menuLeft) - 92
|
this.arrowLeft = Math.abs(this.menuLeft) - 96
|
||||||
} else {
|
} else {
|
||||||
this.menuLeft = -92
|
this.menuLeft = -96
|
||||||
this.arrowLeft = 0
|
this.arrowLeft = 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -109,4 +109,4 @@ export default {
|
|||||||
this.currentPlaybackRate = this.playbackRate
|
this.currentPlaybackRate = this.playbackRate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
<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="flex items-center justify-between">
|
||||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||||
<span class="material-symbols text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
<span class="material-symbols text-lg text-yellow-400" :aria-label="descending ? $strings.LabelSortDescending : $strings.LabelSortAscending">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</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">
|
<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">
|
<div class="flex items-center">
|
||||||
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
|
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
||||||
<span class="material-symbols text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
<span class="material-symbols text-xl" :aria-label="descending ? $strings.LabelSortDescending : $strings.LabelSortAscending">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
<span class="material-symbols text-2xl sm:text-3xl">{{ volumeIcon }}</span>
|
<span class="material-symbols text-2xl sm:text-3xl">{{ volumeIcon }}</span>
|
||||||
</button>
|
</button>
|
||||||
<transition name="menux">
|
<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 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="h-1 w-full bg-gray-500 my-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack">
|
<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 h-full absolute left-0 top-0 pointer-events-none rounded-full" :style="{ width: volume * trackWidth + 'px' }" />
|
<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="{ left: cursorLeft + 'px', top: '-3px' }" />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
@@ -24,10 +24,10 @@ export default {
|
|||||||
isOpen: false,
|
isOpen: false,
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
isHovering: false,
|
isHovering: false,
|
||||||
posX: 0,
|
posY: 0,
|
||||||
lastValue: 0.5,
|
lastValue: 0.5,
|
||||||
isMute: false,
|
isMute: false,
|
||||||
trackWidth: 112 - 20,
|
trackHeight: 112 - 20,
|
||||||
openTimeout: null
|
openTimeout: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -45,9 +45,9 @@ export default {
|
|||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cursorLeft() {
|
cursorBottom() {
|
||||||
var left = this.trackWidth * this.volume
|
var bottom = this.trackHeight * this.volume
|
||||||
return left - 3
|
return bottom - 3
|
||||||
},
|
},
|
||||||
volumeIcon() {
|
volumeIcon() {
|
||||||
if (this.volume <= 0) return 'volume_mute'
|
if (this.volume <= 0) return 'volume_mute'
|
||||||
@@ -89,17 +89,10 @@ export default {
|
|||||||
}, 600)
|
}, 600)
|
||||||
},
|
},
|
||||||
mousemove(e) {
|
mousemove(e) {
|
||||||
var diff = this.posX - e.x
|
var diff = this.posY - e.y
|
||||||
this.posX = e.x
|
this.posY = e.y
|
||||||
var volShift = 0
|
var volShift = diff / this.trackHeight
|
||||||
if (diff < 0) {
|
var newVol = this.volume + volShift
|
||||||
// Volume up
|
|
||||||
volShift = diff / this.trackWidth
|
|
||||||
} else {
|
|
||||||
// volume down
|
|
||||||
volShift = diff / this.trackWidth
|
|
||||||
}
|
|
||||||
var newVol = this.volume - volShift
|
|
||||||
newVol = Math.min(Math.max(0, newVol), 1)
|
newVol = Math.min(Math.max(0, newVol), 1)
|
||||||
this.volume = newVol
|
this.volume = newVol
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -113,8 +106,8 @@ export default {
|
|||||||
},
|
},
|
||||||
mousedownTrack(e) {
|
mousedownTrack(e) {
|
||||||
this.isDragging = true
|
this.isDragging = true
|
||||||
this.posX = e.x
|
this.posY = e.y
|
||||||
var vol = e.offsetX / this.trackWidth
|
var vol = 1 - e.offsetY / this.trackHeight
|
||||||
vol = Math.min(Math.max(vol, 0), 1)
|
vol = Math.min(Math.max(vol, 0), 1)
|
||||||
this.volume = vol
|
this.volume = vol
|
||||||
document.body.addEventListener('mousemove', this.mousemove)
|
document.body.addEventListener('mousemove', this.mousemove)
|
||||||
@@ -137,7 +130,7 @@ export default {
|
|||||||
this.clickVolumeIcon()
|
this.clickVolumeIcon()
|
||||||
},
|
},
|
||||||
clickVolumeTrack(e) {
|
clickVolumeTrack(e) {
|
||||||
var vol = e.offsetX / this.trackWidth
|
var vol = 1 - e.offsetY / this.trackHeight
|
||||||
vol = Math.min(Math.max(vol, 0), 1)
|
vol = Math.min(Math.max(vol, 0), 1)
|
||||||
this.volume = vol
|
this.volume = vol
|
||||||
}
|
}
|
||||||
@@ -147,7 +140,7 @@ export default {
|
|||||||
this.isMute = true
|
this.isMute = true
|
||||||
}
|
}
|
||||||
const storageVolume = localStorage.getItem('volume')
|
const storageVolume = localStorage.getItem('volume')
|
||||||
if (storageVolume) {
|
if (storageVolume && !isNaN(storageVolume)) {
|
||||||
this.volume = parseFloat(storageVolume)
|
this.volume = parseFloat(storageVolume)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -157,4 +150,4 @@ export default {
|
|||||||
document.body.removeEventListener('mouseup', this.mouseup)
|
document.body.removeEventListener('mouseup', this.mouseup)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -56,24 +56,15 @@ export default {
|
|||||||
},
|
},
|
||||||
imgSrc() {
|
imgSrc() {
|
||||||
if (!this.imagePath) return null
|
if (!this.imagePath) return null
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
return `${this.$config.routerBasePath}/api/authors/${this.authorId}/image?ts=${this.updatedAt}`
|
||||||
// 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}`
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
imageLoaded() {
|
imageLoaded() {
|
||||||
var aspectRatio = 1.25
|
|
||||||
if (this.$refs.wrapper) {
|
|
||||||
aspectRatio = this.$refs.wrapper.clientHeight / this.$refs.wrapper.clientWidth
|
|
||||||
}
|
|
||||||
if (this.$refs.img) {
|
if (this.$refs.img) {
|
||||||
var { naturalWidth, naturalHeight } = this.$refs.img
|
var { naturalWidth, naturalHeight } = this.$refs.img
|
||||||
var imgAr = naturalHeight / naturalWidth
|
var imgAr = naturalHeight / naturalWidth
|
||||||
var arDiff = Math.abs(imgAr - aspectRatio)
|
if (imgAr < 0.5 || imgAr > 2) {
|
||||||
if (arDiff > 0.15) {
|
|
||||||
this.showCoverBg = true
|
this.showCoverBg = true
|
||||||
} else {
|
} else {
|
||||||
this.showCoverBg = false
|
this.showCoverBg = false
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ export default {
|
|||||||
|
|
||||||
var img = document.createElement('img')
|
var img = document.createElement('img')
|
||||||
img.src = src
|
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.className = 'absolute top-0 left-0 w-full h-full'
|
||||||
img.style.objectFit = showCoverBg ? 'contain' : 'cover'
|
img.style.objectFit = showCoverBg ? 'contain' : 'cover'
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</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="flex items-center my-2 max-w-md">
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<p id="explicit-content-permissions-toggle">{{ $strings.LabelPermissionsAccessExplicitContent }}</p>
|
<p id="explicit-content-permissions-toggle">{{ $strings.LabelPermissionsAccessExplicitContent }}</p>
|
||||||
@@ -296,7 +305,7 @@ export default {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
this.$toast.error(`${this.$strings.ToastAccountUpdateFailed}: ${data.error}`)
|
this.$toast.error(`${this.$strings.ToastFailedToUpdate}: ${data.error}`)
|
||||||
} else {
|
} else {
|
||||||
console.log('Account updated', data.user)
|
console.log('Account updated', data.user)
|
||||||
|
|
||||||
@@ -313,7 +322,7 @@ export default {
|
|||||||
this.processing = false
|
this.processing = false
|
||||||
console.error('Failed to update account', error)
|
console.error('Failed to update account', error)
|
||||||
var errMsg = error.response ? error.response.data || '' : ''
|
var errMsg = error.response ? error.response.data || '' : ''
|
||||||
this.$toast.error(errMsg || this.$strings.ToastFailedToUpdateAccount)
|
this.$toast.error(errMsg || this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
submitCreateAccount() {
|
submitCreateAccount() {
|
||||||
@@ -351,10 +360,11 @@ export default {
|
|||||||
update: type === 'admin',
|
update: type === 'admin',
|
||||||
delete: type === 'admin',
|
delete: type === 'admin',
|
||||||
upload: type === 'admin',
|
upload: type === 'admin',
|
||||||
accessExplicitContent: true,
|
accessExplicitContent: type === 'admin',
|
||||||
accessAllLibraries: true,
|
accessAllLibraries: true,
|
||||||
accessAllTags: true,
|
accessAllTags: true,
|
||||||
selectedTagsNotAccessible: false
|
selectedTagsNotAccessible: false,
|
||||||
|
createEreader: type === 'admin'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
@@ -386,8 +396,9 @@ export default {
|
|||||||
upload: false,
|
upload: false,
|
||||||
accessAllLibraries: true,
|
accessAllLibraries: true,
|
||||||
accessAllTags: true,
|
accessAllTags: true,
|
||||||
accessExplicitContent: true,
|
accessExplicitContent: false,
|
||||||
selectedTagsNotAccessible: false
|
selectedTagsNotAccessible: false,
|
||||||
|
createEreader: false
|
||||||
},
|
},
|
||||||
librariesAccessible: [],
|
librariesAccessible: [],
|
||||||
itemTagsSelected: []
|
itemTagsSelected: []
|
||||||
|
|||||||
@@ -54,8 +54,7 @@ export default {
|
|||||||
options: {
|
options: {
|
||||||
provider: undefined,
|
provider: undefined,
|
||||||
overrideDetails: true,
|
overrideDetails: true,
|
||||||
overrideCover: true,
|
overrideCover: true
|
||||||
overrideDefaults: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -99,8 +98,8 @@ export default {
|
|||||||
init() {
|
init() {
|
||||||
// If we don't have a set provider (first open of dialog) or we've switched library, set
|
// 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
|
// the selected provider to the current library default provider
|
||||||
if (!this.options.provider || this.options.lastUsedLibrary != this.currentLibraryId) {
|
if (!this.options.provider || this.lastUsedLibrary != this.currentLibraryId) {
|
||||||
this.options.lastUsedLibrary = this.currentLibraryId
|
this.lastUsedLibrary = this.currentLibraryId
|
||||||
this.options.provider = this.libraryProvider
|
this.options.provider = this.libraryProvider
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -116,10 +115,10 @@ export default {
|
|||||||
libraryItemIds: this.selectedBookIds
|
libraryItemIds: this.selectedBookIds
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.info('Batch quick match of ' + this.selectedBookIds.length + ' books started!')
|
this.$toast.info(this.$getString('ToastBatchQuickMatchStarted', [this.selectedBookIds.length]))
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.$toast.error('Batch quick match failed')
|
this.$toast.error(this.$strings.ToastBatchQuickMatchFailed)
|
||||||
console.error('Failed to batch quick match', error)
|
console.error('Failed to batch quick match', error)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|||||||
@@ -5,24 +5,26 @@
|
|||||||
<p class="text-3xl text-white truncate">{{ $strings.LabelYourBookmarks }}</p>
|
<p class="text-3xl text-white truncate">{{ $strings.LabelYourBookmarks }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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 rounded-lg bg-bg box-shadow-md relative" style="max-height: 80vh">
|
||||||
<div v-if="show" class="w-full h-full">
|
<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">
|
<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>
|
</template>
|
||||||
<div v-if="!bookmarks.length" class="flex h-32 items-center justify-center">
|
</div>
|
||||||
<p class="text-xl">{{ $strings.MessageNoBookmarks }}</p>
|
<div v-else class="flex h-32 items-center justify-center">
|
||||||
</div>
|
<p class="text-xl">{{ $strings.MessageNoBookmarks }}</p>
|
||||||
<div v-if="!hideCreate" class="w-full h-px bg-white bg-opacity-10" />
|
</div>
|
||||||
<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 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">
|
<div class="w-16 max-w-16 text-center">
|
||||||
<p class="text-sm font-mono text-gray-400">
|
<p class="text-sm font-mono text-gray-400">
|
||||||
{{ this.$secondsToTimestamp(currentTime) }}
|
{{ this.$secondsToTimestamp(currentTime / playbackRate) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-2">
|
<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>
|
</div>
|
||||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-symbols text-2xl -mt-px">add</span></ui-btn>
|
<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>
|
</div>
|
||||||
@@ -45,6 +47,7 @@ export default {
|
|||||||
default: 0
|
default: 0
|
||||||
},
|
},
|
||||||
libraryItemId: String,
|
libraryItemId: String,
|
||||||
|
playbackRate: Number,
|
||||||
hideCreate: Boolean
|
hideCreate: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -57,6 +60,7 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
show(newVal) {
|
show(newVal) {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
|
this.selectedBookmark = null
|
||||||
this.showBookmarkTitleInput = false
|
this.showBookmarkTitleInput = false
|
||||||
this.newBookmarkTitle = ''
|
this.newBookmarkTitle = ''
|
||||||
}
|
}
|
||||||
@@ -72,7 +76,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
canCreateBookmark() {
|
canCreateBookmark() {
|
||||||
return !this.bookmarks.find((bm) => bm.time === this.currentTime)
|
return !this.bookmarks.find((bm) => Math.abs(this.currentTime - bm.time) < 1)
|
||||||
},
|
},
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.$store.state.serverSettings.dateFormat
|
return this.$store.state.serverSettings.dateFormat
|
||||||
@@ -102,19 +106,6 @@ export default {
|
|||||||
clickBookmark(bm) {
|
clickBookmark(bm) {
|
||||||
this.$emit('select', 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() {
|
submitCreateBookmark() {
|
||||||
if (!this.newBookmarkTitle) {
|
if (!this.newBookmarkTitle) {
|
||||||
this.newBookmarkTitle = this.$formatDatetime(Date.now(), this.dateFormat, this.timeFormat)
|
this.newBookmarkTitle = this.$formatDatetime(Date.now(), this.dateFormat, this.timeFormat)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<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 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">
|
||||||
<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">
|
<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>
|
<span class="material-symbols text-2xl md:text-4xl">close</span>
|
||||||
</div>
|
</button>
|
||||||
<div ref="content" class="text-white">
|
<div ref="content" class="text-white">
|
||||||
<form v-if="selectedSeries" @submit.prevent="submitSeriesForm">
|
<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="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<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" />
|
<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">
|
<button class="absolute top-4 right-4 landscape:top-4 landscape:right-4 md:portrait:top-5 md:portrait:right-5 lg:top-5 lg:right-5 inline-flex text-gray-200 hover:text-white" aria-label="Close modal" @click="clickClose">
|
||||||
<span class="material-symbols text-2xl landscape:text-2xl md:portrait:text-4xl lg:text-4xl">close</span>
|
<span class="material-symbols text-2xl landscape:text-2xl md:portrait:text-4xl lg:text-4xl">close</span>
|
||||||
</button>
|
</button>
|
||||||
<slot name="outer" />
|
<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 />
|
<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">
|
<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 />
|
<ui-loading-indicator />
|
||||||
@@ -126,6 +126,9 @@ export default {
|
|||||||
|
|
||||||
this.$eventBus.$on('modal-hotkey', this.hotkey)
|
this.$eventBus.$on('modal-hotkey', this.hotkey)
|
||||||
this.$store.commit('setOpenModal', this.name)
|
this.$store.commit('setOpenModal', this.name)
|
||||||
|
|
||||||
|
// Set focus to the modal content
|
||||||
|
this.content.focus()
|
||||||
},
|
},
|
||||||
setHide() {
|
setHide() {
|
||||||
if (this.content) this.content.style.transform = 'scale(0)'
|
if (this.content) this.content.style.transform = 'scale(0)'
|
||||||
|
|||||||
@@ -59,12 +59,19 @@ export default {
|
|||||||
setJumpBackwardAmount(val) {
|
setJumpBackwardAmount(val) {
|
||||||
this.jumpBackwardAmount = val
|
this.jumpBackwardAmount = val
|
||||||
this.$store.dispatch('user/updateUserSettings', { jumpBackwardAmount: val })
|
this.$store.dispatch('user/updateUserSettings', { jumpBackwardAmount: 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')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack')
|
this.settingsUpdated()
|
||||||
this.jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount')
|
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||||
this.jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount')
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -19,12 +19,13 @@
|
|||||||
<ui-text-input v-model="currentShareUrl" show-copy readonly class="text-base h-10" />
|
<ui-text-input v-model="currentShareUrl" show-copy readonly class="text-base h-10" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full py-2 px-1">
|
<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>
|
<p v-else>{{ $strings.LabelPermanent }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<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">
|
<div class="w-full sm:w-48">
|
||||||
<label class="px-1 text-sm font-semibold block">{{ $strings.LabelSlug }}</label>
|
<label class="px-1 text-sm font-semibold block">{{ $strings.LabelSlug }}</label>
|
||||||
<ui-text-input v-model="newShareSlug" class="text-base h-10" />
|
<ui-text-input v-model="newShareSlug" class="text-base h-10" />
|
||||||
@@ -46,6 +47,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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('MessageShareURLWillBe', [demoShareUrl])" />
|
||||||
<p class="text-sm text-gray-300 py-1 px-1" v-html="$getString('MessageShareExpirationWillBe', [expirationDateString])" />
|
<p class="text-sm text-gray-300 py-1 px-1" v-html="$getString('MessageShareExpirationWillBe', [expirationDateString])" />
|
||||||
</template>
|
</template>
|
||||||
@@ -81,7 +91,8 @@ export default {
|
|||||||
text: this.$strings.LabelDays,
|
text: this.$strings.LabelDays,
|
||||||
value: 'days'
|
value: 'days'
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
isDownloadable: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -112,11 +123,11 @@ export default {
|
|||||||
return this.$store.state.user.user
|
return this.$store.state.user.user
|
||||||
},
|
},
|
||||||
demoShareUrl() {
|
demoShareUrl() {
|
||||||
return `${window.origin}/share/${this.newShareSlug}`
|
return `${window.origin}${this.$config.routerBasePath}/share/${this.newShareSlug}`
|
||||||
},
|
},
|
||||||
currentShareUrl() {
|
currentShareUrl() {
|
||||||
if (!this.currentShare) return ''
|
if (!this.currentShare) return ''
|
||||||
return `${window.origin}/share/${this.currentShare.slug}`
|
return `${window.origin}${this.$config.routerBasePath}/share/${this.currentShare.slug}`
|
||||||
},
|
},
|
||||||
currentShareTimeRemaining() {
|
currentShareTimeRemaining() {
|
||||||
if (!this.currentShare) return 'Error'
|
if (!this.currentShare) return 'Error'
|
||||||
@@ -172,7 +183,8 @@ export default {
|
|||||||
slug: this.newShareSlug,
|
slug: this.newShareSlug,
|
||||||
mediaItemType: 'book',
|
mediaItemType: 'book',
|
||||||
mediaItemId: this.libraryItem.media.id,
|
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.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export default {
|
|||||||
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
|
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
const errorMsg = error.response ? error.response.data : null
|
const errorMsg = error.response ? error.response.data : null
|
||||||
this.$toast.error(errorMsg || this.$strings.ToastAuthorUpdateFailed)
|
this.$toast.error(errorMsg || this.$strings.ToastFailedToUpdate)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
if (result) {
|
if (result) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<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">
|
<div class="w-16 max-w-16 text-center">
|
||||||
<p class="text-sm font-mono text-gray-400">
|
<p class="text-sm font-mono text-gray-400">
|
||||||
{{ this.$secondsToTimestamp(bookmark.time) }}
|
{{ this.$secondsToTimestamp(bookmark.time / playbackRate) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow overflow-hidden px-2">
|
<div class="flex-grow overflow-hidden px-2">
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<form @submit.prevent="submitUpdate">
|
<form @submit.prevent="submitUpdate">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-grow pr-2">
|
<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>
|
</div>
|
||||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-symbols text-2xl -mt-px">forward</span></ui-btn>
|
<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">
|
<div class="pl-2 flex items-center">
|
||||||
@@ -35,7 +35,8 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
highlight: Boolean
|
highlight: Boolean,
|
||||||
|
playbackRate: Number
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -83,11 +84,19 @@ export default {
|
|||||||
if (this.newBookmarkTitle === this.bookmark.title) {
|
if (this.newBookmarkTitle === this.bookmark.title) {
|
||||||
return this.cancelEditing()
|
return this.cancelEditing()
|
||||||
}
|
}
|
||||||
var bookmark = { ...this.bookmark }
|
const bookmark = { ...this.bookmark }
|
||||||
bookmark.title = this.newBookmarkTitle
|
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>
|
</script>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal v-model="show" name="changelog" :width="800" :height="'unset'">
|
<modals-modal v-model="show" name="changelog" :width="800" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<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>
|
</div>
|
||||||
</template>
|
</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">
|
<div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh">
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<div class="custom-text" v-html="getChangelog(release)" />
|
<div class="custom-text" v-html="getChangelog(release)" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="release !== releasesToShow[releasesToShow.length - 1]" class="border-b border-black-300 my-8" />
|
<div v-if="release !== releasesToShow[releasesToShow.length - 1]" :key="`${release.name}-divider`" class="border-b border-black-300 my-8" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
|
|||||||
@@ -138,7 +138,6 @@ export default {
|
|||||||
.$post(`/api/collections/${collection.id}/batch/remove`, { books: this.selectedBookIds })
|
.$post(`/api/collections/${collection.id}/batch/remove`, { books: this.selectedBookIds })
|
||||||
.then((updatedCollection) => {
|
.then((updatedCollection) => {
|
||||||
console.log(`Books removed from collection`, updatedCollection)
|
console.log(`Books removed from collection`, updatedCollection)
|
||||||
this.$toast.success(this.$strings.ToastCollectionItemsRemoveSuccess)
|
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -152,7 +151,6 @@ export default {
|
|||||||
.$delete(`/api/collections/${collection.id}/book/${this.selectedLibraryItemId}`)
|
.$delete(`/api/collections/${collection.id}/book/${this.selectedLibraryItemId}`)
|
||||||
.then((updatedCollection) => {
|
.then((updatedCollection) => {
|
||||||
console.log(`Book removed from collection`, updatedCollection)
|
console.log(`Book removed from collection`, updatedCollection)
|
||||||
this.$toast.success(this.$strings.ToastCollectionItemsRemoveSuccess)
|
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -167,12 +165,11 @@ export default {
|
|||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
||||||
if (this.showBatchCollectionModal) {
|
if (this.showBatchCollectionModal) {
|
||||||
// BATCH Remove books
|
// BATCH Add books
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/collections/${collection.id}/batch/add`, { books: this.selectedBookIds })
|
.$post(`/api/collections/${collection.id}/batch/add`, { books: this.selectedBookIds })
|
||||||
.then((updatedCollection) => {
|
.then((updatedCollection) => {
|
||||||
console.log(`Books added to collection`, updatedCollection)
|
console.log(`Books added to collection`, updatedCollection)
|
||||||
this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
|
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -187,7 +184,6 @@ export default {
|
|||||||
.$post(`/api/collections/${collection.id}/book`, { id: this.selectedLibraryItemId })
|
.$post(`/api/collections/${collection.id}/book`, { id: this.selectedLibraryItemId })
|
||||||
.then((updatedCollection) => {
|
.then((updatedCollection) => {
|
||||||
console.log(`Book added to collection`, updatedCollection)
|
console.log(`Book added to collection`, updatedCollection)
|
||||||
this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
|
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -214,7 +210,6 @@ export default {
|
|||||||
.$post('/api/collections', newCollection)
|
.$post('/api/collections', newCollection)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log('New Collection Created', data)
|
console.log('New Collection Created', data)
|
||||||
this.$toast.success(`Collection "${data.name}" created`)
|
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.newCollectionName = ''
|
this.newCollectionName = ''
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export default {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update collection', error)
|
console.error('Failed to update collection', error)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.$toast.error(this.$strings.ToastCollectionUpdateFailed)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update device', error)
|
console.error('Failed to update device', error)
|
||||||
this.$toast.error(this.$strings.ToastDeviceUpdateFailed)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|||||||
@@ -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">
|
<modals-modal v-model="show" name="edit-book" :width="800" :height="height" :processing="processing" :content-margin-top="marginTop">
|
||||||
<template #outer>
|
<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">
|
<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>
|
</div>
|
||||||
</template>
|
</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">
|
<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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
<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">
|
||||||
<div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevBook" @mousedown.prevent>arrow_back_ios</div>
|
<component v-if="libraryItem && show" :is="tabName" :library-item="libraryItem" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
|
||||||
</div>
|
|
||||||
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
|
||||||
<div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextBook" @mousedown.prevent>arrow_forward_ios</div>
|
|
||||||
</div>
|
</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">
|
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||||
<component v-if="libraryItem && show" :is="tabName" :library-item="libraryItem" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
|
<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>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<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">
|
<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">
|
<div class="flex -mb-0.5">
|
||||||
<p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': checkingNewEpisodes }">{{ $strings.LabelLimit }}</p>
|
<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.">
|
<ui-tooltip direction="top" :text="$strings.LabelMaxEpisodesToDownload">
|
||||||
<span class="material-symbols text-base">info</span>
|
<span class="material-symbols text-base">info</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,7 +99,7 @@ export default {
|
|||||||
|
|
||||||
if (this.maxEpisodesToDownload < 0) {
|
if (this.maxEpisodesToDownload < 0) {
|
||||||
this.maxEpisodesToDownload = 3
|
this.maxEpisodesToDownload = 3
|
||||||
this.$toast.error('Invalid max episodes to download')
|
this.$toast.error(this.$strings.ToastInvalidMaxEpisodesToDownload)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +113,10 @@ export default {
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
console.log('updateResult', updateResult)
|
console.log('updateResult', updateResult)
|
||||||
|
} else if (!lastEpisodeCheck) {
|
||||||
|
this.$toast.error(this.$strings.ToastDateTimeInvalidOrIncomplete)
|
||||||
|
this.checkingNewEpisodes = false
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$axios
|
this.$axios
|
||||||
@@ -120,9 +124,9 @@ export default {
|
|||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.episodes && response.episodes.length) {
|
if (response.episodes && response.episodes.length) {
|
||||||
console.log('New 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 {
|
} else {
|
||||||
this.$toast.info('No new episodes found')
|
this.$toast.info(this.$strings.ToastNoNewEpisodesFound)
|
||||||
}
|
}
|
||||||
this.checkingNewEpisodes = false
|
this.checkingNewEpisodes = false
|
||||||
})
|
})
|
||||||
@@ -141,4 +145,4 @@ export default {
|
|||||||
this.setLastEpisodeCheckInput()
|
this.setLastEpisodeCheckInput()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" :label="$strings.LabelTitle" />
|
<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">
|
<p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('title', mediaMetadata.title)">{{ mediaMetadata.title || '' }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('title', mediaMetadata.title)">{{ mediaMetadata.title || '' }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" :label="$strings.LabelSubtitle" />
|
<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">
|
<p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('subtitle', mediaMetadata.subtitle)">{{ mediaMetadata.subtitle }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('subtitle', mediaMetadata.subtitle)">{{ mediaMetadata.subtitle }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" :label="$strings.LabelAuthor" />
|
<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">
|
<p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('author', mediaMetadata.authorName)">{{ mediaMetadata.authorName }}</a>
|
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('author', mediaMetadata.authorName)">{{ mediaMetadata.authorName }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-multi-select v-model="selectedMatch.narrator" :items="narrators" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" />
|
<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">
|
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('narrator', mediaMetadata.narrators)">{{ mediaMetadata.narratorName }}</a>
|
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('narrator', mediaMetadata.narrators)">{{ mediaMetadata.narratorName }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" />
|
<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">
|
<p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</a>
|
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" :label="$strings.LabelPublisher" />
|
<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">
|
<p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publisher', mediaMetadata.publisher)">{{ mediaMetadata.publisher }}</a>
|
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publisher', mediaMetadata.publisher)">{{ mediaMetadata.publisher }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" :label="$strings.LabelPublishYear" />
|
<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">
|
<p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publishedYear', mediaMetadata.publishedYear)">{{ mediaMetadata.publishedYear }}</a>
|
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publishedYear', mediaMetadata.publishedYear)">{{ mediaMetadata.publishedYear }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<widgets-series-input-widget v-model="selectedMatch.series" :disabled="!selectedMatchUsage.series" />
|
<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">
|
<p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('series', mediaMetadata.series)">{{ mediaMetadata.seriesName }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('series', mediaMetadata.series)">{{ mediaMetadata.seriesName }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-multi-select v-model="selectedMatch.genres" :items="genres" :disabled="!selectedMatchUsage.genres" :label="$strings.LabelGenres" />
|
<ui-multi-select v-model="selectedMatch.genres" :items="genres" :disabled="!selectedMatchUsage.genres" :label="$strings.LabelGenres" />
|
||||||
<p v-if="mediaMetadata.genres?.length" class="text-xs ml-1 text-white text-opacity-60">
|
<p v-if="mediaMetadata.genres?.length" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('genres', mediaMetadata.genres)">{{ mediaMetadata.genres.join(', ') }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('genres', mediaMetadata.genres)">{{ mediaMetadata.genres.join(', ') }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-multi-select v-model="selectedMatch.tags" :items="tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" />
|
<ui-multi-select v-model="selectedMatch.tags" :items="tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" />
|
||||||
<p v-if="media.tags?.length" class="text-xs ml-1 text-white text-opacity-60">
|
<p v-if="media.tags?.length" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('tags', media.tags)">{{ media.tags.join(', ') }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('tags', media.tags)">{{ media.tags.join(', ') }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,7 +151,7 @@
|
|||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" :label="$strings.LabelLanguage" />
|
<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">
|
<p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('language', mediaMetadata.language)">{{ mediaMetadata.language }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('language', mediaMetadata.language)">{{ mediaMetadata.language }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" />
|
<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">
|
<p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('isbn', mediaMetadata.isbn)">{{ mediaMetadata.isbn }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('isbn', mediaMetadata.isbn)">{{ mediaMetadata.isbn }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,7 +169,7 @@
|
|||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" />
|
<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">
|
<p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('asin', mediaMetadata.asin)">{{ mediaMetadata.asin }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('asin', mediaMetadata.asin)">{{ mediaMetadata.asin }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -179,7 +179,7 @@
|
|||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.itunesId" type="number" :disabled="!selectedMatchUsage.itunesId" label="iTunes ID" />
|
<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">
|
<p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesId', mediaMetadata.itunesId)">{{ mediaMetadata.itunesId }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesId', mediaMetadata.itunesId)">{{ mediaMetadata.itunesId }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.feedUrl" :disabled="!selectedMatchUsage.feedUrl" label="RSS Feed URL" />
|
<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">
|
<p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('feedUrl', mediaMetadata.feedUrl)">{{ mediaMetadata.feedUrl }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('feedUrl', mediaMetadata.feedUrl)">{{ mediaMetadata.feedUrl }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -197,7 +197,7 @@
|
|||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.itunesPageUrl" :disabled="!selectedMatchUsage.itunesPageUrl" label="iTunes Page URL" />
|
<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">
|
<p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesPageUrl', mediaMetadata.itunesPageUrl)">{{ mediaMetadata.itunesPageUrl }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesPageUrl', mediaMetadata.itunesPageUrl)">{{ mediaMetadata.itunesPageUrl }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" :label="$strings.LabelReleaseDate" />
|
<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">
|
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('releaseDate', mediaMetadata.releaseDate)">{{ mediaMetadata.releaseDate }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('releaseDate', mediaMetadata.releaseDate)">{{ mediaMetadata.releaseDate }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -623,7 +623,7 @@ export default {
|
|||||||
this.clearSelectedMatch()
|
this.clearSelectedMatch()
|
||||||
this.$emit('selectTab', 'details')
|
this.$emit('selectTab', 'details')
|
||||||
} else {
|
} else {
|
||||||
this.$toast.error(this.$strings.ToastItemDetailsUpdateFailed)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.clearSelectedMatch()
|
this.clearSelectedMatch()
|
||||||
|
|||||||
@@ -2,28 +2,28 @@
|
|||||||
<div class="w-full h-full relative">
|
<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">
|
<div id="scheduleWrapper" class="w-full overflow-y-auto px-2 py-4 md:px-6 md:py-6">
|
||||||
<template v-if="!feedUrl">
|
<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>
|
||||||
<template v-if="feedUrl || autoDownloadEpisodes">
|
<template v-if="feedUrl || autoDownloadEpisodes">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<p class="text-base md:text-xl font-semibold">Schedule Automatic Episode Downloads</p>
|
<p class="text-base md:text-xl font-semibold">{{ $strings.HeaderScheduleEpisodeDownloads }}</p>
|
||||||
<ui-checkbox v-model="enableAutoDownloadEpisodes" label="Enable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" />
|
<ui-checkbox v-model="enableAutoDownloadEpisodes" :label="$strings.LabelEnable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
|
<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-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">
|
<p class="pl-4 text-base">
|
||||||
Max episodes to keep
|
{{ $strings.LabelMaxEpisodesToKeep }}
|
||||||
<span class="material-symbols icon-text">info</span>
|
<span class="material-symbols icon-text">info</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
|
<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-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">
|
<p class="pl-4 text-base">
|
||||||
Max new episodes to download per check
|
{{ $strings.LabelMaxEpisodesToDownloadPerCheck }}
|
||||||
<span class="material-symbols icon-text">info</span>
|
<span class="material-symbols icon-text">info</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -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 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 items-center px-2 md:px-4">
|
||||||
<div class="flex-grow" />
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,18 +33,18 @@
|
|||||||
<span class="material-symbols text-lg ml-2">launch</span>
|
<span class="material-symbols text-lg ml-2">launch</span>
|
||||||
</ui-btn>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- queued alert -->
|
<!-- queued alert -->
|
||||||
<widgets-alert v-if="isMetadataEmbedQueued" type="warning" class="mt-4">
|
<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>
|
</widgets-alert>
|
||||||
|
|
||||||
<!-- processing alert -->
|
<!-- processing alert -->
|
||||||
<widgets-alert v-if="isEmbedTaskRunning" type="warning" class="mt-4">
|
<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>
|
</widgets-alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
quickEmbed() {
|
quickEmbed() {
|
||||||
const payload = {
|
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) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.$axios
|
this.$axios
|
||||||
|
|||||||
@@ -111,7 +111,6 @@ export default {
|
|||||||
},
|
},
|
||||||
updateLibrary(library) {
|
updateLibrary(library) {
|
||||||
this.mapLibraryToCopy(library)
|
this.mapLibraryToCopy(library)
|
||||||
console.log('Updated library', this.libraryCopy)
|
|
||||||
},
|
},
|
||||||
getNewLibraryData() {
|
getNewLibraryData() {
|
||||||
return {
|
return {
|
||||||
@@ -128,7 +127,9 @@ export default {
|
|||||||
autoScanCronExpression: null,
|
autoScanCronExpression: null,
|
||||||
hideSingleBookSeries: false,
|
hideSingleBookSeries: false,
|
||||||
onlyShowLaterBooksInContinueSeries: false,
|
onlyShowLaterBooksInContinueSeries: false,
|
||||||
metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
|
metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'],
|
||||||
|
markAsFinishedPercentComplete: null,
|
||||||
|
markAsFinishedTimeRemaining: 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -160,7 +161,7 @@ export default {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (!this.libraryCopy.folders.length) {
|
if (!this.libraryCopy.folders.length) {
|
||||||
this.$toast.error('Library must have at least 1 path')
|
this.$toast.error(this.$strings.ToastMustHaveAtLeastOnePath)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +223,7 @@ export default {
|
|||||||
if (error.response && error.response.data) {
|
if (error.response && error.response.data) {
|
||||||
this.$toast.error(error.response.data)
|
this.$toast.error(error.response.data)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.error(this.$strings.ToastLibraryUpdateFailed)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
}
|
}
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
@@ -236,7 +237,6 @@ export default {
|
|||||||
this.show = false
|
this.show = false
|
||||||
this.$toast.success(this.$getString('ToastLibraryCreateSuccess', [res.name]))
|
this.$toast.success(this.$getString('ToastLibraryCreateSuccess', [res.name]))
|
||||||
if (!this.$store.state.libraries.currentLibraryId) {
|
if (!this.$store.state.libraries.currentLibraryId) {
|
||||||
console.log('Setting initially library id', res.id)
|
|
||||||
// First library added
|
// First library added
|
||||||
this.$store.dispatch('libraries/fetch', res.id)
|
this.$store.dispatch('libraries/fetch', res.id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +1,94 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full px-1 md:px-4 py-1 mb-4">
|
<div class="w-full h-full px-1 md:px-4 py-1 mb-4">
|
||||||
<div class="flex items-center py-3">
|
<div class="flex flex-wrap">
|
||||||
<ui-toggle-switch v-model="useSquareBookCovers" @input="formUpdated" />
|
<div class="flex items-center p-2 w-full md:w-1/2">
|
||||||
<ui-tooltip :text="$strings.LabelSettingsSquareBookCoversHelp">
|
<ui-toggle-switch v-model="useSquareBookCovers" size="sm" @input="formUpdated" />
|
||||||
<p class="pl-4 text-base">
|
<ui-tooltip :text="$strings.LabelSettingsSquareBookCoversHelp">
|
||||||
{{ $strings.LabelSettingsSquareBookCovers }}
|
<p class="pl-4 text-sm">
|
||||||
<span class="material-symbols icon-text text-sm">info</span>
|
{{ $strings.LabelSettingsSquareBookCovers }}
|
||||||
</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-symbols icon-text text-sm">info</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-symbols icon-text text-sm">info</span>
|
<span class="material-symbols icon-text text-sm">info</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="p-2 w-full md:w-1/2">
|
||||||
<div v-if="isBookLibrary" class="py-3">
|
<div class="flex items-center">
|
||||||
<div class="flex items-center">
|
<ui-toggle-switch v-if="!globalWatcherDisabled" v-model="enableWatcher" size="sm" @input="formUpdated" />
|
||||||
<ui-toggle-switch v-model="onlyShowLaterBooksInContinueSeries" @input="formUpdated" />
|
<ui-toggle-switch v-else disabled size="sm" :value="false" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp">
|
<p class="pl-4 text-sm">{{ $strings.LabelSettingsEnableWatcherForLibrary }}</p>
|
||||||
<p class="pl-4 text-base">
|
</div>
|
||||||
{{ $strings.LabelSettingsOnlyShowLaterBooksInContinueSeries }}
|
<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>
|
<span class="material-symbols icon-text text-sm">info</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
|
||||||
<div v-if="isBookLibrary" class="py-3">
|
<div class="flex items-center">
|
||||||
<div class="flex items-center">
|
<ui-toggle-switch v-model="skipMatchingMediaWithAsin" size="sm" @input="formUpdated" />
|
||||||
<ui-toggle-switch v-model="epubsAllowScriptedContent" @input="formUpdated" />
|
<p class="pl-4 text-sm">{{ $strings.LabelSettingsSkipMatchingBooksWithASIN }}</p>
|
||||||
<ui-tooltip :text="$strings.LabelSettingsEpubsAllowScriptedContentHelp">
|
</div>
|
||||||
<p class="pl-4 text-base">
|
</div>
|
||||||
{{ $strings.LabelSettingsEpubsAllowScriptedContent }}
|
<div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
|
||||||
<span class="material-symbols icon-text text-sm">info</span>
|
<div class="flex items-center">
|
||||||
</p>
|
<ui-toggle-switch v-model="skipMatchingMediaWithIsbn" size="sm" @input="formUpdated" />
|
||||||
</ui-tooltip>
|
<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>
|
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -97,7 +113,9 @@ export default {
|
|||||||
epubsAllowScriptedContent: false,
|
epubsAllowScriptedContent: false,
|
||||||
hideSingleBookSeries: false,
|
hideSingleBookSeries: false,
|
||||||
onlyShowLaterBooksInContinueSeries: false,
|
onlyShowLaterBooksInContinueSeries: false,
|
||||||
podcastSearchRegion: 'us'
|
podcastSearchRegion: 'us',
|
||||||
|
markAsFinishedWhen: 'timeRemaining',
|
||||||
|
markAsFinishedValue: 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -119,10 +137,34 @@ export default {
|
|||||||
providers() {
|
providers() {
|
||||||
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
|
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
|
||||||
return this.$store.state.scanners.providers
|
return this.$store.state.scanners.providers
|
||||||
|
},
|
||||||
|
maskAsFinishedWhenItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelSettingsLibraryMarkAsFinishedTimeRemaining,
|
||||||
|
value: 'timeRemaining'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelSettingsLibraryMarkAsFinishedPercentComplete,
|
||||||
|
value: 'percentComplete'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
markAsFinishedWhenChanged(val) {
|
||||||
|
if (val === 'percentComplete' && this.markAsFinishedValue > 100) {
|
||||||
|
this.markAsFinishedValue = 100
|
||||||
|
}
|
||||||
|
this.formUpdated()
|
||||||
|
},
|
||||||
|
markAsFinishedChanged(val) {
|
||||||
|
this.formUpdated()
|
||||||
|
},
|
||||||
getLibraryData() {
|
getLibraryData() {
|
||||||
|
let markAsFinishedTimeRemaining = this.markAsFinishedWhen === 'timeRemaining' ? Number(this.markAsFinishedValue) : null
|
||||||
|
let markAsFinishedPercentComplete = this.markAsFinishedWhen === 'percentComplete' ? Number(this.markAsFinishedValue) : null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
settings: {
|
settings: {
|
||||||
coverAspectRatio: this.useSquareBookCovers ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD,
|
coverAspectRatio: this.useSquareBookCovers ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD,
|
||||||
@@ -133,7 +175,9 @@ export default {
|
|||||||
epubsAllowScriptedContent: !!this.epubsAllowScriptedContent,
|
epubsAllowScriptedContent: !!this.epubsAllowScriptedContent,
|
||||||
hideSingleBookSeries: !!this.hideSingleBookSeries,
|
hideSingleBookSeries: !!this.hideSingleBookSeries,
|
||||||
onlyShowLaterBooksInContinueSeries: !!this.onlyShowLaterBooksInContinueSeries,
|
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.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries
|
||||||
this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries
|
this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries
|
||||||
this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us'
|
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() {
|
mounted() {
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
<div class="w-full border border-black-200 p-4 my-8">
|
<div class="w-full border border-black-200 p-4 my-8">
|
||||||
<div class="flex flex-wrap items-center">
|
<div class="flex flex-wrap items-center">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-lg">Remove metadata files in library item folders</p>
|
<p class="text-lg">{{ $strings.LabelRemoveMetadataFile }}</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="max-w-sm text-sm pt-2 text-gray-300">{{ $getString('LabelRemoveMetadataFileHelp', [mediaType]) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<div>
|
<div>
|
||||||
<ui-btn class="mb-4 block" @click.stop="removeAllMetadataClick('json')">Remove all metadata.json</ui-btn>
|
<ui-btn class="mb-4 block" @click.stop="removeAllMetadataClick('json')">{{ $strings.LabelRemoveAllMetadataJson }}</ui-btn>
|
||||||
<ui-btn @click.stop="removeAllMetadataClick('abs')">Remove all metadata.abs</ui-btn>
|
<ui-btn @click.stop="removeAllMetadataClick('abs')">{{ $strings.LabelRemoveAllMetadataAbs }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +43,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
removeAllMetadataClick(ext) {
|
removeAllMetadataClick(ext) {
|
||||||
const payload = {
|
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,
|
persistent: true,
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
@@ -60,16 +60,16 @@ export default {
|
|||||||
.$post(`/api/libraries/${this.libraryId}/remove-metadata?ext=${ext}`)
|
.$post(`/api/libraries/${this.libraryId}/remove-metadata?ext=${ext}`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (!data.found) {
|
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) {
|
} else if (!data.removed) {
|
||||||
this.$toast.success(`No metadata.${ext} files removed`)
|
this.$toast.success(this.$getString('ToastMetadataFilesRemovedNoneRemoved', [ext]))
|
||||||
} else {
|
} else {
|
||||||
this.$toast.success(`Successfully removed ${data.removed} metadata.${ext} files`)
|
this.$toast.success(this.$getString('ToastMetadataFilesRemovedSuccess', [data.removed, ext]))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove metadata files', 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(() => {
|
.finally(() => {
|
||||||
this.$emit('update:processing', false)
|
this.$emit('update:processing', false)
|
||||||
|
|||||||
@@ -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" />
|
<ui-checkbox v-model="enableAutoScan" @input="toggleEnableAutoScan" :label="$strings.LabelEnable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" />
|
||||||
</div>
|
</div>
|
||||||
<widgets-cron-expression-builder ref="cronExpressionBuilder" v-if="enableAutoScan" v-model="cronExpression" @input="updatedCron" />
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,13 @@ export default {
|
|||||||
return this.notificationData.events || []
|
return this.notificationData.events || []
|
||||||
},
|
},
|
||||||
eventOptions() {
|
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() {
|
selectedEventData() {
|
||||||
return this.notificationEvents.find((e) => e.name === this.newNotification.eventName)
|
return this.notificationEvents.find((e) => e.name === this.newNotification.eventName)
|
||||||
@@ -132,7 +138,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update notification', error)
|
console.error('Failed to update notification', error)
|
||||||
this.$toast.error(this.$strings.ToastNotificationUpdateFailed)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|||||||
@@ -130,12 +130,11 @@ export default {
|
|||||||
.$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects })
|
.$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects })
|
||||||
.then((updatedPlaylist) => {
|
.then((updatedPlaylist) => {
|
||||||
console.log(`Items removed from playlist`, updatedPlaylist)
|
console.log(`Items removed from playlist`, updatedPlaylist)
|
||||||
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
|
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove items from playlist', error)
|
console.error('Failed to remove items from playlist', error)
|
||||||
this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -148,12 +147,11 @@ export default {
|
|||||||
.$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects })
|
.$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects })
|
||||||
.then((updatedPlaylist) => {
|
.then((updatedPlaylist) => {
|
||||||
console.log(`Items added to playlist`, updatedPlaylist)
|
console.log(`Items added to playlist`, updatedPlaylist)
|
||||||
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
|
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to add items to playlist', error)
|
console.error('Failed to add items to playlist', error)
|
||||||
this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -174,7 +172,6 @@ export default {
|
|||||||
.$post('/api/playlists', newPlaylist)
|
.$post('/api/playlists', newPlaylist)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log('New playlist created', data)
|
console.log('New playlist created', data)
|
||||||
this.$toast.success(this.$strings.ToastPlaylistCreateSuccess + ': ' + data.name)
|
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.newPlaylistName = ''
|
this.newPlaylistName = ''
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export default {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update playlist', error)
|
console.error('Failed to update playlist', error)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -170,6 +170,12 @@ export default {
|
|||||||
this.show = false
|
this.show = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
libraryItemUpdated(libraryItem) {
|
||||||
|
const episode = libraryItem.media.episodes.find((e) => e.id === this.selectedEpisodeId)
|
||||||
|
if (episode) {
|
||||||
|
this.episodeItem = episode
|
||||||
|
}
|
||||||
|
},
|
||||||
hotkey(action) {
|
hotkey(action) {
|
||||||
if (action === this.$hotkeys.Modal.NEXT_PAGE) {
|
if (action === this.$hotkeys.Modal.NEXT_PAGE) {
|
||||||
this.goNextEpisode()
|
this.goNextEpisode()
|
||||||
@@ -178,9 +184,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
registerListeners() {
|
registerListeners() {
|
||||||
|
if (this.libraryItem) {
|
||||||
|
this.$eventBus.$on(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
|
||||||
|
}
|
||||||
this.$eventBus.$on('modal-hotkey', this.hotkey)
|
this.$eventBus.$on('modal-hotkey', this.hotkey)
|
||||||
},
|
},
|
||||||
unregisterListeners() {
|
unregisterListeners() {
|
||||||
|
if (this.libraryItem) {
|
||||||
|
this.$eventBus.$on(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
|
||||||
|
}
|
||||||
this.$eventBus.$off('modal-hotkey', this.hotkey)
|
this.$eventBus.$off('modal-hotkey', this.hotkey)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -156,7 +156,12 @@ export default {
|
|||||||
return this.selectedFolder.fullPath
|
return this.selectedFolder.fullPath
|
||||||
},
|
},
|
||||||
podcastTypes() {
|
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: {
|
methods: {
|
||||||
|
|||||||
@@ -18,6 +18,23 @@
|
|||||||
<p dir="auto" class="text-lg font-semibold mb-6">{{ title }}</p>
|
<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" v-html="description" />
|
||||||
<p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p>
|
<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>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
</template>
|
</template>
|
||||||
@@ -54,7 +71,7 @@ export default {
|
|||||||
return this.episode.description || ''
|
return this.episode.description || ''
|
||||||
},
|
},
|
||||||
media() {
|
media() {
|
||||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
return this.libraryItem?.media || {}
|
||||||
},
|
},
|
||||||
mediaMetadata() {
|
mediaMetadata() {
|
||||||
return this.media.metadata || {}
|
return this.media.metadata || {}
|
||||||
@@ -65,6 +82,14 @@ export default {
|
|||||||
podcastAuthor() {
|
podcastAuthor() {
|
||||||
return this.mediaMetadata.author
|
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() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<ui-dropdown v-model="newEpisode.episodeType" :label="$strings.LabelEpisodeType" :items="episodeTypes" small />
|
<ui-dropdown v-model="newEpisode.episodeType" :label="$strings.LabelEpisodeType" :items="episodeTypes" small />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-2/5 p-1">
|
<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>
|
||||||
<div class="w-full p-1">
|
<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" />
|
||||||
@@ -33,11 +33,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="enclosureUrl" class="pb-4 pt-6">
|
<div v-if="enclosureUrl" class="pb-4 pt-6">
|
||||||
<ui-text-input-with-label :value="enclosureUrl" readonly class="text-xs">
|
<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>
|
</ui-text-input-with-label>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="py-4">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -97,7 +97,12 @@ export default {
|
|||||||
return this.enclosure.url
|
return this.enclosure.url
|
||||||
},
|
},
|
||||||
episodeTypes() {
|
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: {
|
methods: {
|
||||||
@@ -140,11 +145,18 @@ export default {
|
|||||||
return null
|
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()
|
const updatedDetails = this.getUpdatePayload()
|
||||||
if (!Object.keys(updatedDetails).length) {
|
if (!Object.keys(updatedDetails).length) {
|
||||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.updateDetails(updatedDetails)
|
return this.updateDetails(updatedDetails)
|
||||||
},
|
},
|
||||||
async 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) => {
|
const updateResult = await this.$axios.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatedDetails).catch((error) => {
|
||||||
console.error('Failed update episode', error)
|
console.error('Failed update episode', error)
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
this.$toast.error(error?.response?.data || 'Failed to update episode')
|
this.$toast.error(error?.response?.data || this.$strings.ToastFailedToUpdate)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
if (updateResult) {
|
if (updateResult) {
|
||||||
if (updateResult) {
|
this.$toast.success(this.$strings.ToastItemUpdateSuccess)
|
||||||
this.$toast.success('Podcast episode updated')
|
return true
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</p>
|
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</p>
|
||||||
|
|
||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
<ui-text-input v-model="currentFeed.feedUrl" readonly />
|
<ui-text-input :value="feedUrl" readonly />
|
||||||
|
|
||||||
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(currentFeed.feedUrl)">content_copy</span>
|
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feedUrl)">content_copy</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="currentFeed.meta" class="mt-5">
|
<div v-if="currentFeed.meta" class="mt-5">
|
||||||
@@ -111,8 +111,11 @@ export default {
|
|||||||
userIsAdminOrUp() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
|
feedUrl() {
|
||||||
|
return this.currentFeed ? `${window.origin}${this.$config.routerBasePath}${this.currentFeed.feedUrl}` : ''
|
||||||
|
},
|
||||||
demoFeedUrl() {
|
demoFeedUrl() {
|
||||||
return `${window.origin}/feed/${this.newFeedSlug}`
|
return `${window.origin}${this.$config.routerBasePath}/feed/${this.newFeedSlug}`
|
||||||
},
|
},
|
||||||
isHttp() {
|
isHttp() {
|
||||||
return window.origin.startsWith('http://')
|
return window.origin.startsWith('http://')
|
||||||
@@ -139,7 +142,7 @@ export default {
|
|||||||
slug: this.newFeedSlug,
|
slug: this.newFeedSlug,
|
||||||
metadataDetails: this.metadataDetails
|
metadataDetails: this.metadataDetails
|
||||||
}
|
}
|
||||||
if (this.$isDev) payload.serverAddress = `http://localhost:3333${this.$config.routerBasePath}`
|
if (this.$isDev) payload.serverAddress = process.env.serverUrl
|
||||||
|
|
||||||
console.log('Payload', payload)
|
console.log('Payload', payload)
|
||||||
this.$axios
|
this.$axios
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedGeneral }}</p>
|
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedGeneral }}</p>
|
||||||
|
|
||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
<ui-text-input v-model="feed.feedUrl" readonly />
|
<ui-text-input :value="feedUrl" readonly />
|
||||||
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feed.feedUrl)">content_copy</span>
|
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feedUrl)">content_copy</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="feed.meta" class="mt-5">
|
<div v-if="feed.meta" class="mt-5">
|
||||||
@@ -70,6 +70,9 @@ export default {
|
|||||||
},
|
},
|
||||||
_feed() {
|
_feed() {
|
||||||
return this.feed || {}
|
return this.feed || {}
|
||||||
|
},
|
||||||
|
feedUrl() {
|
||||||
|
return this.feed ? `${window.origin}${this.$config.routerBasePath}${this.feed.feedUrl}` : ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,38 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center pt-4 pb-2 lg:pt-0 lg:pb-2">
|
<div class="flex justify-center pt-4 pb-2 lg:pt-0 lg:pb-2">
|
||||||
<div class="flex-grow" />
|
<div class="flex items-center justify-center flex-grow">
|
||||||
<template v-if="!loading">
|
<template v-if="!loading">
|
||||||
<ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 lg:mr-8">
|
<ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 lg:mr-8">
|
||||||
<button :aria-label="$strings.ButtonPreviousChapter" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
|
<button :aria-label="$strings.ButtonPreviousChapter" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
|
||||||
<span class="material-symbols text-2xl sm:text-3xl">first_page</span>
|
<span class="material-symbols text-2xl sm:text-3xl">first_page</span>
|
||||||
|
</button>
|
||||||
|
</ui-tooltip>
|
||||||
|
<ui-tooltip direction="top" :text="jumpBackwardText">
|
||||||
|
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
||||||
|
<span class="material-symbols text-2xl sm:text-3xl">replay</span>
|
||||||
|
</button>
|
||||||
|
</ui-tooltip>
|
||||||
|
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
||||||
|
<span class="material-symbols fill text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
<ui-tooltip direction="top" :text="jumpForwardText">
|
||||||
<ui-tooltip direction="top" :text="jumpBackwardText">
|
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
||||||
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
<span class="material-symbols text-2xl sm:text-3xl">forward_media</span>
|
||||||
<span class="material-symbols text-2xl sm:text-3xl">replay</span>
|
</button>
|
||||||
</button>
|
</ui-tooltip>
|
||||||
</ui-tooltip>
|
<ui-tooltip direction="top" :text="hasNextLabel" class="ml-4 lg:ml-8">
|
||||||
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
<button :aria-label="hasNextLabel" :disabled="!hasNext" class="text-gray-300 disabled:text-gray-500" @mousedown.prevent @mouseup.prevent @click.stop="next">
|
||||||
<span class="material-symbols fill text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
<span class="material-symbols text-2xl sm:text-3xl">last_page</span>
|
||||||
</button>
|
</button>
|
||||||
<ui-tooltip direction="top" :text="jumpForwardText">
|
</ui-tooltip>
|
||||||
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
</template>
|
||||||
<span class="material-symbols text-2xl sm:text-3xl">forward_media</span>
|
<template v-else>
|
||||||
</button>
|
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
|
||||||
</ui-tooltip>
|
<span class="material-symbols text-2xl">autorenew</span>
|
||||||
<ui-tooltip direction="top" :text="hasNextLabel" class="ml-4 lg:ml-8">
|
</div>
|
||||||
<button :aria-label="hasNextLabel" :disabled="!hasNext" class="text-gray-300 disabled:text-gray-500" @mousedown.prevent @mouseup.prevent @click.stop="next">
|
</template>
|
||||||
<span class="material-symbols text-2xl sm:text-3xl">last_page</span>
|
</div>
|
||||||
</button>
|
|
||||||
</ui-tooltip>
|
|
||||||
<controls-playback-speed-control v-model="playbackRateInput" @input="playbackRateUpdated" @change="playbackRateChanged" />
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
|
|
||||||
<span class="material-symbols text-2xl">autorenew</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="flex-grow" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -41,7 +40,6 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
seekLoading: Boolean,
|
seekLoading: Boolean,
|
||||||
playbackRate: Number,
|
|
||||||
paused: Boolean,
|
paused: Boolean,
|
||||||
hasNextChapter: Boolean,
|
hasNextChapter: Boolean,
|
||||||
hasNextItemInQueue: Boolean
|
hasNextItemInQueue: Boolean
|
||||||
@@ -50,14 +48,6 @@ export default {
|
|||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
playbackRateInput: {
|
|
||||||
get() {
|
|
||||||
return this.playbackRate
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$emit('update:playbackRate', val)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
jumpForwardText() {
|
jumpForwardText() {
|
||||||
return this.getJumpText('jumpForwardAmount', this.$strings.ButtonJumpForward)
|
return this.getJumpText('jumpForwardAmount', this.$strings.ButtonJumpForward)
|
||||||
},
|
},
|
||||||
@@ -89,15 +79,6 @@ export default {
|
|||||||
jumpForward() {
|
jumpForward() {
|
||||||
this.$emit('jumpForward')
|
this.$emit('jumpForward')
|
||||||
},
|
},
|
||||||
playbackRateUpdated(playbackRate) {
|
|
||||||
this.$emit('setPlaybackRate', playbackRate)
|
|
||||||
},
|
|
||||||
playbackRateChanged(playbackRate) {
|
|
||||||
this.$emit('setPlaybackRate', playbackRate)
|
|
||||||
this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => {
|
|
||||||
console.error('Failed to update settings', err)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getJumpText(setting, prefix) {
|
getJumpText(setting, prefix) {
|
||||||
const amount = this.$store.getters['user/getUserSetting'](setting)
|
const amount = this.$store.getters['user/getUserSetting'](setting)
|
||||||
if (!amount) return prefix
|
if (!amount) return prefix
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<div class="w-full -mt-6">
|
<div class="w-full -mt-6">
|
||||||
<div class="w-full relative mb-1">
|
<div class="w-full relative mb-1">
|
||||||
<div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full">
|
<div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full">
|
||||||
<!-- <span class="material-symbols text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
|
<controls-playback-speed-control v-model="playbackRate" @input="setPlaybackRate" @change="playbackRateChanged" class="mx-2 block" />
|
||||||
|
|
||||||
<ui-tooltip direction="top" :text="$strings.LabelVolume">
|
<ui-tooltip direction="bottom" :text="$strings.LabelVolume">
|
||||||
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden sm:block" />
|
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden sm:block" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<span v-if="!sleepTimerSet" class="material-symbols text-2xl">snooze</span>
|
<span v-if="!sleepTimerSet" class="material-symbols text-2xl">snooze</span>
|
||||||
<div v-else class="flex items-center">
|
<div v-else class="flex items-center">
|
||||||
<span class="material-symbols text-lg text-warning">snooze</span>
|
<span class="material-symbols text-lg text-warning">snooze</span>
|
||||||
<p class="text-sm sm:text-lg text-warning font-mono font-semibold text-center px-0.5 sm:pb-0.5 sm:min-w-8">{{ sleepTimerRemainingString }}</p>
|
<p class="text-sm sm:text-lg text-warning font-semibold text-center px-0.5 sm:pb-0.5 sm:min-w-8">{{ sleepTimerRemainingString }}</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip direction="top" :text="$strings.LabelViewPlayerSettings">
|
<ui-tooltip direction="top" :text="$strings.LabelViewPlayerSettings">
|
||||||
<button :aria-label="$strings.LabelViewPlayerSettings" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerSettings')">
|
<button :aria-label="$strings.LabelViewPlayerSettings" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="showPlayerSettings">
|
||||||
<span class="material-symbols text-2xl sm:text-2.5xl">settings_slow_motion</span>
|
<span class="material-symbols text-2xl sm:text-2.5xl">settings_slow_motion</span>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -48,18 +48,24 @@
|
|||||||
|
|
||||||
<player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" :current-chapter="currentChapter" :playback-rate="playbackRate" @seek="seek" />
|
<player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" :current-chapter="currentChapter" :playback-rate="playbackRate" @seek="seek" />
|
||||||
|
|
||||||
<div class="flex">
|
<div class="relative flex items-center justify-between">
|
||||||
<p ref="currentTimestamp" class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">00:00:00</p>
|
<div class="flex-grow flex items-center">
|
||||||
<p class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto"> / {{ progressPercent }}%</p>
|
<p ref="currentTimestamp" class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">00:00:00</p>
|
||||||
<div class="flex-grow" />
|
<p class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto"> / {{ progressPercent }}%</p>
|
||||||
<p class="text-xs sm:text-sm text-gray-300 pt-0.5 px-2 truncate">
|
</div>
|
||||||
{{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400"> ({{ $getString('LabelPlayerChapterNumberMarker', [currentChapterIndex + 1, chapters.length]) }})</span>
|
<div class="absolute left-1/2 transform -translate-x-1/2">
|
||||||
</p>
|
<p class="text-xs sm:text-sm text-gray-300 pt-0.5 px-2 truncate">
|
||||||
<div class="flex-grow" />
|
{{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400"> ({{ $getString('LabelPlayerChapterNumberMarker', [currentChapterIndex + 1, chapters.length]) }})</span>
|
||||||
<p class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow flex items-center justify-end">
|
||||||
|
<p class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :playback-rate="playbackRate" :chapters="chapters" @select="selectChapter" />
|
<modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :playback-rate="playbackRate" :chapters="chapters" @select="selectChapter" />
|
||||||
|
|
||||||
|
<modals-player-settings-modal v-model="showPlayerSettingsModal" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -92,6 +98,7 @@ export default {
|
|||||||
audioEl: null,
|
audioEl: null,
|
||||||
seekLoading: false,
|
seekLoading: false,
|
||||||
showChaptersModal: false,
|
showChaptersModal: false,
|
||||||
|
showPlayerSettingsModal: false,
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
duration: 0
|
duration: 0
|
||||||
}
|
}
|
||||||
@@ -178,22 +185,6 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
toggleFullscreen(isFullscreen) {
|
toggleFullscreen(isFullscreen) {
|
||||||
this.$store.commit('setPlayerIsFullscreen', isFullscreen)
|
this.$store.commit('setPlayerIsFullscreen', isFullscreen)
|
||||||
|
|
||||||
var videoPlayerEl = document.getElementById('video-player')
|
|
||||||
if (videoPlayerEl) {
|
|
||||||
if (isFullscreen) {
|
|
||||||
videoPlayerEl.style.width = '100vw'
|
|
||||||
videoPlayerEl.style.height = '100vh'
|
|
||||||
videoPlayerEl.style.top = '0px'
|
|
||||||
videoPlayerEl.style.left = '0px'
|
|
||||||
} else {
|
|
||||||
videoPlayerEl.style.width = '384px'
|
|
||||||
videoPlayerEl.style.height = '216px'
|
|
||||||
videoPlayerEl.style.top = 'unset'
|
|
||||||
videoPlayerEl.style.bottom = '80px'
|
|
||||||
videoPlayerEl.style.left = '16px'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
setDuration(duration) {
|
setDuration(duration) {
|
||||||
this.duration = duration
|
this.duration = duration
|
||||||
@@ -240,6 +231,12 @@ export default {
|
|||||||
this.playbackRate = Number((this.playbackRate - 0.1).toFixed(1))
|
this.playbackRate = Number((this.playbackRate - 0.1).toFixed(1))
|
||||||
this.setPlaybackRate(this.playbackRate)
|
this.setPlaybackRate(this.playbackRate)
|
||||||
},
|
},
|
||||||
|
playbackRateChanged(playbackRate) {
|
||||||
|
this.setPlaybackRate(playbackRate)
|
||||||
|
this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => {
|
||||||
|
console.error('Failed to update settings', err)
|
||||||
|
})
|
||||||
|
},
|
||||||
setPlaybackRate(playbackRate) {
|
setPlaybackRate(playbackRate) {
|
||||||
this.$emit('setPlaybackRate', playbackRate)
|
this.$emit('setPlaybackRate', playbackRate)
|
||||||
},
|
},
|
||||||
@@ -321,6 +318,9 @@ export default {
|
|||||||
if (!this.chapters.length) return
|
if (!this.chapters.length) return
|
||||||
this.showChaptersModal = !this.showChaptersModal
|
this.showChaptersModal = !this.showChaptersModal
|
||||||
},
|
},
|
||||||
|
showPlayerSettings() {
|
||||||
|
this.showPlayerSettingsModal = !this.showPlayerSettingsModal
|
||||||
|
},
|
||||||
init() {
|
init() {
|
||||||
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
||||||
|
|
||||||
|
|||||||
@@ -97,9 +97,9 @@ export default {
|
|||||||
},
|
},
|
||||||
ebookUrl() {
|
ebookUrl() {
|
||||||
if (this.fileId) {
|
if (this.fileId) {
|
||||||
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
|
return `${this.$config.routerBasePath}/api/items/${this.libraryItemId}/ebook/${this.fileId}`
|
||||||
}
|
}
|
||||||
return `/api/items/${this.libraryItemId}/ebook`
|
return `${this.$config.routerBasePath}/api/items/${this.libraryItemId}/ebook`
|
||||||
},
|
},
|
||||||
themeRules() {
|
themeRules() {
|
||||||
const isDark = this.ereaderSettings.theme === 'dark'
|
const isDark = this.ereaderSettings.theme === 'dark'
|
||||||
|
|||||||
@@ -35,22 +35,22 @@
|
|||||||
<div class="flex justify-between pt-12">
|
<div class="flex justify-between pt-12">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-center">{{ $strings.LabelStatsWeekListening }}</p>
|
<p class="text-sm text-center">{{ $strings.LabelStatsWeekListening }}</p>
|
||||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ totalMinutesListeningThisWeek }}</p>
|
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ $formatNumber(totalMinutesListeningThisWeek) }}</p>
|
||||||
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
|
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-center">{{ $strings.LabelStatsDailyAverage }}</p>
|
<p class="text-sm text-center">{{ $strings.LabelStatsDailyAverage }}</p>
|
||||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ averageMinutesPerDay }}</p>
|
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ $formatNumber(averageMinutesPerDay) }}</p>
|
||||||
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
|
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-center">{{ $strings.LabelStatsBestDay }}</p>
|
<p class="text-sm text-center">{{ $strings.LabelStatsBestDay }}</p>
|
||||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ mostListenedDay }}</p>
|
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ $formatNumber(mostListenedDay) }}</p>
|
||||||
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
|
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-center">{{ $strings.LabelStatsDays }}</p>
|
<p class="text-sm text-center">{{ $strings.LabelStatsDays }}</p>
|
||||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ daysInARow }}</p>
|
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ $formatNumber(daysInARow) }}</p>
|
||||||
<p class="text-sm text-center">{{ $strings.LabelStatsInARow }}</p>
|
<p class="text-sm text-center">{{ $strings.LabelStatsInARow }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="heatmap" class="w-full">
|
<div id="heatmap" class="w-full">
|
||||||
<div class="mx-auto" :style="{ height: innerHeight + 160 + 'px', width: innerWidth + 52 + 'px' }" style="background-color: rgba(13, 17, 23, 0)">
|
<div class="mx-auto" :style="{ height: innerHeight + 160 + 'px', width: innerWidth + 52 + 'px' }" style="background-color: rgba(13, 17, 23, 0)">
|
||||||
<p class="mb-2 px-1 text-sm text-gray-200">{{ $getString('MessageListeningSessionsInTheLastYear', [Object.values(daysListening).length]) }}</p>
|
<p class="mb-2 px-1 text-sm text-gray-200">{{ $getString('MessageDaysListenedInTheLastYear', [daysListenedInTheLastYear]) }}</p>
|
||||||
<div class="border border-white border-opacity-25 rounded py-2 w-full" style="background-color: #232323" :style="{ height: innerHeight + 80 + 'px' }">
|
<div class="border border-white border-opacity-25 rounded py-2 w-full" style="background-color: #232323" :style="{ height: innerHeight + 80 + 'px' }">
|
||||||
<div :style="{ width: innerWidth + 'px', height: innerHeight + 'px' }" class="ml-10 mt-5 absolute" @mouseover="mouseover" @mouseout="mouseout">
|
<div :style="{ width: innerWidth + 'px', height: innerHeight + 'px' }" class="ml-10 mt-5 absolute" @mouseover="mouseover" @mouseout="mouseout">
|
||||||
<div v-for="dayLabel in dayLabels" :key="dayLabel.label" :style="dayLabel.style" class="absolute top-0 left-0 text-gray-300">{{ dayLabel.label }}</div>
|
<div v-for="dayLabel in dayLabels" :key="dayLabel.label" :style="dayLabel.style" class="absolute top-0 left-0 text-gray-300">{{ dayLabel.label }}</div>
|
||||||
@@ -37,6 +37,7 @@ export default {
|
|||||||
innerHeight: 13 * 7,
|
innerHeight: 13 * 7,
|
||||||
blockWidth: 13,
|
blockWidth: 13,
|
||||||
data: [],
|
data: [],
|
||||||
|
daysListenedInTheLastYear: 0,
|
||||||
monthLabels: [],
|
monthLabels: [],
|
||||||
tooltipEl: null,
|
tooltipEl: null,
|
||||||
tooltipTextEl: null,
|
tooltipTextEl: null,
|
||||||
@@ -62,9 +63,6 @@ export default {
|
|||||||
dayOfWeekToday() {
|
dayOfWeekToday() {
|
||||||
return new Date().getDay()
|
return new Date().getDay()
|
||||||
},
|
},
|
||||||
firstWeekStart() {
|
|
||||||
return this.$addDaysToToday(-this.daysToShow)
|
|
||||||
},
|
|
||||||
dayLabels() {
|
dayLabels() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -193,46 +191,59 @@ export default {
|
|||||||
buildData() {
|
buildData() {
|
||||||
this.data = []
|
this.data = []
|
||||||
|
|
||||||
var maxValue = 0
|
let maxValue = 0
|
||||||
var minValue = 0
|
let minValue = 0
|
||||||
Object.values(this.daysListening).forEach((val) => {
|
|
||||||
if (val > maxValue) maxValue = val
|
const dates = []
|
||||||
if (!minValue || val < minValue) minValue = val
|
|
||||||
})
|
const numDaysInTheLastYear = 52 * 7 + this.dayOfWeekToday
|
||||||
|
const firstDay = this.$addDaysToToday(-numDaysInTheLastYear)
|
||||||
|
for (let i = 0; i < numDaysInTheLastYear + 1; i++) {
|
||||||
|
const date = i === 0 ? firstDay : this.$addDaysToDate(firstDay, i)
|
||||||
|
const dateString = this.$formatJsDate(date, 'yyyy-MM-dd')
|
||||||
|
|
||||||
|
if (this.daysListening[dateString] > 0) {
|
||||||
|
this.daysListenedInTheLastYear++
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleDayIndex = i - (numDaysInTheLastYear - this.daysToShow)
|
||||||
|
if (visibleDayIndex < 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateObj = {
|
||||||
|
col: Math.floor(visibleDayIndex / 7),
|
||||||
|
row: visibleDayIndex % 7,
|
||||||
|
date,
|
||||||
|
dateString,
|
||||||
|
datePretty: this.$formatJsDate(date, 'MMM d, yyyy'),
|
||||||
|
monthString: this.$formatJsDate(date, 'MMM'),
|
||||||
|
dayOfMonth: Number(dateString.split('-').pop()),
|
||||||
|
yearString: dateString.split('-').shift(),
|
||||||
|
value: this.daysListening[dateString] || 0
|
||||||
|
}
|
||||||
|
dates.push(dateObj)
|
||||||
|
|
||||||
|
if (dateObj.value > 0) {
|
||||||
|
if (dateObj.value > maxValue) maxValue = dateObj.value
|
||||||
|
if (!minValue || dateObj.value < minValue) minValue = dateObj.value
|
||||||
|
}
|
||||||
|
}
|
||||||
const range = maxValue - minValue + 0.01
|
const range = maxValue - minValue + 0.01
|
||||||
|
|
||||||
for (let i = 0; i < this.daysToShow + 1; i++) {
|
for (const dateObj of dates) {
|
||||||
const col = Math.floor(i / 7)
|
let bgColor = this.bgColors[0]
|
||||||
const row = i % 7
|
let outlineColor = this.outlineColors[0]
|
||||||
|
if (dateObj.value) {
|
||||||
const date = i === 0 ? this.firstWeekStart : this.$addDaysToDate(this.firstWeekStart, i)
|
|
||||||
const dateString = this.$formatJsDate(date, 'yyyy-MM-dd')
|
|
||||||
const datePretty = this.$formatJsDate(date, 'MMM d, yyyy')
|
|
||||||
const monthString = this.$formatJsDate(date, 'MMM')
|
|
||||||
const value = this.daysListening[dateString] || 0
|
|
||||||
const x = col * 13
|
|
||||||
const y = row * 13
|
|
||||||
|
|
||||||
var bgColor = this.bgColors[0]
|
|
||||||
var outlineColor = this.outlineColors[0]
|
|
||||||
if (value) {
|
|
||||||
outlineColor = this.outlineColors[1]
|
outlineColor = this.outlineColors[1]
|
||||||
var percentOfAvg = (value - minValue) / range
|
const percentOfAvg = (dateObj.value - minValue) / range
|
||||||
var bgIndex = Math.floor(percentOfAvg * 4) + 1
|
const bgIndex = Math.floor(percentOfAvg * 4) + 1
|
||||||
bgColor = this.bgColors[bgIndex] || 'red'
|
bgColor = this.bgColors[bgIndex] || 'red'
|
||||||
}
|
}
|
||||||
|
|
||||||
this.data.push({
|
this.data.push({
|
||||||
date,
|
...dateObj,
|
||||||
dateString,
|
style: `transform:translate(${dateObj.col * 13}px,${dateObj.row * 13}px);background-color:${bgColor};outline:1px solid ${outlineColor};outline-offset:-1px;`
|
||||||
datePretty,
|
|
||||||
monthString,
|
|
||||||
dayOfMonth: Number(dateString.split('-').pop()),
|
|
||||||
yearString: dateString.split('-').shift(),
|
|
||||||
value,
|
|
||||||
col,
|
|
||||||
row,
|
|
||||||
style: `transform:translate(${x}px,${y}px);background-color:${bgColor};outline:1px solid ${outlineColor};outline-offset:-1px;`
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,6 +271,7 @@ export default {
|
|||||||
const heatmapEl = document.getElementById('heatmap')
|
const heatmapEl = document.getElementById('heatmap')
|
||||||
this.contentWidth = heatmapEl.clientWidth
|
this.contentWidth = heatmapEl.clientWidth
|
||||||
this.maxInnerWidth = this.contentWidth - 52
|
this.maxInnerWidth = this.contentWidth - 52
|
||||||
|
this.daysListenedInTheLastYear = 0
|
||||||
this.buildData()
|
this.buildData()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
|
<div v-if="processing" role="img" :aria-label="$strings.MessageLoading" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
|
||||||
<widgets-loading-spinner />
|
<widgets-loading-spinner />
|
||||||
</div>
|
</div>
|
||||||
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" />
|
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" :aria-label="$getString('LabelPersonalYearReview', [variant + 1])" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<p class="hidden md:block text-xl font-semibold">{{ $getString('HeaderYearReview', [yearInReviewYear]) }}</p>
|
<h1 class="hidden md:block text-xl font-semibold">{{ $getString('HeaderYearReview', [yearInReviewYear]) }}</h1>
|
||||||
<div class="hidden md:block flex-grow" />
|
<div class="hidden md:block flex-grow" />
|
||||||
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide : $strings.LabelYearReviewShow }}</ui-btn>
|
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide : $strings.LabelYearReviewShow }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
@@ -16,17 +16,22 @@
|
|||||||
<div v-if="showYearInReview">
|
<div v-if="showYearInReview">
|
||||||
<div class="w-full h-px bg-slate-200/10 my-4" />
|
<div class="w-full h-px bg-slate-200/10 my-4" />
|
||||||
|
|
||||||
<div class="flex items-center justify-center mb-2 max-w-[800px] mx-auto">
|
<div v-if="availableYears.length > 1" class="mb-2 py-2 max-w-[800px] mx-auto">
|
||||||
|
<!-- year selector -->
|
||||||
|
<ui-dropdown v-model="yearInReviewYear" small :items="availableYears" :disabled="processingYearInReview" class="max-w-24" @input="yearInReviewYearChanged" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div role="toolbar" class="flex items-center justify-center mb-2 max-w-[800px] mx-auto">
|
||||||
<!-- previous button -->
|
<!-- previous button -->
|
||||||
<ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--">
|
<ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" :aria-label="$strings.ButtonPrevious" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--">
|
||||||
<span class="material-symbols text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
<span class="material-symbols text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
||||||
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
|
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<!-- share button -->
|
<!-- share button -->
|
||||||
<ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview">{{ $strings.ButtonShare }} </ui-btn>
|
<ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview">{{ $strings.ButtonShare }} </ui-btn>
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelPersonalYearReview', [yearInReviewVariant + 1]) }}</p>
|
<h2 class="hidden sm:block text-lg font-semibold">{{ $getString('LabelPersonalYearReview', [yearInReviewVariant + 1]) }}</h2>
|
||||||
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p>
|
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
@@ -36,7 +41,7 @@
|
|||||||
<span class="material-symbols sm:!hidden text-lg py-px">refresh</span>
|
<span class="material-symbols sm:!hidden text-lg py-px">refresh</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<!-- next button -->
|
<!-- next button -->
|
||||||
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
|
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" :aria-label="$strings.ButtonNext" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
|
||||||
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
|
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
|
||||||
<span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
<span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
@@ -46,23 +51,23 @@
|
|||||||
<!-- your year in review short -->
|
<!-- your year in review short -->
|
||||||
<div class="w-full max-w-[800px] mx-auto my-4">
|
<div class="w-full max-w-[800px] mx-auto my-4">
|
||||||
<!-- share button -->
|
<!-- share button -->
|
||||||
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex sm:hidden items-center font-semibold mb-1" @click="shareYearInReviewShort">{{ $strings.ButtonShare }}</ui-btn>
|
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex items-center font-semibold mb-1" @click="shareYearInReviewShort">{{ $strings.ButtonShare }}</ui-btn>
|
||||||
<stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" />
|
<stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- your server in review -->
|
<!-- your server in review -->
|
||||||
<div v-if="isAdminOrUp" class="w-full max-w-[800px] mx-auto mb-2 mt-4 border-t pt-4 border-white/10">
|
<div v-if="isAdminOrUp" role="toolbar" class="w-full max-w-[800px] mx-auto mb-2 mt-4 border-t pt-4 border-white/10">
|
||||||
<div class="flex items-center justify-center mb-2">
|
<div class="flex items-center justify-center mb-2">
|
||||||
<!-- previous button -->
|
<!-- previous button -->
|
||||||
<ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--">
|
<ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" :aria-label="$strings.ButtonPrevious" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--">
|
||||||
<span class="material-symbols text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
<span class="material-symbols text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
||||||
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
|
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<!-- share button -->
|
<!-- share button -->
|
||||||
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer">{{ $strings.ButtonShare }} </ui-btn>
|
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer">{{ $strings.ButtonShare }} </ui-btn>
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelServerYearReview', [yearInReviewServerVariant + 1]) }}</p>
|
<h2 class="hidden sm:block text-lg font-semibold">{{ $getString('LabelServerYearReview', [yearInReviewServerVariant + 1]) }}</h2>
|
||||||
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p>
|
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
@@ -72,7 +77,7 @@
|
|||||||
<span class="material-symbols sm:!hidden text-lg py-px">refresh</span>
|
<span class="material-symbols sm:!hidden text-lg py-px">refresh</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<!-- next button -->
|
<!-- next button -->
|
||||||
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
|
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" :aria-label="$strings.ButtonNext" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
|
||||||
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
|
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
|
||||||
<span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
<span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
@@ -88,6 +93,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showYearInReview: false,
|
showYearInReview: false,
|
||||||
|
availableYears: [],
|
||||||
yearInReviewYear: 0,
|
yearInReviewYear: 0,
|
||||||
yearInReviewVariant: 0,
|
yearInReviewVariant: 0,
|
||||||
yearInReviewServerVariant: 0,
|
yearInReviewServerVariant: 0,
|
||||||
@@ -100,6 +106,9 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
isAdminOrUp() {
|
isAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
|
},
|
||||||
|
user() {
|
||||||
|
return this.$store.state.user.user
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -112,25 +121,57 @@ export default {
|
|||||||
shareYearInReviewShort() {
|
shareYearInReviewShort() {
|
||||||
this.$refs.yearInReviewShort.share()
|
this.$refs.yearInReviewShort.share()
|
||||||
},
|
},
|
||||||
|
yearInReviewYearChanged() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.refreshYearInReview()
|
||||||
|
this.refreshYearInReviewServer()
|
||||||
|
})
|
||||||
|
},
|
||||||
refreshYearInReviewServer() {
|
refreshYearInReviewServer() {
|
||||||
this.$refs.yearInReviewServer.refresh()
|
if (this.$refs.yearInReviewServer != null) {
|
||||||
|
this.$refs.yearInReviewServer.refresh()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
refreshYearInReview() {
|
refreshYearInReview() {
|
||||||
this.$refs.yearInReview.refresh()
|
if (this.$refs.yearInReview != null && this.$refs.yearInReviewShort != null) {
|
||||||
this.$refs.yearInReviewShort.refresh()
|
this.$refs.yearInReview.refresh()
|
||||||
|
this.$refs.yearInReviewShort.refresh()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
clickShowYearInReview() {
|
clickShowYearInReview() {
|
||||||
this.showYearInReview = !this.showYearInReview
|
this.showYearInReview = !this.showYearInReview
|
||||||
|
},
|
||||||
|
getAvailableYears() {
|
||||||
|
if (this.user) {
|
||||||
|
const oldestDate = this.user.createdAt
|
||||||
|
if (oldestDate) {
|
||||||
|
const date = new Date(oldestDate)
|
||||||
|
const oldestYear = date.getFullYear()
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
|
||||||
|
const years = []
|
||||||
|
for (let year = currentYear; year >= oldestYear; year--) {
|
||||||
|
years.push({ value: year, text: year.toString() })
|
||||||
|
}
|
||||||
|
|
||||||
|
return years
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback on error
|
||||||
|
return [{ value: this.yearInReviewYear, text: this.yearInReviewYear.toString() }]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
this.yearInReviewYear = new Date().getFullYear()
|
this.yearInReviewYear = new Date().getFullYear()
|
||||||
|
|
||||||
// When not December show previous year
|
// When not December show previous year
|
||||||
if (new Date().getMonth() < 11) {
|
if (new Date().getMonth() < 11) {
|
||||||
this.yearInReviewYear--
|
this.yearInReviewYear--
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.availableYears = this.getAvailableYears()
|
||||||
|
|
||||||
if (typeof navigator.share !== 'undefined' && navigator.share) {
|
if (typeof navigator.share !== 'undefined' && navigator.share) {
|
||||||
this.showShareButton = true
|
this.showShareButton = true
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
|
<div v-if="processing" role="img" :aria-label="$strings.MessageLoading" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
|
||||||
<widgets-loading-spinner />
|
<widgets-loading-spinner />
|
||||||
</div>
|
</div>
|
||||||
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" />
|
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" :aria-label="$getString('LabelServerYearReview', [variant + 1])" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update collection', error)
|
console.error('Failed to update collection', error)
|
||||||
this.$toast.error(this.$strings.ToastCollectionUpdateFailed)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
editBook(book) {
|
editBook(book) {
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update playlist', error)
|
console.error('Failed to update playlist', error)
|
||||||
this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ export default {
|
|||||||
this.users = res.users.sort((a, b) => {
|
this.users = res.users.sort((a, b) => {
|
||||||
return a.createdAt - b.createdAt
|
return a.createdAt - b.createdAt
|
||||||
})
|
})
|
||||||
|
this.$emit('numUsers', this.users.length)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
|
|||||||
@@ -218,12 +218,11 @@ export default {
|
|||||||
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
|
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
|
||||||
} else {
|
} else {
|
||||||
console.log(`Item removed from playlist`, updatedPlaylist)
|
console.log(`Item removed from playlist`, updatedPlaylist)
|
||||||
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove item from playlist', error)
|
console.error('Failed to remove item from playlist', error)
|
||||||
this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processingRemove = false
|
this.processingRemove = false
|
||||||
|
|||||||
@@ -12,10 +12,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="h-8 flex items-center">
|
<div class="h-8 flex items-center">
|
||||||
<div class="w-full inline-flex justify-between max-w-xl">
|
<div class="w-full inline-flex justify-between max-w-xl">
|
||||||
<p v-if="episode?.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p>
|
<p v-if="episode?.season" class="text-sm text-gray-300">{{ $getString('LabelSeasonNumber', [episode.season]) }}</p>
|
||||||
<p v-if="episode?.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p>
|
<p v-if="episode?.episode" class="text-sm text-gray-300">{{ $getString('LabelEpisodeNumber', [episode.episode]) }}</p>
|
||||||
<p v-if="episode?.chapters?.length" class="text-sm text-gray-300">{{ episode.chapters.length }} Chapters</p>
|
<p v-if="episode?.chapters?.length" class="text-sm text-gray-300">{{ $getString('LabelChapterCount', [episode.chapters.length]) }}</p>
|
||||||
<p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, dateFormat) }}</p>
|
<p v-if="publishedAt" class="text-sm text-gray-300">{{ $getString('LabelPublishedDate', [$formatDate(publishedAt, dateFormat)]) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ export default {
|
|||||||
return this.episode?.title || ''
|
return this.episode?.title || ''
|
||||||
},
|
},
|
||||||
episodeSubtitle() {
|
episodeSubtitle() {
|
||||||
return this.episode?.subtitle || ''
|
return this.episode?.subtitle || this.episode?.description || ''
|
||||||
},
|
},
|
||||||
episodeType() {
|
episodeType() {
|
||||||
return this.episode?.episodeType || ''
|
return this.episode?.episodeType || ''
|
||||||
@@ -132,13 +132,13 @@ export default {
|
|||||||
return this.store.state.streamIsPlaying && this.isStreaming
|
return this.store.state.streamIsPlaying && this.isStreaming
|
||||||
},
|
},
|
||||||
timeRemaining() {
|
timeRemaining() {
|
||||||
if (this.streamIsPlaying) return 'Playing'
|
if (this.streamIsPlaying) return this.$strings.ButtonPlaying
|
||||||
if (!this.itemProgress) return this.$elapsedPretty(this.episode?.duration || 0)
|
if (!this.itemProgress) return this.$elapsedPretty(this.episode?.duration || 0)
|
||||||
if (this.userIsFinished) return 'Finished'
|
if (this.userIsFinished) return this.$strings.LabelFinished
|
||||||
|
|
||||||
const duration = this.itemProgress.duration || this.episode?.duration || 0
|
const duration = this.itemProgress.duration || this.episode?.duration || 0
|
||||||
const remaining = Math.floor(duration - this.itemProgress.currentTime)
|
const remaining = Math.floor(duration - this.itemProgress.currentTime)
|
||||||
return `${this.$elapsedPretty(remaining)} left`
|
return this.$getString('LabelTimeLeft', [this.$elapsedPretty(remaining)])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -182,7 +182,7 @@ export default {
|
|||||||
toggleFinished(confirmed = false) {
|
toggleFinished(confirmed = false) {
|
||||||
if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) {
|
if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) {
|
||||||
const payload = {
|
const payload = {
|
||||||
message: `Are you sure you want to mark "${this.episodeTitle}" as finished?`,
|
message: this.$getString('MessageConfirmMarkItemFinished', [this.episodeTitle]),
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.toggleFinished(true)
|
this.toggleFinished(true)
|
||||||
|
|||||||
@@ -25,13 +25,12 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p> -->
|
|
||||||
<div v-if="episodes.length" class="w-full py-3 mx-auto flex">
|
<div v-if="episodes.length" class="w-full py-3 mx-auto flex">
|
||||||
<form @submit.prevent="submit" class="flex flex-grow">
|
<form @submit.prevent="submit" class="flex flex-grow">
|
||||||
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
|
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative min-h-[176px]">
|
<div class="relative min-h-44">
|
||||||
<template v-for="episode in totalEpisodes">
|
<template v-for="episode in totalEpisodes">
|
||||||
<div :key="episode" :id="`episode-${episode - 1}`" class="w-full h-44 px-2 py-3 overflow-hidden relative border-b border-white/10">
|
<div :key="episode" :id="`episode-${episode - 1}`" class="w-full h-44 px-2 py-3 overflow-hidden relative border-b border-white/10">
|
||||||
<!-- episode is mounted here -->
|
<!-- episode is mounted here -->
|
||||||
@@ -40,7 +39,7 @@
|
|||||||
<div v-if="isSearching" class="w-full h-full absolute inset-0 flex justify-center py-12" :class="{ 'bg-black/50': totalEpisodes }">
|
<div v-if="isSearching" class="w-full h-full absolute inset-0 flex justify-center py-12" :class="{ 'bg-black/50': totalEpisodes }">
|
||||||
<ui-loading-indicator />
|
<ui-loading-indicator />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!totalEpisodes" class="h-44 flex items-center justify-center">
|
<div v-else-if="!totalEpisodes" id="no-episodes" class="h-44 flex items-center justify-center">
|
||||||
<p class="text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
<p class="text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,7 +80,8 @@ export default {
|
|||||||
episodeComponentRefs: {},
|
episodeComponentRefs: {},
|
||||||
windowHeight: 0,
|
windowHeight: 0,
|
||||||
episodesTableOffsetTop: 0,
|
episodesTableOffsetTop: 0,
|
||||||
episodeRowHeight: 176
|
episodeRowHeight: 44 * 4, // h-44,
|
||||||
|
currScrollTop: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -93,17 +93,18 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
contextMenuItems() {
|
contextMenuItems() {
|
||||||
if (!this.userIsAdminOrUp) return []
|
const menuItems = []
|
||||||
return [
|
if (this.userIsAdminOrUp) {
|
||||||
{
|
menuItems.push({
|
||||||
text: 'Quick match all episodes',
|
text: this.$strings.MessageQuickMatchAllEpisodes,
|
||||||
action: 'quick-match-episodes'
|
action: 'quick-match-episodes'
|
||||||
},
|
})
|
||||||
{
|
}
|
||||||
text: this.allEpisodesFinished ? this.$strings.MessageMarkAllEpisodesNotFinished : this.$strings.MessageMarkAllEpisodesFinished,
|
menuItems.push({
|
||||||
action: 'batch-mark-as-finished'
|
text: this.allEpisodesFinished ? this.$strings.MessageMarkAllEpisodesNotFinished : this.$strings.MessageMarkAllEpisodesFinished,
|
||||||
}
|
action: 'batch-mark-as-finished'
|
||||||
]
|
})
|
||||||
|
return menuItems
|
||||||
},
|
},
|
||||||
sortItems() {
|
sortItems() {
|
||||||
return [
|
return [
|
||||||
@@ -261,21 +262,21 @@ export default {
|
|||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
message: 'Quick matching episodes will overwrite details if a match is found. Only unmatched episodes will be updated. Are you sure?',
|
message: this.$strings.MessageConfirmQuickMatchEpisodes,
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/podcasts/${this.libraryItem.id}/match-episodes?override=1`)
|
.$post(`/api/podcasts/${this.libraryItem.id}/match-episodes?override=1`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.numEpisodesUpdated) {
|
if (data.numEpisodesUpdated) {
|
||||||
this.$toast.success(`${data.numEpisodesUpdated} episodes updated`)
|
this.$toast.success(this.$getString('ToastEpisodeUpdateSuccess', [data.numEpisodesUpdated]))
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to request match episodes', error)
|
console.error('Failed to request match episodes', error)
|
||||||
this.$toast.error('Failed to match episodes')
|
this.$toast.error(this.$strings.ToastFailedToMatch)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.processing = false
|
this.processing = false
|
||||||
@@ -484,9 +485,8 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scroll(evt) {
|
handleScroll() {
|
||||||
if (!evt?.target?.scrollTop) return
|
const scrollTop = this.currScrollTop
|
||||||
const scrollTop = Math.max(evt.target.scrollTop - this.episodesTableOffsetTop, 0)
|
|
||||||
let firstEpisodeIndex = Math.floor(scrollTop / this.episodeRowHeight)
|
let firstEpisodeIndex = Math.floor(scrollTop / this.episodeRowHeight)
|
||||||
let lastEpisodeIndex = Math.ceil((scrollTop + this.windowHeight) / this.episodeRowHeight)
|
let lastEpisodeIndex = Math.ceil((scrollTop + this.windowHeight) / this.episodeRowHeight)
|
||||||
lastEpisodeIndex = Math.min(this.totalEpisodes - 1, lastEpisodeIndex)
|
lastEpisodeIndex = Math.min(this.totalEpisodes - 1, lastEpisodeIndex)
|
||||||
@@ -501,6 +501,12 @@ export default {
|
|||||||
})
|
})
|
||||||
this.mountEpisodes(firstEpisodeIndex, lastEpisodeIndex + 1)
|
this.mountEpisodes(firstEpisodeIndex, lastEpisodeIndex + 1)
|
||||||
},
|
},
|
||||||
|
scroll(evt) {
|
||||||
|
if (!evt?.target?.scrollTop) return
|
||||||
|
const scrollTop = Math.max(evt.target.scrollTop - this.episodesTableOffsetTop, 0)
|
||||||
|
this.currScrollTop = scrollTop
|
||||||
|
this.handleScroll()
|
||||||
|
},
|
||||||
initListeners() {
|
initListeners() {
|
||||||
const itemPageWrapper = document.getElementById('item-page-wrapper')
|
const itemPageWrapper = document.getElementById('item-page-wrapper')
|
||||||
if (itemPageWrapper) {
|
if (itemPageWrapper) {
|
||||||
@@ -514,6 +520,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
filterSortChanged() {
|
filterSortChanged() {
|
||||||
|
// Save filterKey and sortKey to local storage
|
||||||
|
localStorage.setItem('podcastEpisodesFilter', this.filterKey)
|
||||||
|
localStorage.setItem('podcastEpisodesSortBy', this.sortKey + (this.sortDesc ? '-desc' : ''))
|
||||||
|
|
||||||
this.init()
|
this.init()
|
||||||
},
|
},
|
||||||
refresh() {
|
refresh() {
|
||||||
@@ -528,14 +538,32 @@ export default {
|
|||||||
this.episodesTableOffsetTop = (lazyEpisodesTableEl?.offsetTop || 0) + 64
|
this.episodesTableOffsetTop = (lazyEpisodesTableEl?.offsetTop || 0) + 64
|
||||||
|
|
||||||
this.windowHeight = window.innerHeight
|
this.windowHeight = window.innerHeight
|
||||||
this.episodesPerPage = Math.ceil(this.windowHeight / this.episodeRowHeight)
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.mountEpisodes(0, Math.min(this.episodesPerPage, this.totalEpisodes))
|
this.recalcEpisodeRowHeight()
|
||||||
|
this.episodesPerPage = Math.ceil(this.windowHeight / this.episodeRowHeight)
|
||||||
|
// Maybe update currScrollTop if items were removed
|
||||||
|
const itemPageWrapper = document.getElementById('item-page-wrapper')
|
||||||
|
const { scrollHeight, clientHeight } = itemPageWrapper
|
||||||
|
const maxScrollTop = scrollHeight - clientHeight
|
||||||
|
this.currScrollTop = Math.min(this.currScrollTop, maxScrollTop)
|
||||||
|
this.handleScroll()
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
recalcEpisodeRowHeight() {
|
||||||
|
const episodeRowEl = document.getElementById('episode-0') || document.getElementById('no-episodes')
|
||||||
|
if (episodeRowEl) {
|
||||||
|
const height = getComputedStyle(episodeRowEl).height
|
||||||
|
this.episodeRowHeight = parseInt(height) || this.episodeRowHeight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.filterKey = localStorage.getItem('podcastEpisodesFilter') || 'incomplete'
|
||||||
|
const sortBy = localStorage.getItem('podcastEpisodesSortBy') || 'publishedAt-desc'
|
||||||
|
this.sortKey = sortBy.split('-')[0]
|
||||||
|
this.sortDesc = sortBy.split('-')[1] === 'desc'
|
||||||
|
|
||||||
this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
|
this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
|
||||||
this.initListeners()
|
this.initListeners()
|
||||||
this.init()
|
this.init()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
|
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
|
||||||
<slot :disabled="disabled" :showMenu="showMenu" :clickShowMenu="clickShowMenu" :processing="processing">
|
<slot :disabled="disabled" :showMenu="showMenu" :clickShowMenu="clickShowMenu" :processing="processing">
|
||||||
<button v-if="!processing" type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
<button v-if="!processing" type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" :aria-label="$strings.LabelMore" aria-haspopup="menu" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||||
<span class="material-symbols text-2xl" :class="iconClass"></span>
|
<span class="material-symbols text-2xl" :class="iconClass"></span>
|
||||||
</button>
|
</button>
|
||||||
<div v-else class="h-full w-full flex items-center justify-center">
|
<div v-else class="h-full w-full flex items-center justify-center">
|
||||||
@@ -10,12 +10,12 @@
|
|||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<transition name="menu">
|
<transition name="menu">
|
||||||
<div v-show="showMenu" ref="menuWrapper" class="absolute right-0 mt-1 z-10 bg-bg border border-black-200 shadow-lg rounded-md py-1 focus:outline-none sm:text-sm" :style="{ width: menuWidth + 'px' }">
|
<div v-show="showMenu" ref="menuWrapper" role="menu" class="absolute right-0 mt-1 z-10 bg-bg border border-black-200 shadow-lg rounded-md py-1 focus:outline-none sm:text-sm" :style="{ width: menuWidth + 'px' }">
|
||||||
<template v-for="(item, index) in items">
|
<template v-for="(item, index) in items">
|
||||||
<template v-if="item.subitems">
|
<template v-if="item.subitems">
|
||||||
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-default" :class="{ 'bg-white/5': mouseoverItemIndex == index }" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop>
|
<button :key="index" role="menuitem" aria-haspopup="menu" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-default w-full" :class="{ 'bg-white/5': mouseoverItemIndex == index }" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop>
|
||||||
<p>{{ item.text }}</p>
|
<p>{{ item.text }}</p>
|
||||||
</div>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-if="mouseoverItemIndex === index"
|
v-if="mouseoverItemIndex === index"
|
||||||
:key="`subitems-${index}`"
|
:key="`subitems-${index}`"
|
||||||
@@ -25,14 +25,14 @@
|
|||||||
:class="openSubMenuLeft ? 'rounded-l-md' : 'rounded-r-md'"
|
:class="openSubMenuLeft ? 'rounded-l-md' : 'rounded-r-md'"
|
||||||
:style="{ left: submenuLeftPos + 'px', top: index * 28 + 'px', width: submenuWidth + 'px' }"
|
:style="{ left: submenuLeftPos + 'px', top: index * 28 + 'px', width: submenuWidth + 'px' }"
|
||||||
>
|
>
|
||||||
<div v-for="(subitem, subitemindex) in item.subitems" :key="`subitem-${subitemindex}`" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer" @click.stop="clickAction(subitem.action, subitem.data)">
|
<button v-for="(subitem, subitemindex) in item.subitems" :key="`subitem-${subitemindex}`" role="menuitem" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer w-full" @click.stop="clickAction(subitem.action, subitem.data)">
|
||||||
<p>{{ subitem.text }}</p>
|
<p>{{ subitem.text }}</p>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.action)">
|
<button v-else :key="index" role="menuitem" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer w-full" @click.stop="clickAction(item.action)">
|
||||||
<p class="text-left">{{ item.text }}</p>
|
<p class="text-left">{{ item.text }}</p>
|
||||||
</div>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative w-full" v-click-outside="clickOutsideObj">
|
<div class="relative w-full" v-click-outside="clickOutsideObj">
|
||||||
<p v-if="label" class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
<p v-if="label" class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
||||||
<button type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
<button type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left sm:text-sm" :class="buttonClass" aria-haspopup="menu" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small }">{{ selectedText }}</span>
|
<span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small }">{{ selectedText }}</span>
|
||||||
<span v-if="selectedSubtext">: </span>
|
<span v-if="selectedSubtext">: </span>
|
||||||
@@ -13,9 +13,9 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<transition name="menu">
|
<transition name="menu">
|
||||||
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="listbox" :style="{ maxHeight: menuMaxHeight }">
|
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="menu" :style="{ maxHeight: menuMaxHeight }">
|
||||||
<template v-for="item in itemsToShow">
|
<template v-for="item in itemsToShow">
|
||||||
<li :key="item.value" class="text-gray-100 relative py-2 cursor-pointer hover:bg-black-400" :id="'listbox-option-' + item.value" role="option" tabindex="0" @keyup.enter="clickedOption(item.value)" @click="clickedOption(item.value)">
|
<li :key="item.value" class="text-gray-100 relative py-2 cursor-pointer hover:bg-black-400" role="menuitem" tabindex="0" @keyup.enter="clickedOption(item.value)" @click="clickedOption(item.value)">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="ml-3 block truncate font-sans text-sm" :class="{ 'font-semibold': item.subtext }">{{ item.text }}</span>
|
<span class="ml-3 block truncate font-sans text-sm" :class="{ 'font-semibold': item.subtext }">{{ item.text }}</span>
|
||||||
<span v-if="item.subtext">: </span>
|
<span v-if="item.subtext">: </span>
|
||||||
@@ -119,4 +119,4 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<button class="icon-btn rounded-md flex items-center justify-center relative" @mousedown.prevent :disabled="disabled || loading" :class="className" @click="clickBtn">
|
<button :aria-label="ariaLabel" class="icon-btn rounded-md flex items-center justify-center relative" @mousedown.prevent :disabled="disabled || loading" :class="className" @click="clickBtn">
|
||||||
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
||||||
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||||
@@ -28,7 +28,8 @@ export default {
|
|||||||
size: {
|
size: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 9
|
default: 9
|
||||||
}
|
},
|
||||||
|
ariaLabel: String
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
class="w-10 sm:w-full relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200"
|
class="w-10 sm:w-full relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200"
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="menu"
|
||||||
:aria-expanded="showMenu"
|
:aria-expanded="showMenu"
|
||||||
:aria-label="$strings.ButtonLibrary + ': ' + currentLibrary.name"
|
:aria-label="$strings.ButtonLibrary + ': ' + currentLibrary.name"
|
||||||
@click.stop.prevent="clickShowMenu"
|
@click.stop.prevent="clickShowMenu"
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<transition name="menu">
|
<transition name="menu">
|
||||||
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full min-w-48 bg-primary border border-black-200 shadow-lg rounded-b-md py-1 overflow-auto focus:outline-none sm:text-sm librariesDropdownMenu" tabindex="-1" role="listbox">
|
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full min-w-48 bg-primary border border-black-200 shadow-lg rounded-b-md py-1 overflow-auto focus:outline-none sm:text-sm librariesDropdownMenu" tabindex="-1" role="menu">
|
||||||
<template v-for="library in librariesFiltered">
|
<template v-for="library in librariesFiltered">
|
||||||
<li :key="library.id" class="text-gray-400 hover:text-white relative py-2 cursor-pointer hover:bg-black-400" role="option" tabindex="0" @keydown.enter="selectLibrary(library)" @click="selectLibrary(library)">
|
<li :key="library.id" class="text-gray-400 hover:text-white relative py-2 cursor-pointer hover:bg-black-400" role="menuitem" tabindex="0" @keydown.enter="selectLibrary(library)" @click="selectLibrary(library)">
|
||||||
<div class="flex items-center px-2">
|
<div class="flex items-center px-2">
|
||||||
<ui-library-icon :icon="library.icon" class="mr-1.5" />
|
<ui-library-icon :icon="library.icon" class="mr-1.5" />
|
||||||
<span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span>
|
<span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p>
|
<label :for="identifier" class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</label>
|
||||||
<div ref="wrapper" class="relative">
|
<div ref="wrapper" class="relative">
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
<div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-1" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
<div ref="inputWrapper" role="list" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-1" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
||||||
<div v-for="item in selected" :key="item" class="rounded-full px-2 py-1 mx-0.5 my-0.5 text-xs bg-bg flex flex-nowrap break-all items-center relative">
|
<div v-for="item in selected" :key="item" role="listitem" class="rounded-full px-2 py-1 mx-0.5 my-0.5 text-xs bg-bg flex flex-nowrap break-all items-center relative">
|
||||||
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 px-1 bg-bg bg-opacity-75 flex items-center justify-end opacity-0 hover:opacity-100">
|
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 px-1 bg-bg bg-opacity-75 flex items-center justify-end opacity-0 hover:opacity-100" :class="{ 'opacity-100': inputFocused }">
|
||||||
<span v-if="showEdit" class="material-symbols text-white hover:text-warning cursor-pointer" style="font-size: 1.1rem" @click.stop="editItem(item)">edit</span>
|
<button v-if="showEdit" type="button" :aria-label="$strings.ButtonEdit" class="material-symbols text-white hover:text-warning cursor-pointer" style="font-size: 1.1rem" @click.stop="editItem(item)">edit</button>
|
||||||
<span class="material-symbols text-white hover:text-error cursor-pointer" style="font-size: 1.1rem" @click.stop="removeItem(item)">close</span>
|
<button type="button" :aria-label="$strings.ButtonRemove" class="material-symbols text-white hover:text-error focus:text-error cursor-pointer" style="font-size: 1.1rem" @click.stop="removeItem(item)" @keydown.enter.stop.prevent="removeItem(item)" @focus="setInputFocused(true)" @blur="setInputFocused(false)" tabindex="0">close</button>
|
||||||
</div>
|
</div>
|
||||||
{{ item }}
|
{{ item }}
|
||||||
</div>
|
</div>
|
||||||
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
|
<input v-show="!readonly" v-model="textInput" ref="input" :id="identifier" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -66,7 +66,8 @@ export default {
|
|||||||
typingTimeout: null,
|
typingTimeout: null,
|
||||||
isFocused: false,
|
isFocused: false,
|
||||||
menu: null,
|
menu: null,
|
||||||
filteredItems: null
|
filteredItems: null,
|
||||||
|
inputFocused: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -100,6 +101,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.filteredItems
|
return this.filteredItems
|
||||||
|
},
|
||||||
|
identifier() {
|
||||||
|
return Math.random().toString(36).substring(2)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -129,6 +133,9 @@ export default {
|
|||||||
}, 100)
|
}, 100)
|
||||||
this.setInputWidth()
|
this.setInputWidth()
|
||||||
},
|
},
|
||||||
|
setInputFocused(focused) {
|
||||||
|
this.inputFocused = focused
|
||||||
|
},
|
||||||
setInputWidth() {
|
setInputWidth() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
var value = this.$refs.input.value
|
var value = this.$refs.input.value
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p>
|
<label :for="identifier" class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</label>
|
||||||
<div ref="wrapper" class="relative">
|
<div ref="wrapper" class="relative">
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
<div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-0.5" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
<div ref="inputWrapper" role="list" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-0.5" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
||||||
<div v-for="item in selected" :key="item.id" class="rounded-full px-2 py-0.5 m-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center justify-center relative min-w-12">
|
<div v-for="item in selected" :key="item.id" role="listitem" class="rounded-full px-2 py-0.5 m-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center justify-center relative min-w-12">
|
||||||
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer">
|
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer" :class="{ 'opacity-100': inputFocused }">
|
||||||
<span v-if="showEdit" class="material-symbols text-base text-white hover:text-warning mr-1" @click.stop="editItem(item)">edit</span>
|
<button v-if="showEdit" type="button" :aria-label="$strings.ButtonEdit" class="material-symbols text-base text-white hover:text-warning focus:text-warning mr-1" @click.stop="editItem(item)" @keydown.enter.stop.prevent="editItem(item)" @focus="setInputFocused(true)" @blur="setInputFocused(false)" tabindex="0">edit</button>
|
||||||
<span class="material-symbols text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item.id)">close</span>
|
<button type="button" :aria-label="$strings.ButtonRemove" class="material-symbols text-white hover:text-error focus:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item.id)" @keydown.enter.stop="removeItem(item.id)" @focus="setInputFocused(true)" @blur="setInputFocused(false)" tabindex="0">close</button>
|
||||||
</div>
|
</div>
|
||||||
{{ item[textKey] }}
|
{{ item[textKey] }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showEdit && !disabled" class="rounded-full cursor-pointer w-6 h-6 mx-0.5 bg-bg flex items-center justify-center">
|
<div v-if="showEdit && !disabled" class="rounded-full cursor-pointer w-6 h-6 mx-0.5 bg-bg flex items-center justify-center">
|
||||||
<span class="material-symbols text-white hover:text-success pt-px pr-px" style="font-size: 1.1rem" @click.stop="addItem">add</span>
|
<button type="button" :aria-label="$strings.ButtonAdd" class="material-symbols text-white hover:text-success focus:text-success pt-px pr-px" style="font-size: 1.1rem" @click.stop="addItem" @keydown.enter.stop="addItem" tabindex="0">add</button>
|
||||||
</div>
|
</div>
|
||||||
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
|
<input v-show="!readonly" v-model="textInput" ref="input" :id="identifier" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -65,6 +65,7 @@ export default {
|
|||||||
currentSearch: null,
|
currentSearch: null,
|
||||||
typingTimeout: null,
|
typingTimeout: null,
|
||||||
isFocused: false,
|
isFocused: false,
|
||||||
|
inputFocused: false,
|
||||||
menu: null,
|
menu: null,
|
||||||
items: []
|
items: []
|
||||||
}
|
}
|
||||||
@@ -102,6 +103,9 @@ export default {
|
|||||||
},
|
},
|
||||||
filterData() {
|
filterData() {
|
||||||
return this.$store.state.libraries.filterData || {}
|
return this.$store.state.libraries.filterData || {}
|
||||||
|
},
|
||||||
|
identifier() {
|
||||||
|
return Math.random().toString(36).substring(2)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -114,6 +118,9 @@ export default {
|
|||||||
getIsSelected(itemValue) {
|
getIsSelected(itemValue) {
|
||||||
return !!this.selected.find((i) => i.id === itemValue)
|
return !!this.selected.find((i) => i.id === itemValue)
|
||||||
},
|
},
|
||||||
|
setInputFocused(focused) {
|
||||||
|
this.inputFocused = focused
|
||||||
|
},
|
||||||
search() {
|
search() {
|
||||||
if (!this.textInput) return
|
if (!this.textInput) return
|
||||||
this.currentSearch = this.textInput
|
this.currentSearch = this.textInput
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<button class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" :class="borderless ? '' : 'bg-primary border border-gray-600'" @click="clickBtn">
|
<button :aria-label="isRead ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" :class="borderless ? '' : 'bg-primary border border-gray-600'" @click="clickBtn">
|
||||||
<div class="w-5 h-5 text-white relative">
|
<div class="w-5 h-5 text-white relative">
|
||||||
<svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
|
<svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
|
||||||
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
|
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
|
||||||
|
|||||||
@@ -1,24 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="relative">
|
<div ref="wrapper" class="relative">
|
||||||
<input
|
<input :id="inputId" :name="inputName" ref="input" v-model="inputValue" :type="actualType" :step="step" :min="min" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" dir="auto" class="rounded bg-primary text-gray-200 focus:bg-bg focus:outline-none border h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
|
||||||
:id="inputId"
|
|
||||||
:name="inputName"
|
|
||||||
ref="input"
|
|
||||||
v-model="inputValue"
|
|
||||||
:type="actualType"
|
|
||||||
:step="step"
|
|
||||||
:min="min"
|
|
||||||
:readonly="readonly"
|
|
||||||
:disabled="disabled"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
dir="auto"
|
|
||||||
class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full"
|
|
||||||
:class="classList"
|
|
||||||
@keyup="keyup"
|
|
||||||
@change="change"
|
|
||||||
@focus="focused"
|
|
||||||
@blur="blurred"
|
|
||||||
/>
|
|
||||||
<div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
|
<div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
|
||||||
<span class="material-symbols text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
|
<span class="material-symbols text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,14 +39,16 @@ export default {
|
|||||||
inputName: String,
|
inputName: String,
|
||||||
showCopy: Boolean,
|
showCopy: Boolean,
|
||||||
step: [String, Number],
|
step: [String, Number],
|
||||||
min: [String, Number]
|
min: [String, Number],
|
||||||
|
customInputClass: String
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showPassword: false,
|
showPassword: false,
|
||||||
isHovering: false,
|
isHovering: false,
|
||||||
isFocused: false,
|
isFocused: false,
|
||||||
hasCopied: false
|
hasCopied: false,
|
||||||
|
isInvalidDate: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -82,6 +66,11 @@ export default {
|
|||||||
_list.push(`py-${this.paddingY}`)
|
_list.push(`py-${this.paddingY}`)
|
||||||
if (this.noSpinner) _list.push('no-spinner')
|
if (this.noSpinner) _list.push('no-spinner')
|
||||||
if (this.textCenter) _list.push('text-center')
|
if (this.textCenter) _list.push('text-center')
|
||||||
|
if (this.customInputClass) _list.push(this.customInputClass)
|
||||||
|
|
||||||
|
if (this.isInvalidDate) _list.push('border-error')
|
||||||
|
else _list.push('focus:border-gray-300 border-gray-600')
|
||||||
|
|
||||||
return _list.join(' ')
|
return _list.join(' ')
|
||||||
},
|
},
|
||||||
actualType() {
|
actualType() {
|
||||||
@@ -116,6 +105,14 @@ export default {
|
|||||||
},
|
},
|
||||||
keyup(e) {
|
keyup(e) {
|
||||||
this.$emit('keyup', e)
|
this.$emit('keyup', e)
|
||||||
|
|
||||||
|
if (this.type === 'datetime-local') {
|
||||||
|
if (e.target.validity?.badInput) {
|
||||||
|
this.isInvalidDate = true
|
||||||
|
} else {
|
||||||
|
this.isInvalidDate = false
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
blur() {
|
blur() {
|
||||||
if (this.$refs.input) this.$refs.input.blur()
|
if (this.$refs.input) this.$refs.input.blur()
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<slot>
|
<slot>
|
||||||
<label :for="identifier" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }"
|
<label :for="identifier" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
|
||||||
>{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em></label
|
{{ label }}
|
||||||
>
|
<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
|
||||||
|
</label>
|
||||||
</slot>
|
</slot>
|
||||||
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" :class="inputClass" @blur="inputBlurred" />
|
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" :class="inputClass" @blur="inputBlurred" />
|
||||||
</div>
|
</div>
|
||||||
@@ -57,4 +58,4 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<button :aria-labelledby="labeledBy" role="checkbox" type="button" class="border rounded-full border-black-100 flex items-center cursor-pointer w-10 justify-start" :aria-checked="toggleValue" :class="className" @click="clickToggle">
|
<button :aria-labelledby="labeledBy" :aria-label="label" role="checkbox" type="button" class="border rounded-full border-black-100 flex items-center cursor-pointer justify-start" :style="{ width: buttonWidth + 'px' }" :aria-checked="toggleValue" :class="className" @click="clickToggle">
|
||||||
<span class="rounded-full border w-5 h-5 border-black-50 shadow transform transition-transform duration-100" :class="switchClassName"></span>
|
<span class="rounded-full border border-black-50 shadow transform transition-transform duration-100" :style="{ width: cursorHeightWidth + 'px', height: cursorHeightWidth + 'px' }" :class="switchClassName"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -19,7 +19,12 @@ export default {
|
|||||||
default: 'primary'
|
default: 'primary'
|
||||||
},
|
},
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
labeledBy: String
|
labeledBy: String,
|
||||||
|
label: String,
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'md'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
toggleValue: {
|
toggleValue: {
|
||||||
@@ -37,6 +42,13 @@ export default {
|
|||||||
switchClassName() {
|
switchClassName() {
|
||||||
var bgColor = this.disabled ? 'bg-gray-300' : 'bg-white'
|
var bgColor = this.disabled ? 'bg-gray-300' : 'bg-white'
|
||||||
return this.toggleValue ? 'translate-x-5 ' + bgColor : bgColor
|
return this.toggleValue ? 'translate-x-5 ' + bgColor : bgColor
|
||||||
|
},
|
||||||
|
cursorHeightWidth() {
|
||||||
|
if (this.size === 'sm') return 16
|
||||||
|
return 20
|
||||||
|
},
|
||||||
|
buttonWidth() {
|
||||||
|
return this.cursorHeightWidth * 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -249,11 +249,33 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return target
|
return target
|
||||||
|
},
|
||||||
|
enableBreakParagraphOnReturn() {
|
||||||
|
// Trix works with divs by default, we want paragraphs instead
|
||||||
|
Trix.config.blockAttributes.default.tagName = 'p'
|
||||||
|
// Enable break paragraph on Enter (Shift + Enter will still create a line break)
|
||||||
|
Trix.config.blockAttributes.default.breakOnReturn = true
|
||||||
|
|
||||||
|
// Hack to fix buggy paragraph breaks
|
||||||
|
// Copied from https://github.com/basecamp/trix/issues/680#issuecomment-735742942
|
||||||
|
Trix.Block.prototype.breaksOnReturn = function () {
|
||||||
|
const attr = this.getLastAttribute()
|
||||||
|
const config = Trix.getBlockConfig(attr ? attr : 'default')
|
||||||
|
return config ? config.breakOnReturn : false
|
||||||
|
}
|
||||||
|
Trix.LineBreakInsertion.prototype.shouldInsertBlockBreak = function () {
|
||||||
|
if (this.block.hasAttributes() && this.block.isListItem() && !this.block.isEmpty()) {
|
||||||
|
return this.startLocation.offset > 0
|
||||||
|
} else {
|
||||||
|
return !this.shouldBreakFormattedBlock() ? this.breaksOnReturn : false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
/** Override editor configuration */
|
/** Override editor configuration */
|
||||||
this.overrideConfig(this.config)
|
this.overrideConfig(this.config)
|
||||||
|
this.enableBreakParagraphOnReturn()
|
||||||
/** Check if editor read-only mode is required */
|
/** Check if editor read-only mode is required */
|
||||||
this.decorateDisabledEditor(this.disabledEditor)
|
this.decorateDisabledEditor(this.disabledEditor)
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -283,4 +305,4 @@ export default {
|
|||||||
.trix_container .trix-content {
|
.trix_container .trix-content {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<ui-tooltip v-if="alreadyInLibrary" :text="$strings.LabelAlreadyInYourLibrary" direction="top">
|
<ui-tooltip :text="$strings.LabelAlreadyInYourLibrary" direction="top" class="inline-flex">
|
||||||
<span class="material-symbols ml-1 text-success" style="font-size: 0.8rem">check_circle</span>
|
<span class="material-symbols ml-1 text-sm text-success">check_circle</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
alreadyInLibrary: Boolean
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent>
|
<div aria-hidden="true" class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent>
|
||||||
<span class="material-symbols" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize" aria-label="Decrease Cover Size" role="button"></span>
|
<span class="material-symbols" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize" aria-label="Decrease Cover Size" role="button"></span>
|
||||||
<p class="px-2 font-mono" style="font-size: 1rem">{{ bookCoverWidth }}</p>
|
<p class="px-2 font-mono" style="font-size: 1rem">{{ bookCoverWidth }}</p>
|
||||||
<span class="material-symbols" :class="selectedSizeIndex === availableSizes.length - 1 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="increaseSize" aria-label="Increase Cover Size" role="button"></span>
|
<span class="material-symbols" :class="selectedSizeIndex === availableSizes.length - 1 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="increaseSize" aria-label="Increase Cover Size" role="button"></span>
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
<div class="flex items-center py-3e">
|
<div class="flex items-center py-3e">
|
||||||
<slot />
|
<slot />
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<button cy-id="leftScrollButton" v-if="isScrollable" class="w-8e h-8e mx-1e flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
|
<button cy-id="leftScrollButton" v-if="isScrollable" :aria-label="$strings.ButtonScrollLeft" class="w-8e h-8e mx-1e flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
|
||||||
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">chevron_left</span>
|
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">chevron_left</span>
|
||||||
</button>
|
</button>
|
||||||
<button cy-id="rightScrollButton" v-if="isScrollable" class="w-8e h-8e mx-1e flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
|
<button cy-id="rightScrollButton" v-if="isScrollable" :aria-label="$strings.ButtonScrollRight" class="w-8e h-8e mx-1e flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
|
||||||
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">chevron_right</span>
|
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">chevron_right</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,7 +65,7 @@ export default {
|
|||||||
},
|
},
|
||||||
authors: {
|
authors: {
|
||||||
component: 'cards-author-card',
|
component: 'cards-author-card',
|
||||||
itemPropName: 'author',
|
itemPropName: 'author-mount',
|
||||||
itemIdFunc: (item) => item.id
|
itemIdFunc: (item) => item.id
|
||||||
},
|
},
|
||||||
narrators: {
|
narrators: {
|
||||||
|
|||||||
@@ -101,7 +101,12 @@ export default {
|
|||||||
return this.$store.state.libraries.filterData || {}
|
return this.$store.state.libraries.filterData || {}
|
||||||
},
|
},
|
||||||
podcastTypes() {
|
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: {
|
methods: {
|
||||||
|
|||||||
@@ -71,8 +71,6 @@ export default {
|
|||||||
this.showSeriesForm = true
|
this.showSeriesForm = true
|
||||||
},
|
},
|
||||||
submitSeriesForm() {
|
submitSeriesForm() {
|
||||||
console.log('submit series form', this.value, this.selectedSeries)
|
|
||||||
|
|
||||||
if (!this.selectedSeries.name) {
|
if (!this.selectedSeries.name) {
|
||||||
this.$toast.error('Must enter a series')
|
this.$toast.error('Must enter a series')
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import Tooltip from '@/components/ui/Tooltip.vue'
|
|||||||
import LoadingSpinner from '@/components/widgets/LoadingSpinner.vue'
|
import LoadingSpinner from '@/components/widgets/LoadingSpinner.vue'
|
||||||
|
|
||||||
describe('AuthorCard', () => {
|
describe('AuthorCard', () => {
|
||||||
const author = {
|
const authorMount = {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'John Doe',
|
name: 'John Doe',
|
||||||
numBooks: 5
|
numBooks: 5
|
||||||
}
|
}
|
||||||
|
|
||||||
const propsData = {
|
const propsData = {
|
||||||
author,
|
authorMount,
|
||||||
nameBelow: false
|
nameBelow: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import '@/plugins/utils'
|
||||||
|
|
||||||
|
// This is the actual function that is being tested
|
||||||
|
const elapsedPrettyExtended = Vue.prototype.$elapsedPrettyExtended
|
||||||
|
|
||||||
|
// Helper function to convert days, hours, minutes, seconds to total seconds
|
||||||
|
function DHMStoSeconds(days, hours, minutes, seconds) {
|
||||||
|
return seconds + minutes * 60 + hours * 3600 + days * 86400
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('$elapsedPrettyExtended', () => {
|
||||||
|
describe('function is on the Vue Prototype', () => {
|
||||||
|
it('exists as a function on Vue.prototype', () => {
|
||||||
|
expect(Vue.prototype.$elapsedPrettyExtended).to.exist
|
||||||
|
expect(Vue.prototype.$elapsedPrettyExtended).to.be.a('function')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('param default values', () => {
|
||||||
|
const testSeconds = DHMStoSeconds(0, 25, 1, 5) // 25h 1m 5s = 90065 seconds
|
||||||
|
|
||||||
|
it('uses useDays=true showSeconds=true by default', () => {
|
||||||
|
expect(elapsedPrettyExtended(testSeconds)).to.equal('1d 1h 1m 5s')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('only useDays=false overrides useDays but keeps showSeconds=true', () => {
|
||||||
|
expect(elapsedPrettyExtended(testSeconds, false)).to.equal('25h 1m 5s')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('explicit useDays=false showSeconds=false overrides both', () => {
|
||||||
|
expect(elapsedPrettyExtended(testSeconds, false, false)).to.equal('25h 1m')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useDays=false showSeconds=true', () => {
|
||||||
|
const useDaysFalse = false
|
||||||
|
const showSecondsTrue = true
|
||||||
|
const testCases = [
|
||||||
|
[[0, 0, 0, 0], '', '0s -> ""'],
|
||||||
|
[[0, 1, 0, 1], '1h 1s', '1h 1s -> 1h 1s'],
|
||||||
|
[[0, 25, 0, 1], '25h 1s', '25h 1s -> 25h 1s']
|
||||||
|
]
|
||||||
|
|
||||||
|
testCases.forEach(([dhms, expected, description]) => {
|
||||||
|
it(description, () => {
|
||||||
|
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysFalse, showSecondsTrue)).to.equal(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useDays=true showSeconds=true', () => {
|
||||||
|
const useDaysTrue = true
|
||||||
|
const showSecondsTrue = true
|
||||||
|
const testCases = [
|
||||||
|
[[0, 0, 0, 0], '', '0s -> ""'],
|
||||||
|
[[0, 1, 0, 1], '1h 1s', '1h 1s -> 1h 1s'],
|
||||||
|
[[0, 25, 0, 1], '1d 1h 1s', '25h 1s -> 1d 1h 1s']
|
||||||
|
]
|
||||||
|
|
||||||
|
testCases.forEach(([dhms, expected, description]) => {
|
||||||
|
it(description, () => {
|
||||||
|
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysTrue, showSecondsTrue)).to.equal(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useDays=true showSeconds=false', () => {
|
||||||
|
const useDaysTrue = true
|
||||||
|
const showSecondsFalse = false
|
||||||
|
const testCases = [
|
||||||
|
[[0, 0, 0, 0], '', '0s -> ""'],
|
||||||
|
[[0, 1, 0, 0], '1h', '1h -> 1h'],
|
||||||
|
[[0, 1, 0, 1], '1h', '1h 1s -> 1h'],
|
||||||
|
[[0, 1, 1, 0], '1h 1m', '1h 1m -> 1h 1m'],
|
||||||
|
[[0, 25, 0, 0], '1d 1h', '25h -> 1d 1h'],
|
||||||
|
[[0, 25, 0, 1], '1d 1h', '25h 1s -> 1d 1h'],
|
||||||
|
[[2, 0, 0, 0], '2d', '2d -> 2d']
|
||||||
|
]
|
||||||
|
|
||||||
|
testCases.forEach(([dhms, expected, description]) => {
|
||||||
|
it(description, () => {
|
||||||
|
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysTrue, showSecondsFalse)).to.equal(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('rounding useDays=true showSeconds=true', () => {
|
||||||
|
const useDaysTrue = true
|
||||||
|
const showSecondsTrue = true
|
||||||
|
const testCases = [
|
||||||
|
// Seconds rounding
|
||||||
|
[[0, 0, 0, 1], '1s', '1s -> 1s'],
|
||||||
|
[[0, 0, 0, 29.9], '30s', '29.9s -> 30s'],
|
||||||
|
[[0, 0, 0, 30], '30s', '30s -> 30s'],
|
||||||
|
[[0, 0, 0, 30.1], '30s', '30.1s -> 30s'],
|
||||||
|
[[0, 0, 0, 59.4], '59s', '59.4s -> 59s'],
|
||||||
|
[[0, 0, 0, 59.5], '1m', '59.5s -> 1m'],
|
||||||
|
|
||||||
|
// Minutes rounding
|
||||||
|
[[0, 0, 59, 29], '59m 29s', '59m 29s -> 59m 29s'],
|
||||||
|
[[0, 0, 59, 30], '59m 30s', '59m 30s -> 59m 30s'],
|
||||||
|
[[0, 0, 59, 59.5], '1h', '59m 59.5s -> 1h'],
|
||||||
|
|
||||||
|
// Hours rounding
|
||||||
|
[[0, 23, 59, 29], '23h 59m 29s', '23h 59m 29s -> 23h 59m 29s'],
|
||||||
|
[[0, 23, 59, 30], '23h 59m 30s', '23h 59m 30s -> 23h 59m 30s'],
|
||||||
|
[[0, 23, 59, 59.5], '1d', '23h 59m 59.5s -> 1d'],
|
||||||
|
|
||||||
|
// The actual bug case
|
||||||
|
[[44, 23, 59, 30], '44d 23h 59m 30s', '44d 23h 59m 30s -> 44d 23h 59m 30s']
|
||||||
|
]
|
||||||
|
|
||||||
|
testCases.forEach(([dhms, expected, description]) => {
|
||||||
|
it(description, () => {
|
||||||
|
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysTrue, showSecondsTrue)).to.equal(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('rounding useDays=true showSeconds=false', () => {
|
||||||
|
const useDaysTrue = true
|
||||||
|
const showSecondsFalse = false
|
||||||
|
const testCases = [
|
||||||
|
// Seconds rounding - these cases changed behavior from original
|
||||||
|
[[0, 0, 0, 1], '', '1s -> ""'],
|
||||||
|
[[0, 0, 0, 29.9], '', '29.9s -> ""'],
|
||||||
|
[[0, 0, 0, 30], '', '30s -> ""'],
|
||||||
|
[[0, 0, 0, 30.1], '', '30.1s -> ""'],
|
||||||
|
[[0, 0, 0, 59.4], '', '59.4s -> ""'],
|
||||||
|
[[0, 0, 0, 59.5], '1m', '59.5s -> 1m'],
|
||||||
|
// This is unexpected behavior, but it's consistent with the original behavior
|
||||||
|
// We preserved the test case, to document the current behavior
|
||||||
|
// - with showSeconds=false,
|
||||||
|
// one might expect: 1m 29.5s --round(1.4901m)-> 1m
|
||||||
|
// actual implementation: 1h 29.5s --roundSeconds-> 1h 30s --roundMinutes-> 2m
|
||||||
|
// So because of the separate rounding of seconds, and then minutes, it returns 2m
|
||||||
|
[[0, 0, 1, 29.5], '2m', '1m 29.5s -> 2m'],
|
||||||
|
|
||||||
|
// Minutes carry - actual bug fixes below
|
||||||
|
[[0, 0, 59, 29], '59m', '59m 29s -> 59m'],
|
||||||
|
[[0, 0, 59, 30], '1h', '59m 30s -> 1h'], // This was an actual bug, used to return 60m
|
||||||
|
[[0, 0, 59, 59.5], '1h', '59m 59.5s -> 1h'],
|
||||||
|
|
||||||
|
// Hours carry
|
||||||
|
[[0, 23, 59, 29], '23h 59m', '23h 59m 29s -> 23h 59m'],
|
||||||
|
[[0, 23, 59, 30], '1d', '23h 59m 30s -> 1d'], // This was an actual bug, used to return 23h 60m
|
||||||
|
[[0, 23, 59, 59.5], '1d', '23h 59m 59.5s -> 1d'],
|
||||||
|
|
||||||
|
// The actual bug case
|
||||||
|
[[44, 23, 59, 30], '45d', '44d 23h 59m 30s -> 45d'] // This was an actual bug, used to return 44d 23h 60m
|
||||||
|
]
|
||||||
|
|
||||||
|
testCases.forEach(([dhms, expected, description]) => {
|
||||||
|
it(description, () => {
|
||||||
|
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysTrue, showSecondsFalse)).to.equal(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('empty values', () => {
|
||||||
|
const paramCombos = [
|
||||||
|
// useDays, showSeconds, description
|
||||||
|
[true, true, 'with days and seconds'],
|
||||||
|
[true, false, 'with days, no seconds'],
|
||||||
|
[false, true, 'no days, with seconds'],
|
||||||
|
[false, false, 'no days, no seconds']
|
||||||
|
]
|
||||||
|
|
||||||
|
const emptyInputs = [
|
||||||
|
// input, description
|
||||||
|
[null, 'null input'],
|
||||||
|
[undefined, 'undefined input'],
|
||||||
|
[0, 'zero'],
|
||||||
|
[0.49, 'rounds to zero'] // Just under rounding threshold
|
||||||
|
]
|
||||||
|
|
||||||
|
paramCombos.forEach(([useDays, showSeconds, paramDesc]) => {
|
||||||
|
describe(paramDesc, () => {
|
||||||
|
emptyInputs.forEach(([input, desc]) => {
|
||||||
|
it(desc, () => {
|
||||||
|
expect(elapsedPrettyExtended(input, useDays, showSeconds)).to.equal('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -357,7 +357,8 @@ export default {
|
|||||||
teardown: false,
|
teardown: false,
|
||||||
transports: ['websocket'],
|
transports: ['websocket'],
|
||||||
upgrade: false,
|
upgrade: false,
|
||||||
reconnection: true
|
reconnection: true,
|
||||||
|
path: `${this.$config.routerBasePath}/socket.io`
|
||||||
})
|
})
|
||||||
this.$root.socket = this.socket
|
this.$root.socket = this.socket
|
||||||
console.log('Socket initialized')
|
console.log('Socket initialized')
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import LazySeriesCard from '@/components/cards/LazySeriesCard'
|
|||||||
import LazyCollectionCard from '@/components/cards/LazyCollectionCard'
|
import LazyCollectionCard from '@/components/cards/LazyCollectionCard'
|
||||||
import LazyPlaylistCard from '@/components/cards/LazyPlaylistCard'
|
import LazyPlaylistCard from '@/components/cards/LazyPlaylistCard'
|
||||||
import LazyAlbumCard from '@/components/cards/LazyAlbumCard'
|
import LazyAlbumCard from '@/components/cards/LazyAlbumCard'
|
||||||
|
import AuthorCard from '@/components/cards/AuthorCard'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
@@ -20,6 +21,7 @@ export default {
|
|||||||
if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard)
|
if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard)
|
||||||
if (this.entityName === 'playlists') return Vue.extend(LazyPlaylistCard)
|
if (this.entityName === 'playlists') return Vue.extend(LazyPlaylistCard)
|
||||||
if (this.entityName === 'albums') return Vue.extend(LazyAlbumCard)
|
if (this.entityName === 'albums') return Vue.extend(LazyAlbumCard)
|
||||||
|
if (this.entityName === 'authors') return Vue.extend(AuthorCard)
|
||||||
return Vue.extend(LazyBookCard)
|
return Vue.extend(LazyBookCard)
|
||||||
},
|
},
|
||||||
getComponentName() {
|
getComponentName() {
|
||||||
@@ -27,6 +29,7 @@ export default {
|
|||||||
if (this.entityName === 'collections') return 'cards-lazy-collection-card'
|
if (this.entityName === 'collections') return 'cards-lazy-collection-card'
|
||||||
if (this.entityName === 'playlists') return 'cards-lazy-playlist-card'
|
if (this.entityName === 'playlists') return 'cards-lazy-playlist-card'
|
||||||
if (this.entityName === 'albums') return 'cards-lazy-album-card'
|
if (this.entityName === 'albums') return 'cards-lazy-album-card'
|
||||||
|
if (this.entityName === 'authors') return 'cards-author-card'
|
||||||
return 'cards-lazy-book-card'
|
return 'cards-lazy-book-card'
|
||||||
},
|
},
|
||||||
async setCardSize() {
|
async setCardSize() {
|
||||||
@@ -46,16 +49,18 @@ export default {
|
|||||||
props.orderBy = this.seriesSortBy
|
props.orderBy = this.seriesSortBy
|
||||||
}
|
}
|
||||||
const instance = new ComponentClass({
|
const instance = new ComponentClass({
|
||||||
propsData: props
|
propsData: props,
|
||||||
|
parent: this
|
||||||
})
|
})
|
||||||
instance.$mount()
|
instance.$mount()
|
||||||
this.resizeObserver = new ResizeObserver((entries) => {
|
this.resizeObserver = new ResizeObserver((entries) => {
|
||||||
for (let entry of entries) {
|
for (let entry of entries) {
|
||||||
this.cardWidth = entry.contentRect.width
|
this.cardWidth = entry.borderBoxSize[0].inlineSize
|
||||||
this.cardHeight = entry.contentRect.height
|
this.cardHeight = entry.borderBoxSize[0].blockSize
|
||||||
this.resizeObserver.disconnect()
|
|
||||||
this.$refs.bookshelf.removeChild(instance.$el)
|
|
||||||
}
|
}
|
||||||
|
this.coverHeight = instance.coverHeight
|
||||||
|
this.resizeObserver.disconnect()
|
||||||
|
this.$refs.bookshelf.removeChild(instance.$el)
|
||||||
})
|
})
|
||||||
instance.$el.style.visibility = 'hidden'
|
instance.$el.style.visibility = 'hidden'
|
||||||
instance.$el.style.position = 'absolute'
|
instance.$el.style.position = 'absolute'
|
||||||
@@ -72,7 +77,7 @@ export default {
|
|||||||
})
|
})
|
||||||
const timeAfter = performance.now()
|
const timeAfter = performance.now()
|
||||||
},
|
},
|
||||||
async mountEntityCard(index) {
|
mountEntityCard(index) {
|
||||||
var shelf = Math.floor(index / this.entitiesPerShelf)
|
var shelf = Math.floor(index / this.entitiesPerShelf)
|
||||||
var shelfEl = document.getElementById(`shelf-${shelf}`)
|
var shelfEl = document.getElementById(`shelf-${shelf}`)
|
||||||
if (!shelfEl) {
|
if (!shelfEl) {
|
||||||
@@ -114,6 +119,7 @@ export default {
|
|||||||
const _this = this
|
const _this = this
|
||||||
const instance = new ComponentClass({
|
const instance = new ComponentClass({
|
||||||
propsData: props,
|
propsData: props,
|
||||||
|
parent: this,
|
||||||
created() {
|
created() {
|
||||||
this.$on('edit', (entity) => {
|
this.$on('edit', (entity) => {
|
||||||
if (_this.editEntity) _this.editEntity(entity)
|
if (_this.editEntity) _this.editEntity(entity)
|
||||||
@@ -126,10 +132,7 @@ export default {
|
|||||||
this.entityComponentRefs[index] = instance
|
this.entityComponentRefs[index] = instance
|
||||||
|
|
||||||
instance.$mount()
|
instance.$mount()
|
||||||
const shelfOffsetY = this.shelfPaddingHeight * this.sizeMultiplier
|
instance.$el.style.transform = this.entityTransform((index % this.entitiesPerShelf) + 1)
|
||||||
const row = index % this.entitiesPerShelf
|
|
||||||
const shelfOffsetX = row * this.totalEntityCardWidth + this.bookshelfMarginLeft
|
|
||||||
instance.$el.style.transform = `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)`
|
|
||||||
instance.$el.classList.add('absolute', 'top-0', 'left-0')
|
instance.$el.classList.add('absolute', 'top-0', 'left-0')
|
||||||
shelfEl.appendChild(instance.$el)
|
shelfEl.appendChild(instance.$el)
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,8 @@ export default {
|
|||||||
var validOtherFiles = []
|
var validOtherFiles = []
|
||||||
var ignoredFiles = []
|
var ignoredFiles = []
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
// var filetype = this.checkFileType(file.name)
|
|
||||||
if (!file.filetype) ignoredFiles.push(file)
|
if (!file.filetype) ignoredFiles.push(file)
|
||||||
else {
|
else {
|
||||||
// file.filetype = filetype
|
|
||||||
if (file.filetype === 'audio' || (file.filetype === 'ebook' && mediaType === 'book')) validItemFiles.push(file)
|
if (file.filetype === 'audio' || (file.filetype === 'ebook' && mediaType === 'book')) validItemFiles.push(file)
|
||||||
else validOtherFiles.push(file)
|
else validOtherFiles.push(file)
|
||||||
}
|
}
|
||||||
@@ -165,7 +163,7 @@ export default {
|
|||||||
|
|
||||||
var firstBookPath = Path.dirname(firstBookFile.filepath)
|
var firstBookPath = Path.dirname(firstBookFile.filepath)
|
||||||
|
|
||||||
var dirs = firstBookPath.split('/').filter(d => !!d && d !== '.')
|
var dirs = firstBookPath.split('/').filter((d) => !!d && d !== '.')
|
||||||
if (dirs.length) {
|
if (dirs.length) {
|
||||||
audiobook.title = dirs.pop()
|
audiobook.title = dirs.pop()
|
||||||
if (dirs.length > 1) {
|
if (dirs.length > 1) {
|
||||||
@@ -189,7 +187,7 @@ export default {
|
|||||||
var firstAudioFile = podcast.itemFiles[0]
|
var firstAudioFile = podcast.itemFiles[0]
|
||||||
if (!firstAudioFile.filepath) return podcast // No path
|
if (!firstAudioFile.filepath) return podcast // No path
|
||||||
var firstPath = Path.dirname(firstAudioFile.filepath)
|
var firstPath = Path.dirname(firstAudioFile.filepath)
|
||||||
var dirs = firstPath.split('/').filter(d => !!d && d !== '.')
|
var dirs = firstPath.split('/').filter((d) => !!d && d !== '.')
|
||||||
if (dirs.length) {
|
if (dirs.length) {
|
||||||
podcast.title = dirs.length > 1 ? dirs[1] : dirs[0]
|
podcast.title = dirs.length > 1 ? dirs[1] : dirs[0]
|
||||||
} else {
|
} else {
|
||||||
@@ -212,13 +210,15 @@ export default {
|
|||||||
}
|
}
|
||||||
var ignoredFiles = itemData.ignoredFiles
|
var ignoredFiles = itemData.ignoredFiles
|
||||||
var index = 1
|
var index = 1
|
||||||
var items = itemData.items.filter((ab) => {
|
var items = itemData.items
|
||||||
if (!ab.itemFiles.length) {
|
.filter((ab) => {
|
||||||
if (ab.otherFiles.length) ignoredFiles = ignoredFiles.concat(ab.otherFiles)
|
if (!ab.itemFiles.length) {
|
||||||
if (ab.ignoredFiles.length) ignoredFiles = ignoredFiles.concat(ab.ignoredFiles)
|
if (ab.otherFiles.length) ignoredFiles = ignoredFiles.concat(ab.otherFiles)
|
||||||
}
|
if (ab.ignoredFiles.length) ignoredFiles = ignoredFiles.concat(ab.ignoredFiles)
|
||||||
return ab.itemFiles.length
|
}
|
||||||
}).map(ab => this.cleanItem(ab, mediaType, index++))
|
return ab.itemFiles.length
|
||||||
|
})
|
||||||
|
.map((ab) => this.cleanItem(ab, mediaType, index++))
|
||||||
return {
|
return {
|
||||||
items,
|
items,
|
||||||
ignoredFiles
|
ignoredFiles
|
||||||
@@ -259,7 +259,7 @@ export default {
|
|||||||
|
|
||||||
otherFiles.forEach((file) => {
|
otherFiles.forEach((file) => {
|
||||||
var dir = Path.dirname(file.filepath)
|
var dir = Path.dirname(file.filepath)
|
||||||
var findItem = Object.values(itemMap).find(b => dir.startsWith(b.path))
|
var findItem = Object.values(itemMap).find((b) => dir.startsWith(b.path))
|
||||||
if (findItem) {
|
if (findItem) {
|
||||||
findItem.otherFiles.push(file)
|
findItem.otherFiles.push(file)
|
||||||
} else {
|
} else {
|
||||||
@@ -270,18 +270,18 @@ export default {
|
|||||||
var items = []
|
var items = []
|
||||||
var index = 1
|
var index = 1
|
||||||
// If book media type and all files are audio files then treat each one as an audiobook
|
// If book media type and all files are audio files then treat each one as an audiobook
|
||||||
if (itemMap[''] && !otherFiles.length && mediaType === 'book' && !itemMap[''].itemFiles.some(f => f.filetype !== 'audio')) {
|
if (itemMap[''] && !otherFiles.length && mediaType === 'book' && !itemMap[''].itemFiles.some((f) => f.filetype !== 'audio')) {
|
||||||
items = itemMap[''].itemFiles.map((audioFile) => {
|
items = itemMap[''].itemFiles.map((audioFile) => {
|
||||||
return this.cleanItem({ itemFiles: [audioFile], otherFiles: [], ignoredFiles: [] }, mediaType, index++)
|
return this.cleanItem({ itemFiles: [audioFile], otherFiles: [], ignoredFiles: [] }, mediaType, index++)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
items = Object.values(itemMap).map(i => this.cleanItem(i, mediaType, index++))
|
items = Object.values(itemMap).map((i) => this.cleanItem(i, mediaType, index++))
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items,
|
items,
|
||||||
ignoredFiles: ignoredFiles
|
ignoredFiles: ignoredFiles
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-52
@@ -1,19 +1,24 @@
|
|||||||
const pkg = require('./package.json')
|
const pkg = require('./package.json')
|
||||||
|
|
||||||
|
const routerBasePath = process.env.ROUTER_BASE_PATH || '/audiobookshelf'
|
||||||
|
const serverHostUrl = process.env.NODE_ENV === 'production' ? '' : 'http://localhost:3333'
|
||||||
|
const serverPaths = ['api/', 'public/', 'hls/', 'auth/', 'feed/', 'status', 'login', 'logout', 'init']
|
||||||
|
const proxy = Object.fromEntries(serverPaths.map((path) => [`${routerBasePath}/${path}`, { target: process.env.NODE_ENV !== 'production' ? serverHostUrl : '/' }]))
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode
|
// Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode
|
||||||
ssr: false,
|
ssr: false,
|
||||||
target: 'static',
|
target: 'static',
|
||||||
dev: process.env.NODE_ENV !== 'production',
|
dev: process.env.NODE_ENV !== 'production',
|
||||||
env: {
|
env: {
|
||||||
serverUrl: process.env.NODE_ENV === 'production' ? process.env.ROUTER_BASE_PATH || '' : 'http://localhost:3333',
|
serverUrl: serverHostUrl + routerBasePath,
|
||||||
chromecastReceiver: 'FD1F76C5'
|
chromecastReceiver: 'FD1F76C5'
|
||||||
},
|
},
|
||||||
telemetry: false,
|
telemetry: false,
|
||||||
|
|
||||||
publicRuntimeConfig: {
|
publicRuntimeConfig: {
|
||||||
version: pkg.version,
|
version: pkg.version,
|
||||||
routerBasePath: process.env.ROUTER_BASE_PATH || ''
|
routerBasePath
|
||||||
},
|
},
|
||||||
|
|
||||||
// Global page headers: https://go.nuxtjs.dev/config-head
|
// Global page headers: https://go.nuxtjs.dev/config-head
|
||||||
@@ -22,38 +27,23 @@ module.exports = {
|
|||||||
htmlAttrs: {
|
htmlAttrs: {
|
||||||
lang: 'en'
|
lang: 'en'
|
||||||
},
|
},
|
||||||
meta: [
|
meta: [{ charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', name: 'description', content: '' }, { hid: 'robots', name: 'robots', content: 'noindex' }],
|
||||||
{ charset: 'utf-8' },
|
|
||||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
|
||||||
{ hid: 'description', name: 'description', content: '' },
|
|
||||||
{ hid: 'robots', name: 'robots', content: 'noindex' }
|
|
||||||
],
|
|
||||||
script: [],
|
script: [],
|
||||||
link: [
|
link: [
|
||||||
{ rel: 'icon', type: 'image/x-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/favicon.ico' },
|
{ rel: 'icon', type: 'image/x-icon', href: routerBasePath + '/favicon.ico' },
|
||||||
{ rel: 'apple-touch-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/ios_icon.png' }
|
{ rel: 'apple-touch-icon', href: routerBasePath + '/ios_icon.png' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
router: {
|
router: {
|
||||||
base: process.env.ROUTER_BASE_PATH || ''
|
base: routerBasePath
|
||||||
},
|
},
|
||||||
|
|
||||||
// Global CSS: https://go.nuxtjs.dev/config-css
|
// Global CSS: https://go.nuxtjs.dev/config-css
|
||||||
css: [
|
css: ['@/assets/tailwind.css', '@/assets/app.css'],
|
||||||
'@/assets/tailwind.css',
|
|
||||||
'@/assets/app.css'
|
|
||||||
],
|
|
||||||
|
|
||||||
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
|
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
|
||||||
plugins: [
|
plugins: ['@/plugins/constants.js', '@/plugins/init.client.js', '@/plugins/axios.js', '@/plugins/toast.js', '@/plugins/utils.js', '@/plugins/i18n.js'],
|
||||||
'@/plugins/constants.js',
|
|
||||||
'@/plugins/init.client.js',
|
|
||||||
'@/plugins/axios.js',
|
|
||||||
'@/plugins/toast.js',
|
|
||||||
'@/plugins/utils.js',
|
|
||||||
'@/plugins/i18n.js'
|
|
||||||
],
|
|
||||||
|
|
||||||
// Auto import components: https://go.nuxtjs.dev/config-components
|
// Auto import components: https://go.nuxtjs.dev/config-components
|
||||||
components: true,
|
components: true,
|
||||||
@@ -65,30 +55,25 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
// Modules: https://go.nuxtjs.dev/config-modules
|
// Modules: https://go.nuxtjs.dev/config-modules
|
||||||
modules: [
|
modules: ['nuxt-socket-io', '@nuxtjs/axios', '@nuxtjs/proxy'],
|
||||||
'nuxt-socket-io',
|
|
||||||
'@nuxtjs/axios',
|
|
||||||
'@nuxtjs/proxy'
|
|
||||||
],
|
|
||||||
|
|
||||||
proxy: {
|
proxy,
|
||||||
'/api/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
|
|
||||||
'/dev/': { target: 'http://localhost:3333', pathRewrite: { '^/dev/': '' } }
|
|
||||||
},
|
|
||||||
|
|
||||||
io: {
|
io: {
|
||||||
sockets: [{
|
sockets: [
|
||||||
name: 'dev',
|
{
|
||||||
url: 'http://localhost:3333'
|
name: 'dev',
|
||||||
},
|
url: serverHostUrl
|
||||||
{
|
},
|
||||||
name: 'prod'
|
{
|
||||||
}]
|
name: 'prod'
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// Axios module configuration: https://go.nuxtjs.dev/config-axios
|
// Axios module configuration: https://go.nuxtjs.dev/config-axios
|
||||||
axios: {
|
axios: {
|
||||||
baseURL: process.env.ROUTER_BASE_PATH || ''
|
baseURL: routerBasePath
|
||||||
},
|
},
|
||||||
|
|
||||||
// nuxt/pwa https://pwa.nuxtjs.org
|
// nuxt/pwa https://pwa.nuxtjs.org
|
||||||
@@ -108,11 +93,11 @@ module.exports = {
|
|||||||
background_color: '#232323',
|
background_color: '#232323',
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg',
|
src: routerBasePath + '/icon.svg',
|
||||||
sizes: 'any'
|
sizes: 'any'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: (process.env.ROUTER_BASE_PATH || '') + '/icon192.png',
|
src: routerBasePath + '/icon192.png',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
sizes: 'any'
|
sizes: 'any'
|
||||||
}
|
}
|
||||||
@@ -129,10 +114,12 @@ module.exports = {
|
|||||||
// Build Configuration: https://go.nuxtjs.dev/config-build
|
// Build Configuration: https://go.nuxtjs.dev/config-build
|
||||||
build: {
|
build: {
|
||||||
postcss: {
|
postcss: {
|
||||||
plugins: {
|
postcssOptions: {
|
||||||
tailwindcss: {},
|
plugins: {
|
||||||
autoprefixer: {},
|
tailwindcss: {},
|
||||||
},
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watchers: {
|
watchers: {
|
||||||
@@ -147,12 +134,12 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temporary workaround for @nuxt-community/tailwindcss-module.
|
* Temporary workaround for @nuxt-community/tailwindcss-module.
|
||||||
*
|
*
|
||||||
* Reported: 2022-05-23
|
* Reported: 2022-05-23
|
||||||
* See: [Issue tracker](https://github.com/nuxt-community/tailwindcss-module/issues/480)
|
* See: [Issue tracker](https://github.com/nuxt-community/tailwindcss-module/issues/480)
|
||||||
*/
|
*/
|
||||||
devServerHandlers: [],
|
devServerHandlers: [],
|
||||||
|
|
||||||
ignore: ["**/*.test.*", "**/*.cy.*"]
|
ignore: ['**/*.test.*', '**/*.cy.*']
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+3106
-2020
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user