mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-04 09:50:42 +02:00
Compare commits
587 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7c21cc137 | |||
| 7e4c7a7e3b | |||
| 40babc9650 | |||
| 7a94f014ea | |||
| 32adb1bafd | |||
| f9a6239049 | |||
| 8dee1ec942 | |||
| 58e43cc6a7 | |||
| b8999fbc37 | |||
| 0dda4b6b27 | |||
| 817f2f6915 | |||
| 77fc6bba1a | |||
| c66d652a53 | |||
| 86bddba5c3 | |||
| 7779fd2972 | |||
| 05a4577792 | |||
| 56dc042282 | |||
| 95973243a6 | |||
| 18ad23d016 | |||
| e258f122f1 | |||
| 18200a8f01 | |||
| 9c47f404c9 | |||
| 2f6de71a3a | |||
| deb121c523 | |||
| 320e4dfb47 | |||
| 6194c48549 | |||
| 6aa9ecaaba | |||
| b3d020b89f | |||
| e196a6e5ca | |||
| 73cf22b499 | |||
| ac7464ce7e | |||
| 84e742f2a5 | |||
| a1e882cbf1 | |||
| 09121acbd5 | |||
| 5b9df84ba3 | |||
| 266db491aa | |||
| c7a317a87b | |||
| b027f3bda1 | |||
| cea991b82f | |||
| 7e2b51e6d2 | |||
| 8f310b6bf0 | |||
| b2a5fb46f1 | |||
| 6d7639853b | |||
| 3a16acbba4 | |||
| 027e1efaca | |||
| d1fabba86b | |||
| b290a4ada3 | |||
| bb477c617e | |||
| 9238c38842 | |||
| d268516fcb | |||
| d353cff1ae | |||
| 604f17f60b | |||
| 3911a7273b | |||
| 138bb563b8 | |||
| 3801ef062a | |||
| e4b9ac5446 | |||
| 9987d219f8 | |||
| dc7045c562 | |||
| 2cc6e56bd1 | |||
| a89a24e48e | |||
| a968aca304 | |||
| 8d1f460640 | |||
| 553ffd1934 | |||
| fd4932cdbb | |||
| dcaca43817 | |||
| 0eed4e82f9 | |||
| 2ed2328401 | |||
| 8b260c8bc6 | |||
| 7dcb9b98a0 | |||
| 311ac7104e | |||
| 2c45b28d48 | |||
| b53613f82c | |||
| 751371abb8 | |||
| 6365c02875 | |||
| fb3834156b | |||
| c03f3f722d | |||
| a06f48ca29 | |||
| 9d79552dda | |||
| ed98614b6f | |||
| 09dd2cc79c | |||
| e87237048a | |||
| d71968fd80 | |||
| f83c605ae1 | |||
| 4325f470dd | |||
| 800ecf8e82 | |||
| 5cb143d50b | |||
| 798c73c66c | |||
| 0fa7c46274 | |||
| c2d420ec70 | |||
| 152daf7bf3 | |||
| 8d99249e50 | |||
| c6724ba353 | |||
| a519d44666 | |||
| 7e8bf977cc | |||
| 4018be6330 | |||
| 99a3867ce9 | |||
| 2116f60133 | |||
| 794f0ef42a | |||
| 3e423839a1 | |||
| 2773c8c4a9 | |||
| e510174f12 | |||
| 08c9e8d47d | |||
| 1908ec3df5 | |||
| df3878d4ca | |||
| 1097de6f1f | |||
| e408070b19 | |||
| af67c2e86f | |||
| 6a52d2a968 | |||
| 3337b3af18 | |||
| 835d2c7f36 | |||
| 03f91099e0 | |||
| c04afd0787 | |||
| b03bd79f5d | |||
| 5ef632a7eb | |||
| 79b4042e8e | |||
| 8f718ef91c | |||
| 4053b20623 | |||
| c4d654635f | |||
| ef5d0ffa48 | |||
| 6a826cdb36 | |||
| 1d837f5f21 | |||
| 80873b379c | |||
| 82a8f8f126 | |||
| 4725a466da | |||
| 031edc870c | |||
| 625e2445b5 | |||
| 1640af2f1c | |||
| c76f76cc27 | |||
| 74af212293 | |||
| e04efb9c6a | |||
| ee17e7a555 | |||
| 694a852c07 | |||
| 18068bb261 | |||
| 71257f6c6c | |||
| 4d70929d2e | |||
| 578e9559e4 | |||
| 894ea0b80a | |||
| e54571f011 | |||
| 77d7a50b99 | |||
| 32da0f1224 | |||
| 2054accdc9 | |||
| 7d8b857c77 | |||
| 0107cb4782 | |||
| f273eee807 | |||
| 4af21b079a | |||
| c9eaf2db2d | |||
| cae1560344 | |||
| a5fb0d9cdb | |||
| 53c80d9798 | |||
| 832165716b | |||
| d9f2d8bf1d | |||
| a7a3a56509 | |||
| 4082fadf90 | |||
| 93160b83bf | |||
| 472240f994 | |||
| c3f0fb8e5e | |||
| b156ebeb9f | |||
| e4c775c847 | |||
| 45e8e72759 | |||
| 0ae7340889 | |||
| 8c38987d92 | |||
| 878f0787ba | |||
| 880d85eaef | |||
| f7aaebc1de | |||
| d96ebbe82d | |||
| 70d67156f0 | |||
| f293b317be | |||
| 1f23794f88 | |||
| e6bfd118f6 | |||
| 1166400ab1 | |||
| 55f0ac871b | |||
| 3584f6e24f | |||
| 23bf2594c8 | |||
| 8fb460ce05 | |||
| 8c4bbfd6a2 | |||
| 742961e0b8 | |||
| 5b6807892f | |||
| b911a25c57 | |||
| 53110674e4 | |||
| f963cd4753 | |||
| 0dccc3bcae | |||
| 5b4fd5b254 | |||
| bdb9d3caeb | |||
| 9aca824b59 | |||
| 8e891805eb | |||
| 2760517445 | |||
| 889ee33320 | |||
| 4f65801713 | |||
| 3e75acd4ef | |||
| 3e8fe2ef60 | |||
| 0bc441de20 | |||
| a8c2f0d4c8 | |||
| b59da8bd0c | |||
| 77cb4f75c6 | |||
| 9cf1711fae | |||
| f472116dc3 | |||
| c7eb9d7799 | |||
| c66380eaeb | |||
| 1bebb22705 | |||
| 4e96649fe3 | |||
| a21cec806e | |||
| 8a3b8d2249 | |||
| 581277914c | |||
| e678fe6e2f | |||
| 3845940245 | |||
| 6c63e2131c | |||
| e25e2b238f | |||
| 99110f587a | |||
| b553e959e2 | |||
| f7b94a4b6d | |||
| e9a705587a | |||
| 264ae928a9 | |||
| f5248a9f00 | |||
| 3473ff594a | |||
| 20bb6e13b5 | |||
| a05d32b1d7 | |||
| c6b3521cb6 | |||
| 2444504c6a | |||
| d38532c07a | |||
| 4f7831611f | |||
| d09db19cd5 | |||
| 030e43f382 | |||
| f081a7fdc1 | |||
| f0d5f46199 | |||
| 0b8f6db45e | |||
| 806c0a2991 | |||
| 7d6d3e6687 | |||
| ad07ed7e25 | |||
| d3402e30c2 | |||
| 25fe4dee3a | |||
| 3c21c82ce1 | |||
| 3c8876a37d | |||
| fba70c9831 | |||
| 27e40d16fd | |||
| 448cbf8530 | |||
| f1153f9da5 | |||
| d09a21d922 | |||
| 62afa3c3ee | |||
| 85446be0e5 | |||
| 018ca8e7ee | |||
| f02453ac92 | |||
| 84b77f4c7f | |||
| d41276ba8c | |||
| 576d7dc024 | |||
| 6d2b1df560 | |||
| 8255e4308c | |||
| 794adf0292 | |||
| f2e0b9762c | |||
| 7d0def0edb | |||
| 0653572396 | |||
| d9a3750667 | |||
| 9c0c7b6b08 | |||
| df1391d93f | |||
| bf6d81b333 | |||
| 8775e55762 | |||
| d0d152c20d | |||
| 4ff7355262 | |||
| 6cc7a44a22 | |||
| ad092ef8f8 | |||
| 4102ed8be4 | |||
| 691f291843 | |||
| ac381854e5 | |||
| 9c8900560c | |||
| d9cfcc86e7 | |||
| ce803dd6de | |||
| 97afd22f81 | |||
| e24eaab3f1 | |||
| e201247d69 | |||
| a24dae5262 | |||
| e59babdf24 | |||
| 8dbe1e4e5d | |||
| cdc37ddb0f | |||
| f127a7beb5 | |||
| df60aeb456 | |||
| 30c327d92a | |||
| 596bddf791 | |||
| 44ff90a6f2 | |||
| 293851d931 | |||
| 8b995a179d | |||
| 4d32a22de9 | |||
| af1ff12dbb | |||
| d96ed01ce4 | |||
| 7610e97f0f | |||
| 4f5123e842 | |||
| d102065d02 | |||
| 34315d4c10 | |||
| 276a179446 | |||
| 4462d32e98 | |||
| 9722674072 | |||
| 35bb77c9c2 | |||
| cf6f49ce75 | |||
| d614373c64 | |||
| b9969c78a6 | |||
| fbf482d6b6 | |||
| dd74d0a726 | |||
| b13b80e011 | |||
| e384863148 | |||
| 9c44fc0d01 | |||
| d21fe49ce2 | |||
| a992400d6a | |||
| 108b2a60f5 | |||
| af684e6a69 | |||
| 5336d0525e | |||
| bb4eec9355 | |||
| 28404f37b8 | |||
| 7b92c15a46 | |||
| c150ed4e98 | |||
| cb7632b216 | |||
| b8849677de | |||
| 9bf8d7de11 | |||
| 6634ce8fd4 | |||
| 9d4303ef7b | |||
| 1f7be58124 | |||
| 6b8b27b04f | |||
| ba4061e5a4 | |||
| 5017e7ce9e | |||
| 693dc00fa3 | |||
| f3f5f3b9bd | |||
| b515c6c746 | |||
| 35e196238a | |||
| 2dc93258f1 | |||
| 5123f7d240 | |||
| 06d3bd76a8 | |||
| 52196afd99 | |||
| 3e44ee6f50 | |||
| 9841826e10 | |||
| def93d18ec | |||
| 387a3d05b4 | |||
| 398d04fc08 | |||
| c5e5e516af | |||
| 1c6f99b876 | |||
| d0af82e71a | |||
| 76e7616439 | |||
| fe99a269bc | |||
| 5315f65023 | |||
| c2809808c3 | |||
| 204ac4f204 | |||
| accd5d1096 | |||
| 5025c6a3ea | |||
| 6d0d1415e4 | |||
| 514f5c2409 | |||
| 2cc58b2c8a | |||
| 777a055fcd | |||
| b45085d2d6 | |||
| 22f6e86a12 | |||
| dc6783ea76 | |||
| a6f10ca48e | |||
| aac01d6d9a | |||
| a617994207 | |||
| 7a33a412fc | |||
| 0135b3560c | |||
| 6968a5c02a | |||
| 5e2bb0b12c | |||
| 7122756e58 | |||
| 8ecc912c2d | |||
| c8cea4e6af | |||
| 9da0be6d36 | |||
| 0c5d05d319 | |||
| c41bdb951c | |||
| 4a3eb7727b | |||
| 81640464ba | |||
| 54815ea9c7 | |||
| 679ffed0ea | |||
| 09397cf3de | |||
| eda7036f70 | |||
| e669a8d378 | |||
| 8e01859075 | |||
| f0525d4f0d | |||
| 84c9c6cb50 | |||
| 346df3680c | |||
| 6aa7c8a3d8 | |||
| 704c6f7bde | |||
| f01055f6e6 | |||
| 759c58d3f7 | |||
| 357176b301 | |||
| 9bb4dc3ab0 | |||
| 709c33f27a | |||
| 4d846e225a | |||
| 5dc6d613bd | |||
| 63ccdb68f0 | |||
| 424ef1aec3 | |||
| b6995ba5d1 | |||
| 9968743a93 | |||
| c377b57601 | |||
| 262d0b46e3 | |||
| 32fc4f6555 | |||
| 81572adab6 | |||
| 1ad2e71fd5 | |||
| db66b9eaeb | |||
| 28c2e62e61 | |||
| 96401c377c | |||
| 9d45880b37 | |||
| 9052ceedd3 | |||
| 4968864498 | |||
| f44c2d9e11 | |||
| 0c8e334b1a | |||
| abaa7b5ad0 | |||
| df01e493ec | |||
| 949c8ce230 | |||
| 9eaa0c26cd | |||
| d71f091e3e | |||
| 2589121908 | |||
| ff425212e7 | |||
| 243baaf775 | |||
| 7275b1063b | |||
| 4fd97510b8 | |||
| 6e67b1d9dd | |||
| 0fc6afec26 | |||
| c950ac7d69 | |||
| 8979e19e92 | |||
| 6a51cb07e8 | |||
| 846a8c3881 | |||
| 0cd698cc8d | |||
| 13d9462868 | |||
| d8e2ff8b0e | |||
| 35c2a5c1a3 | |||
| 19dc096d22 | |||
| 535ebc10f0 | |||
| 7486a0659b | |||
| 273866fe92 | |||
| 6425d95deb | |||
| 68a39449a2 | |||
| 8e08458ea2 | |||
| 1119ddef8a | |||
| 3d0219a866 | |||
| 6ce1806359 | |||
| f05a513767 | |||
| d03c338b48 | |||
| 5e5a988f7a | |||
| 6d1f0b27df | |||
| de25763a74 | |||
| a894ceb9cf | |||
| 387e58a714 | |||
| d01a7cb756 | |||
| cae874ef05 | |||
| 733afc3e29 | |||
| 0772730336 | |||
| 8b02fe07c8 | |||
| 98f93a665c | |||
| 754566b221 | |||
| f4f9adad35 | |||
| 16f7f1166e | |||
| f527b0f4d5 | |||
| 4f41df53c9 | |||
| 8a15f775a2 | |||
| 5e83bcd283 | |||
| 2fd5dfcb66 | |||
| 872ce4fa38 | |||
| ba792d91e5 | |||
| 4997c716db | |||
| fd72d05280 | |||
| 241b56ad45 | |||
| 635c384952 | |||
| ef930fd1b4 | |||
| 49997a1336 | |||
| 8d0434143c | |||
| 8e0319994e | |||
| 0ed6045d1e | |||
| 25c7e95a64 | |||
| 1781c4bbcb | |||
| c4ce72d44e | |||
| 78813c4b28 | |||
| 990baa2dc6 | |||
| c85f4467d2 | |||
| 59f7609054 | |||
| 2ef827e3fa | |||
| 5cadc8d90f | |||
| 40e7e36ef6 | |||
| d60ad96f8a | |||
| 46ba342d49 | |||
| ace6b2b81f | |||
| fa7e2dfafe | |||
| 015310c15d | |||
| f624f04dec | |||
| 7c13cfcda2 | |||
| fc265dadae | |||
| f9905f887e | |||
| eb72bfbbc0 | |||
| c268cace09 | |||
| 9666caf7a3 | |||
| 9e01e5c24e | |||
| 25e613a867 | |||
| fe23a86eaa | |||
| cb5a7d6aef | |||
| 7deb89ce7a | |||
| 1e300c77c9 | |||
| ed7cc42959 | |||
| f681ff68a1 | |||
| ba112bf9c2 | |||
| 718434545a | |||
| 0e9a4c95a9 | |||
| 3c997c8468 | |||
| eb49646256 | |||
| c54b5eadfd | |||
| 659c671c25 | |||
| 0df5a7816d | |||
| 26c976b6b9 | |||
| bdeb22615e | |||
| 257bf2ebe0 | |||
| fc33da447a | |||
| df45347690 | |||
| b876256736 | |||
| 3ce6e45761 | |||
| 5ac6b85da1 | |||
| 69e0a0732a | |||
| 087835a9f3 | |||
| 1f7b181b7b | |||
| 1afb8840db | |||
| d9531166b6 | |||
| 336de49d8d | |||
| 3cc527484d | |||
| 45987ffd63 | |||
| 1a1ef9c378 | |||
| 342d100f3e | |||
| e0b90c6813 | |||
| 2706a9c4aa | |||
| 2cc9d1b7f8 | |||
| 2b7268c952 | |||
| e097fe1e88 | |||
| 6819c0b108 | |||
| 58cd751b43 | |||
| 9f834a5345 | |||
| 5eaf9c69ad | |||
| a1074e69ac | |||
| 65aec6a099 | |||
| 38957d4f32 | |||
| a2dc76e190 | |||
| fd84cd0d7f | |||
| db7744eb84 | |||
| af513a2fb6 | |||
| 4cb5c934d5 | |||
| 37f84a0f62 | |||
| 70595181f1 | |||
| b357bbed60 | |||
| f7a720c6ac | |||
| 6549605efd | |||
| 33952fb1fd | |||
| 7b207dc5d8 | |||
| cb24a9c1ec | |||
| 3b42af5213 | |||
| b56691f1a2 | |||
| ac3154093c | |||
| 01ef24f5e6 | |||
| 3fb73c7426 | |||
| bf3bc06322 | |||
| 2733c28784 | |||
| b3dac831e6 | |||
| 35702aa770 | |||
| b2ffb3b7b9 | |||
| c52fe4b583 | |||
| af8ace7d1f | |||
| de37e40a1e | |||
| 56f5df91dc | |||
| fc590abb09 | |||
| bc7bbc1b7d | |||
| 4345973213 | |||
| b4ff9f5944 | |||
| a9a253f769 | |||
| a9783efa34 | |||
| a380ee080f | |||
| eabefd099c | |||
| 97799919e6 | |||
| 35870a0158 | |||
| ec05bd36e4 | |||
| be041f93c2 | |||
| a156d3595b | |||
| a1d549a2b1 | |||
| 812cb5a160 | |||
| e6264540af | |||
| 79fe064c4a | |||
| 7e69713683 | |||
| 3bbeb8f27a | |||
| 04fb8fa61d | |||
| 2caa861b8a | |||
| d7f0815fb3 | |||
| e6ab05e177 | |||
| c2ecfd428b | |||
| 9f26274ca8 | |||
| 7764f1cf75 | |||
| dc3c978f8d | |||
| 13fac2d5bc | |||
| fd0af6b2dd | |||
| 121805ba39 | |||
| f9bbd71174 | |||
| 2fbb31e0ea | |||
| 89167543fa | |||
| 33e0987d73 |
@@ -23,7 +23,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
if: ${{ !contains(github.event.head_commit.message, 'skip ci') && github.repository == 'advplyr/audiobookshelf' }}
|
if: ${{ !contains(github.event.head_commit.message, 'skip ci') && github.repository == 'advplyr/audiobookshelf' }}
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
|
|||||||
@@ -23,3 +23,4 @@ sw.*
|
|||||||
.DS_STORE
|
.DS_STORE
|
||||||
.idea/*
|
.idea/*
|
||||||
tailwind.compiled.css
|
tailwind.compiled.css
|
||||||
|
tailwind.config.js
|
||||||
|
|||||||
+34
-16
@@ -1,34 +1,32 @@
|
|||||||
|
ARG NUSQLITE3_DIR="/usr/local/lib/nusqlite3"
|
||||||
|
ARG NUSQLITE3_PATH="${NUSQLITE3_DIR}/libnusqlite3.so"
|
||||||
|
|
||||||
### STAGE 0: Build client ###
|
### STAGE 0: Build client ###
|
||||||
FROM node:20-alpine AS build
|
FROM node:20-alpine AS build-client
|
||||||
|
|
||||||
WORKDIR /client
|
WORKDIR /client
|
||||||
COPY /client /client
|
COPY /client /client
|
||||||
RUN npm ci && npm cache clean --force
|
RUN npm ci && npm cache clean --force
|
||||||
RUN npm run generate
|
RUN npm run generate
|
||||||
|
|
||||||
### STAGE 1: Build server ###
|
### STAGE 1: Build server ###
|
||||||
FROM node:20-alpine
|
FROM node:20-alpine AS build-server
|
||||||
|
|
||||||
|
ARG NUSQLITE3_DIR
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk add --no-cache --update \
|
||||||
apk add --no-cache --update \
|
|
||||||
curl \
|
curl \
|
||||||
tzdata \
|
|
||||||
ffmpeg \
|
|
||||||
make \
|
make \
|
||||||
python3 \
|
python3 \
|
||||||
g++ \
|
g++ \
|
||||||
tini \
|
|
||||||
unzip
|
unzip
|
||||||
|
|
||||||
COPY --from=build /client/dist /client/dist
|
WORKDIR /server
|
||||||
COPY index.js package* /
|
COPY index.js package* /server
|
||||||
COPY server server
|
COPY /server /server/server
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
|
||||||
|
|
||||||
ENV NUSQLITE3_DIR="/usr/local/lib/nusqlite3"
|
|
||||||
ENV NUSQLITE3_PATH="${NUSQLITE3_DIR}/libnusqlite3.so"
|
|
||||||
|
|
||||||
RUN case "$TARGETPLATFORM" in \
|
RUN case "$TARGETPLATFORM" in \
|
||||||
"linux/amd64") \
|
"linux/amd64") \
|
||||||
@@ -42,14 +40,34 @@ RUN case "$TARGETPLATFORM" in \
|
|||||||
|
|
||||||
RUN npm ci --only=production
|
RUN npm ci --only=production
|
||||||
|
|
||||||
RUN apk del make python3 g++
|
### STAGE 2: Create minimal runtime image ###
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
ARG NUSQLITE3_DIR
|
||||||
|
ARG NUSQLITE3_PATH
|
||||||
|
|
||||||
|
# Install only runtime dependencies
|
||||||
|
RUN apk add --no-cache --update \
|
||||||
|
tzdata \
|
||||||
|
ffmpeg \
|
||||||
|
tini
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy compiled frontend and server from build stages
|
||||||
|
COPY --from=build-client /client/dist /app/client/dist
|
||||||
|
COPY --from=build-server /server /app
|
||||||
|
COPY --from=build-server ${NUSQLITE3_PATH} ${NUSQLITE3_PATH}
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
ENV PORT=80
|
ENV PORT=80
|
||||||
|
ENV NODE_ENV=production
|
||||||
ENV CONFIG_PATH="/config"
|
ENV CONFIG_PATH="/config"
|
||||||
ENV METADATA_PATH="/metadata"
|
ENV METADATA_PATH="/metadata"
|
||||||
ENV SOURCE="docker"
|
ENV SOURCE="docker"
|
||||||
|
ENV NUSQLITE3_DIR=${NUSQLITE3_DIR}
|
||||||
|
ENV NUSQLITE3_PATH=${NUSQLITE3_PATH}
|
||||||
|
|
||||||
ENTRYPOINT ["tini", "--"]
|
ENTRYPOINT ["tini", "--"]
|
||||||
CMD ["node", "index.js"]
|
CMD ["node", "index.js"]
|
||||||
|
|||||||
@@ -217,6 +217,16 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.results.episodes?.length) {
|
||||||
|
shelves.push({
|
||||||
|
id: 'episodes',
|
||||||
|
label: 'Episodes',
|
||||||
|
labelStringKey: 'LabelEpisodes',
|
||||||
|
type: 'episode',
|
||||||
|
entities: this.results.episodes.map((res) => res.libraryItem)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (this.results.series?.length) {
|
if (this.results.series?.length) {
|
||||||
shelves.push({
|
shelves.push({
|
||||||
id: 'series',
|
id: 'series',
|
||||||
|
|||||||
@@ -3,24 +3,18 @@
|
|||||||
<div class="flex md:hidden h-10 items-center">
|
<div class="flex md:hidden h-10 items-center">
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}`" class="grow h-full flex justify-center items-center" :class="isHomePage ? 'bg-primary/80' : 'bg-primary/40'">
|
<nuxt-link :to="`/library/${currentLibraryId}`" class="grow h-full flex justify-center items-center" :class="isHomePage ? 'bg-primary/80' : 'bg-primary/40'">
|
||||||
<p v-if="isHomePage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonHome }}</p>
|
<p v-if="isHomePage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonHome }}</p>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<span v-else class="material-symbols text-lg">home</span>
|
||||||
<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" />
|
|
||||||
</svg>
|
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="grow h-full flex justify-center items-center" :class="isLibraryPage ? 'bg-primary/80' : 'bg-primary/40'">
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="grow h-full flex justify-center items-center" :class="isLibraryPage ? 'bg-primary/80' : 'bg-primary/40'">
|
||||||
<p v-if="isLibraryPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonLibrary }}</p>
|
<p v-if="isLibraryPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonLibrary }}</p>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<span v-else class="material-symbols text-lg">import_contacts</span>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
|
||||||
</svg>
|
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="grow h-full flex justify-center items-center" :class="isPodcastLatestPage ? 'bg-primary/80' : 'bg-primary/40'">
|
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="grow h-full flex justify-center items-center" :class="isPodcastLatestPage ? 'bg-primary/80' : 'bg-primary/40'">
|
||||||
<p class="text-sm">{{ $strings.ButtonLatest }}</p>
|
<p class="text-sm">{{ $strings.ButtonLatest }}</p>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary/80' : 'bg-primary/40'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary/80' : 'bg-primary/40'">
|
||||||
<p v-if="isSeriesPage" class="text-sm">{{ $strings.ButtonSeries }}</p>
|
<p v-if="isSeriesPage" class="text-sm">{{ $strings.ButtonSeries }}</p>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<span v-else class="material-symbols text-lg">view_column</span>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
|
||||||
</svg>
|
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="grow h-full flex justify-center items-center" :class="isPlaylistsPage ? 'bg-primary/80' : 'bg-primary/40'">
|
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="grow h-full flex justify-center items-center" :class="isPlaylistsPage ? 'bg-primary/80' : 'bg-primary/40'">
|
||||||
<p v-if="isPlaylistsPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonPlaylists }}</p>
|
<p v-if="isPlaylistsPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonPlaylists }}</p>
|
||||||
@@ -32,12 +26,7 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary/80' : 'bg-primary/40'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary/80' : 'bg-primary/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">
|
<span v-else class="material-symbols text-lg">groups</span>
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary/80' : 'bg-primary/40'">
|
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary/80' : 'bg-primary/40'">
|
||||||
<p class="text-sm">{{ $strings.ButtonAdd }}</p>
|
<p class="text-sm">{{ $strings.ButtonAdd }}</p>
|
||||||
@@ -274,15 +263,10 @@ export default {
|
|||||||
isAuthorsPage() {
|
isAuthorsPage() {
|
||||||
return this.page === 'authors'
|
return this.page === 'authors'
|
||||||
},
|
},
|
||||||
isAlbumsPage() {
|
|
||||||
return this.page === 'albums'
|
|
||||||
},
|
|
||||||
numShowing() {
|
numShowing() {
|
||||||
return this.totalEntities
|
return this.totalEntities
|
||||||
},
|
},
|
||||||
entityName() {
|
entityName() {
|
||||||
if (this.isAlbumsPage) return 'Albums'
|
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -70,6 +70,11 @@ export default {
|
|||||||
title: this.$strings.HeaderUsers,
|
title: this.$strings.HeaderUsers,
|
||||||
path: '/config/users'
|
path: '/config/users'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'config-api-keys',
|
||||||
|
title: this.$strings.HeaderApiKeys,
|
||||||
|
path: '/config/api-keys'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'config-sessions',
|
id: 'config-sessions',
|
||||||
title: this.$strings.HeaderListeningSessions,
|
title: this.$strings.HeaderListeningSessions,
|
||||||
|
|||||||
@@ -778,10 +778,6 @@ export default {
|
|||||||
windowResize() {
|
windowResize() {
|
||||||
this.executeRebuild()
|
this.executeRebuild()
|
||||||
},
|
},
|
||||||
socketInit() {
|
|
||||||
// Server settings are set on socket init
|
|
||||||
this.executeRebuild()
|
|
||||||
},
|
|
||||||
initListeners() {
|
initListeners() {
|
||||||
window.addEventListener('resize', this.windowResize)
|
window.addEventListener('resize', this.windowResize)
|
||||||
|
|
||||||
@@ -794,7 +790,6 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
|
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||||
this.$eventBus.$on('socket_init', this.socketInit)
|
|
||||||
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||||
|
|
||||||
if (this.$root.socket) {
|
if (this.$root.socket) {
|
||||||
@@ -826,7 +821,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
|
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||||
this.$eventBus.$off('socket_init', this.socketInit)
|
|
||||||
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||||
|
|
||||||
if (this.$root.socket) {
|
if (this.$root.socket) {
|
||||||
|
|||||||
@@ -5,9 +5,7 @@
|
|||||||
|
|
||||||
<div id="siderail-buttons-container" role="navigation" aria-label="Library Navigation" :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/70 hover:bg-primary cursor-pointer relative" :class="homePage ? 'bg-primary/80' : 'bg-bg/60'">
|
<nuxt-link :to="`/library/${currentLibraryId}`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="homePage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<span class="material-symbols text-2xl">home</span>
|
||||||
<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" />
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonHome }}</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonHome }}</p>
|
||||||
|
|
||||||
@@ -23,9 +21,7 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="showLibrary ? 'bg-primary/80' : 'bg-bg/60'">
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="showLibrary ? 'bg-primary/80' : 'bg-bg/60'">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<span class="material-symbols text-2xl">import_contacts</span>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLibrary }}</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLibrary }}</p>
|
||||||
|
|
||||||
@@ -33,9 +29,7 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isSeriesPage ? 'bg-primary/80' : 'bg-bg/60'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isSeriesPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<span class="material-symbols text-2xl">view_column</span>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSeries }}</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSeries }}</p>
|
||||||
|
|
||||||
@@ -59,12 +53,7 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary/80' : 'bg-bg/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/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||||
<svg class="w-6 h-6" viewBox="0 0 24 24">
|
<span class="material-symbols text-2xl">groups</span>
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAuthors }}</p>
|
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAuthors }}</p>
|
||||||
|
|
||||||
@@ -116,7 +105,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full h-12 px-1 py-2 border-t border-black/20 bg-bg absolute left-0" :style="{ bottom: streamLibraryItem ? '224px' : '65px' }">
|
<div class="w-full h-12 px-1 py-2 border-t border-black/20 bg-bg absolute left-0" :style="{ bottom: streamLibraryItem ? '224px' : '65px' }">
|
||||||
<p class="underline font-mono text-xs text-center text-gray-300 leading-3 mb-1" @click="clickChangelog">v{{ $config.version }}</p>
|
<p class="underline font-mono text-xs text-center text-gray-300 leading-3 mb-1 cursor-pointer" @click="clickChangelog">v{{ $config.version }}</p>
|
||||||
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xxs text-center block leading-3">Update</a>
|
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xxs text-center block leading-3">Update</a>
|
||||||
<p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p>
|
<p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,9 +71,6 @@ export default {
|
|||||||
coverHeight() {
|
coverHeight() {
|
||||||
return this.cardHeight
|
return this.cardHeight
|
||||||
},
|
},
|
||||||
userToken() {
|
|
||||||
return this.store.getters['user/getToken']
|
|
||||||
},
|
|
||||||
_author() {
|
_author() {
|
||||||
return this.author || {}
|
return this.author || {}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,9 +13,17 @@
|
|||||||
<div class="grow" />
|
<div class="grow" />
|
||||||
<p class="text-sm md:text-base">{{ book.publishedYear }}</p>
|
<p class="text-sm md:text-base">{{ book.publishedYear }}</p>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="book.author" class="text-gray-300 text-xs md:text-sm">{{ $getString('LabelByAuthor', [book.author]) }}</p>
|
|
||||||
<p v-if="book.narrator" class="text-gray-400 text-xs">{{ $strings.LabelNarrators }}: {{ book.narrator }}</p>
|
<div class="flex items-center">
|
||||||
<p v-if="book.duration" class="text-gray-400 text-xs">{{ $strings.LabelDuration }}: {{ $elapsedPrettyExtended(bookDuration, false) }} {{ bookDurationComparison }}</p>
|
<div>
|
||||||
|
<p v-if="book.author" class="text-gray-300 text-xs md:text-sm">{{ $getString('LabelByAuthor', [book.author]) }}</p>
|
||||||
|
<p v-if="book.narrator" class="text-gray-400 text-xs">{{ $strings.LabelNarrators }}: {{ book.narrator }}</p>
|
||||||
|
<p v-if="book.duration" class="text-gray-400 text-xs">{{ $strings.LabelDuration }}: {{ $elapsedPrettyExtended(bookDuration, false) }} {{ bookDurationComparison }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="grow" />
|
||||||
|
<div v-if="book.matchConfidence" class="rounded-full px-2 py-1 text-xs whitespace-nowrap text-white" :class="book.matchConfidence > 0.95 ? 'bg-success/80' : 'bg-info/80'">{{ $strings.LabelMatchConfidence }}: {{ (book.matchConfidence * 100).toFixed(0) }}%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="book.series?.length" class="flex py-1 -mx-1">
|
<div v-if="book.series?.length" class="flex py-1 -mx-1">
|
||||||
<div v-for="(series, index) in book.series" :key="index" class="bg-white/10 rounded-full px-1 py-0.5 mx-1">
|
<div v-for="(series, index) in book.series" :key="index" class="bg-white/10 rounded-full px-1 py-0.5 mx-1">
|
||||||
<p class="leading-3 text-xs text-gray-400">
|
<p class="leading-3 text-xs text-gray-400">
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center h-full px-1 overflow-hidden">
|
||||||
|
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
<div class="grow px-2 episodeSearchCardContent">
|
||||||
|
<p class="truncate text-sm">{{ episodeTitle }}</p>
|
||||||
|
<p class="text-xs text-gray-200 truncate">{{ podcastTitle }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
episode: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
bookCoverAspectRatio() {
|
||||||
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
|
},
|
||||||
|
coverWidth() {
|
||||||
|
if (this.bookCoverAspectRatio === 1) return 50 * 1.2
|
||||||
|
return 50
|
||||||
|
},
|
||||||
|
media() {
|
||||||
|
return this.libraryItem?.media || {}
|
||||||
|
},
|
||||||
|
mediaMetadata() {
|
||||||
|
return this.media.metadata || {}
|
||||||
|
},
|
||||||
|
episodeTitle() {
|
||||||
|
return this.episode.title || 'No Title'
|
||||||
|
},
|
||||||
|
podcastTitle() {
|
||||||
|
return this.mediaMetadata.title || 'No Title'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.episodeSearchCardContent {
|
||||||
|
width: calc(100% - 80px);
|
||||||
|
height: 75px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -20,10 +20,10 @@
|
|||||||
<div class="w-1/2 px-2">
|
<div class="w-1/2 px-2">
|
||||||
<div v-if="!isPodcast" class="flex items-end">
|
<div v-if="!isPodcast" class="flex items-end">
|
||||||
<ui-text-input-with-label v-model.trim="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
|
<ui-text-input-with-label v-model.trim="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
|
||||||
<ui-tooltip :text="$strings.LabelUploaderItemFetchMetadataHelp">
|
<ui-tooltip direction="top" :text="$strings.LabelUploaderItemFetchMetadataHelp">
|
||||||
<div class="ml-2 mb-1 w-8 h-8 bg-bg border border-white/10 flex items-center justify-center rounded-full hover:bg-primary cursor-pointer" @click="fetchMetadata">
|
<button type="button" class="ml-2 mb-1 w-8 h-8 bg-bg border border-white/10 flex items-center justify-center rounded-full hover:bg-primary cursor-pointer" @click="fetchMetadata">
|
||||||
<span class="text-base text-white/80 font-mono material-symbols">sync</span>
|
<span class="text-base text-white/80 font-mono material-symbols">sync</span>
|
||||||
</div>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full">
|
<div v-else class="w-full">
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div ref="card" :id="`album-card-${index}`" :style="{ width: cardWidth + 'px' }" class="absolute top-0 left-0 rounded-xs z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
|
||||||
<div class="relative" :style="{ height: coverHeight + 'px' }">
|
|
||||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
|
||||||
<div class="w-full h-full bg-primary relative rounded-sm overflow-hidden">
|
|
||||||
<covers-preview-cover ref="cover" :src="coverSrc" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative w-full">
|
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6e h-6e rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
|
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-xs border" :style="{ padding: `0em ${0.5}em` }">
|
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8e h-8e py-1e rounded-md text-center">
|
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
|
|
||||||
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ artist || ' ' }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
index: Number,
|
|
||||||
width: Number,
|
|
||||||
height: {
|
|
||||||
type: Number,
|
|
||||||
default: 192
|
|
||||||
},
|
|
||||||
bookshelfView: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
},
|
|
||||||
albumMount: {
|
|
||||||
type: Object,
|
|
||||||
default: () => null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
album: null,
|
|
||||||
isSelectionMode: false,
|
|
||||||
selected: false,
|
|
||||||
isHovering: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
bookCoverAspectRatio() {
|
|
||||||
return this.store.getters['libraries/getBookCoverAspectRatio']
|
|
||||||
},
|
|
||||||
cardWidth() {
|
|
||||||
return this.width || this.coverHeight
|
|
||||||
},
|
|
||||||
coverHeight() {
|
|
||||||
return this.height * this.sizeMultiplier
|
|
||||||
},
|
|
||||||
/*
|
|
||||||
cardHeight() {
|
|
||||||
return this.coverHeight + this.bottomTextHeight
|
|
||||||
},
|
|
||||||
bottomTextHeight() {
|
|
||||||
if (!this.isAlternativeBookshelfView) return 0
|
|
||||||
const lineHeight = 1.5
|
|
||||||
const remSize = 16
|
|
||||||
const baseHeight = this.sizeMultiplier * lineHeight * remSize
|
|
||||||
const titleHeight = this.labelFontSize * baseHeight
|
|
||||||
const paddingHeight = 4 * 2 * this.sizeMultiplier // py-1
|
|
||||||
return titleHeight + paddingHeight
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
coverSrc() {
|
|
||||||
const config = this.$config || this.$nuxt.$config
|
|
||||||
if (!this.album || !this.album.libraryItemId) return `${config.routerBasePath}/book_placeholder.jpg`
|
|
||||||
return this.store.getters['globals/getLibraryItemCoverSrcById'](this.album.libraryItemId)
|
|
||||||
},
|
|
||||||
labelFontSize() {
|
|
||||||
if (this.width < 160) return 0.75
|
|
||||||
return 0.9
|
|
||||||
},
|
|
||||||
sizeMultiplier() {
|
|
||||||
return this.store.getters['user/getSizeMultiplier']
|
|
||||||
},
|
|
||||||
title() {
|
|
||||||
return this.album ? this.album.title : ''
|
|
||||||
},
|
|
||||||
artist() {
|
|
||||||
return this.album ? this.album.artist : ''
|
|
||||||
},
|
|
||||||
store() {
|
|
||||||
return this.$store || this.$nuxt.$store
|
|
||||||
},
|
|
||||||
currentLibraryId() {
|
|
||||||
return this.store.state.libraries.currentLibraryId
|
|
||||||
},
|
|
||||||
isAlternativeBookshelfView() {
|
|
||||||
const constants = this.$constants || this.$nuxt.$constants
|
|
||||||
return this.bookshelfView == constants.BookshelfView.DETAIL
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
setEntity(album) {
|
|
||||||
this.album = album
|
|
||||||
},
|
|
||||||
setSelectionMode(val) {
|
|
||||||
this.isSelectionMode = val
|
|
||||||
},
|
|
||||||
mouseover() {
|
|
||||||
this.isHovering = true
|
|
||||||
},
|
|
||||||
mouseleave() {
|
|
||||||
this.isHovering = false
|
|
||||||
},
|
|
||||||
clickCard() {
|
|
||||||
if (!this.album) return
|
|
||||||
// const router = this.$router || this.$nuxt.$router
|
|
||||||
// router.push(`/album/${this.$encode(this.title)}`)
|
|
||||||
},
|
|
||||||
clickEdit() {
|
|
||||||
this.$emit('edit', this.album)
|
|
||||||
},
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
if (this.albumMount) {
|
|
||||||
this.setEntity(this.albumMount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -101,7 +101,8 @@
|
|||||||
<!-- Podcast Episode # -->
|
<!-- Podcast Episode # -->
|
||||||
<div cy-id="podcastEpisodeNumber" v-if="recentEpisodeNumber !== null && !isHovering && !isSelectionMode && !processing" class="absolute rounded-lg bg-black/90 box-shadow-md z-10" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }">
|
<div cy-id="podcastEpisodeNumber" v-if="recentEpisodeNumber !== null && !isHovering && !isSelectionMode && !processing" class="absolute rounded-lg bg-black/90 box-shadow-md z-10" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }">
|
||||||
<p :style="{ fontSize: 0.8 + 'em' }">
|
<p :style="{ fontSize: 0.8 + 'em' }">
|
||||||
Episode<span v-if="recentEpisodeNumber"> #{{ recentEpisodeNumber }}</span>
|
Episode
|
||||||
|
<span v-if="recentEpisodeNumber">#{{ recentEpisodeNumber }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -198,7 +199,10 @@ export default {
|
|||||||
return this.store.getters['user/getSizeMultiplier']
|
return this.store.getters['user/getSizeMultiplier']
|
||||||
},
|
},
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.store.state.serverSettings.dateFormat
|
return this.store.getters['getServerSetting']('dateFormat')
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.store.getters['getServerSetting']('timeFormat')
|
||||||
},
|
},
|
||||||
_libraryItem() {
|
_libraryItem() {
|
||||||
return this.libraryItem || {}
|
return this.libraryItem || {}
|
||||||
@@ -345,6 +349,18 @@ export default {
|
|||||||
if (this.mediaMetadata.publishedYear) return this.$getString('LabelPublishedDate', [this.mediaMetadata.publishedYear])
|
if (this.mediaMetadata.publishedYear) return this.$getString('LabelPublishedDate', [this.mediaMetadata.publishedYear])
|
||||||
return '\u00A0'
|
return '\u00A0'
|
||||||
}
|
}
|
||||||
|
if (this.orderBy === 'progress') {
|
||||||
|
if (!this.userProgressLastUpdated) return '\u00A0'
|
||||||
|
return this.$getString('LabelLastProgressDate', [this.$formatDatetime(this.userProgressLastUpdated, this.dateFormat, this.timeFormat)])
|
||||||
|
}
|
||||||
|
if (this.orderBy === 'progress.createdAt') {
|
||||||
|
if (!this.userProgressStartedDate) return '\u00A0'
|
||||||
|
return this.$getString('LabelStartedDate', [this.$formatDatetime(this.userProgressStartedDate, this.dateFormat, this.timeFormat)])
|
||||||
|
}
|
||||||
|
if (this.orderBy === 'progress.finishedAt') {
|
||||||
|
if (!this.userProgressFinishedDate) return '\u00A0'
|
||||||
|
return this.$getString('LabelFinishedDate', [this.$formatDatetime(this.userProgressFinishedDate, this.dateFormat, this.timeFormat)])
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
episodeProgress() {
|
episodeProgress() {
|
||||||
@@ -377,6 +393,18 @@ export default {
|
|||||||
let progressPercent = this.itemIsFinished ? 1 : this.booksInSeries ? this.seriesProgressPercent : this.useEBookProgress ? this.userProgress?.ebookProgress || 0 : this.userProgress?.progress || 0
|
let progressPercent = this.itemIsFinished ? 1 : this.booksInSeries ? this.seriesProgressPercent : this.useEBookProgress ? this.userProgress?.ebookProgress || 0 : this.userProgress?.progress || 0
|
||||||
return Math.max(Math.min(1, progressPercent), 0)
|
return Math.max(Math.min(1, progressPercent), 0)
|
||||||
},
|
},
|
||||||
|
userProgressLastUpdated() {
|
||||||
|
if (!this.userProgress) return null
|
||||||
|
return this.userProgress.lastUpdate
|
||||||
|
},
|
||||||
|
userProgressStartedDate() {
|
||||||
|
if (!this.userProgress) return null
|
||||||
|
return this.userProgress.startedAt
|
||||||
|
},
|
||||||
|
userProgressFinishedDate() {
|
||||||
|
if (!this.userProgress) return null
|
||||||
|
return this.userProgress.finishedAt
|
||||||
|
},
|
||||||
itemIsFinished() {
|
itemIsFinished() {
|
||||||
if (this.booksInSeries) return this.seriesIsFinished
|
if (this.booksInSeries) return this.seriesIsFinished
|
||||||
return this.userProgress ? !!this.userProgress.isFinished : false
|
return this.userProgress ? !!this.userProgress.isFinished : false
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export default {
|
|||||||
return this.height * this.sizeMultiplier
|
return this.height * this.sizeMultiplier
|
||||||
},
|
},
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.store.state.serverSettings.dateFormat
|
return this.store.getters['getServerSetting']('dateFormat')
|
||||||
},
|
},
|
||||||
labelFontSize() {
|
labelFontSize() {
|
||||||
if (this.width < 160) return 0.75
|
if (this.width < 160) return 0.75
|
||||||
|
|||||||
@@ -39,6 +39,15 @@
|
|||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<p v-if="episodeResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">{{ $strings.LabelEpisodes }}</p>
|
||||||
|
<template v-for="item in episodeResults">
|
||||||
|
<li :key="item.libraryItem.recentEpisode.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||||
|
<nuxt-link :to="`/item/${item.libraryItem.id}`">
|
||||||
|
<cards-episode-search-card :episode="item.libraryItem.recentEpisode" :library-item="item.libraryItem" />
|
||||||
|
</nuxt-link>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
<p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelAuthors }}</p>
|
<p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelAuthors }}</p>
|
||||||
<template v-for="item in authorResults">
|
<template v-for="item in authorResults">
|
||||||
<li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
<li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||||
@@ -100,6 +109,7 @@ export default {
|
|||||||
isFetching: false,
|
isFetching: false,
|
||||||
search: null,
|
search: null,
|
||||||
podcastResults: [],
|
podcastResults: [],
|
||||||
|
episodeResults: [],
|
||||||
bookResults: [],
|
bookResults: [],
|
||||||
authorResults: [],
|
authorResults: [],
|
||||||
seriesResults: [],
|
seriesResults: [],
|
||||||
@@ -115,7 +125,7 @@ export default {
|
|||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
totalResults() {
|
totalResults() {
|
||||||
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length + this.genreResults.length + this.podcastResults.length + this.narratorResults.length
|
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length + this.genreResults.length + this.podcastResults.length + this.narratorResults.length + this.episodeResults.length
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -132,6 +142,7 @@ export default {
|
|||||||
this.search = null
|
this.search = null
|
||||||
this.lastSearch = null
|
this.lastSearch = null
|
||||||
this.podcastResults = []
|
this.podcastResults = []
|
||||||
|
this.episodeResults = []
|
||||||
this.bookResults = []
|
this.bookResults = []
|
||||||
this.authorResults = []
|
this.authorResults = []
|
||||||
this.seriesResults = []
|
this.seriesResults = []
|
||||||
@@ -175,6 +186,7 @@ export default {
|
|||||||
if (!this.isFetching) return
|
if (!this.isFetching) return
|
||||||
|
|
||||||
this.podcastResults = searchResults.podcast || []
|
this.podcastResults = searchResults.podcast || []
|
||||||
|
this.episodeResults = searchResults.episodes || []
|
||||||
this.bookResults = searchResults.book || []
|
this.bookResults = searchResults.book || []
|
||||||
this.authorResults = searchResults.authors || []
|
this.authorResults = searchResults.authors || []
|
||||||
this.seriesResults = searchResults.series || []
|
this.seriesResults = searchResults.series || []
|
||||||
|
|||||||
@@ -94,6 +94,9 @@ export default {
|
|||||||
userIsAdminOrUp() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
|
userCanAccessExplicitContent() {
|
||||||
|
return this.$store.getters['user/getUserCanAccessExplicitContent']
|
||||||
|
},
|
||||||
libraryMediaType() {
|
libraryMediaType() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
},
|
},
|
||||||
@@ -239,6 +242,15 @@ export default {
|
|||||||
sublist: false
|
sublist: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (this.userCanAccessExplicitContent) {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.LabelExplicit,
|
||||||
|
value: 'explicit',
|
||||||
|
sublist: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (this.userIsAdminOrUp) {
|
if (this.userIsAdminOrUp) {
|
||||||
items.push({
|
items.push({
|
||||||
text: this.$strings.LabelShareOpen,
|
text: this.$strings.LabelShareOpen,
|
||||||
@@ -249,7 +261,7 @@ export default {
|
|||||||
return items
|
return items
|
||||||
},
|
},
|
||||||
podcastItems() {
|
podcastItems() {
|
||||||
return [
|
const items = [
|
||||||
{
|
{
|
||||||
text: this.$strings.LabelAll,
|
text: this.$strings.LabelAll,
|
||||||
value: 'all'
|
value: 'all'
|
||||||
@@ -276,8 +288,23 @@ export default {
|
|||||||
text: this.$strings.ButtonIssues,
|
text: this.$strings.ButtonIssues,
|
||||||
value: 'issues',
|
value: 'issues',
|
||||||
sublist: false
|
sublist: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelRSSFeedOpen,
|
||||||
|
value: 'feed-open',
|
||||||
|
sublist: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (this.userCanAccessExplicitContent) {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.LabelExplicit,
|
||||||
|
value: 'explicit',
|
||||||
|
sublist: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
},
|
},
|
||||||
selectItems() {
|
selectItems() {
|
||||||
if (this.isSeries) return this.seriesItems
|
if (this.isSeries) return this.seriesItems
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</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/5 overflow-auto focus:outline-hidden text-sm" role="menu">
|
<ul v-show="showMenu" class="librarySortMenu 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/5 overflow-auto focus:outline-hidden 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="menuitem" @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">
|
||||||
@@ -130,6 +130,18 @@ export default {
|
|||||||
text: this.$strings.LabelFileModified,
|
text: this.$strings.LabelFileModified,
|
||||||
value: 'mtimeMs'
|
value: 'mtimeMs'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelLibrarySortByProgress,
|
||||||
|
value: 'progress'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelLibrarySortByProgressStarted,
|
||||||
|
value: 'progress.createdAt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelLibrarySortByProgressFinished,
|
||||||
|
value: 'progress.finishedAt'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.LabelRandomly,
|
text: this.$strings.LabelRandomly,
|
||||||
value: 'random'
|
value: 'random'
|
||||||
@@ -191,3 +203,9 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.librarySortMenu {
|
||||||
|
max-height: calc(100vh - 125px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -39,9 +39,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
userToken() {
|
|
||||||
return this.$store.getters['user/getToken']
|
|
||||||
},
|
|
||||||
_author() {
|
_author() {
|
||||||
return this.author || {}
|
return this.author || {}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -309,9 +309,9 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
console.log('Account updated', data.user)
|
console.log('Account updated', data.user)
|
||||||
|
|
||||||
if (data.user.id === this.user.id && data.user.token !== this.user.token) {
|
if (data.user.id === this.user.id && data.user.accessToken !== this.user.accessToken) {
|
||||||
console.log('Current user token was updated')
|
console.log('Current user access token was updated')
|
||||||
this.$store.commit('user/setUserToken', data.user.token)
|
this.$store.commit('user/setAccessToken', data.user.accessToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$toast.success(this.$strings.ToastAccountUpdateSuccess)
|
this.$toast.success(this.$strings.ToastAccountUpdateSuccess)
|
||||||
@@ -351,9 +351,6 @@ export default {
|
|||||||
this.$toast.error(errMsg || 'Failed to create account')
|
this.$toast.error(errMsg || 'Failed to create account')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
toggleActive() {
|
|
||||||
this.newUser.isActive = !this.newUser.isActive
|
|
||||||
},
|
|
||||||
userTypeUpdated(type) {
|
userTypeUpdated(type) {
|
||||||
this.newUser.permissions = {
|
this.newUser.permissions = {
|
||||||
download: type !== 'guest',
|
download: type !== 'guest',
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<modals-modal ref="modal" v-model="show" name="api-key-created" :width="800" :height="'unset'" persistent>
|
||||||
|
<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="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="min-height: 200px; max-height: 80vh">
|
||||||
|
<div class="w-full p-8">
|
||||||
|
<p class="text-lg text-white mb-4">{{ $getString('LabelApiKeyCreated', [apiKeyName]) }}</p>
|
||||||
|
|
||||||
|
<p class="text-lg text-white mb-4">{{ $strings.LabelApiKeyCreatedDescription }}</p>
|
||||||
|
|
||||||
|
<ui-text-input label="API Key" :value="apiKeyKey" readonly show-copy />
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
|
<ui-btn color="bg-primary" @click="show = false">{{ $strings.ButtonClose }}</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean,
|
||||||
|
apiKey: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return this.$strings.HeaderNewApiKey
|
||||||
|
},
|
||||||
|
apiKeyName() {
|
||||||
|
return this.apiKey?.name || ''
|
||||||
|
},
|
||||||
|
apiKeyKey() {
|
||||||
|
return this.apiKey?.apiKey || ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
<template>
|
||||||
|
<modals-modal ref="modal" v-model="show" name="api-key" :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="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="min-height: 400px; max-height: 80vh">
|
||||||
|
<div class="w-full p-8">
|
||||||
|
<div class="flex py-2">
|
||||||
|
<div class="w-1/2 px-2">
|
||||||
|
<ui-text-input-with-label v-model.trim="newApiKey.name" :readonly="!isNew" :label="$strings.LabelName" />
|
||||||
|
</div>
|
||||||
|
<div v-if="isNew" class="w-1/2 px-2">
|
||||||
|
<ui-text-input-with-label v-model.trim="newApiKey.expiresIn" :label="$strings.LabelExpiresInSeconds" type="number" :min="0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center pt-4 pb-2 gap-2">
|
||||||
|
<div class="flex items-center px-2">
|
||||||
|
<p class="px-3 font-semibold" id="user-enabled-toggle">{{ $strings.LabelEnable }}</p>
|
||||||
|
<ui-toggle-switch :disabled="isExpired && !apiKey.isActive" labeledBy="user-enabled-toggle" v-model="newApiKey.isActive" />
|
||||||
|
</div>
|
||||||
|
<div v-if="isExpired" class="px-2">
|
||||||
|
<p class="text-sm text-error">{{ $strings.LabelExpired }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full border-t border-b border-black-200 py-4 px-3 mt-4">
|
||||||
|
<p class="text-lg mb-2 font-semibold">{{ $strings.LabelApiKeyUser }}</p>
|
||||||
|
<p class="text-sm mb-2 text-gray-400">{{ $strings.LabelApiKeyUserDescription }}</p>
|
||||||
|
<ui-select-input v-model="newApiKey.userId" :disabled="isExpired && !apiKey.isActive" :items="userItems" :placeholder="$strings.LabelSelectUser" :label="$strings.LabelApiKeyUser" label-hidden />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex pt-4 px-2">
|
||||||
|
<div class="grow" />
|
||||||
|
<ui-btn color="bg-success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean,
|
||||||
|
apiKey: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processing: false,
|
||||||
|
newApiKey: {},
|
||||||
|
isNew: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return this.isNew ? this.$strings.HeaderNewApiKey : this.$strings.HeaderUpdateApiKey
|
||||||
|
},
|
||||||
|
userItems() {
|
||||||
|
return this.users
|
||||||
|
.filter((u) => {
|
||||||
|
// Only show root user if the current user is root
|
||||||
|
return u.type !== 'root' || this.$store.getters['user/getIsRoot']
|
||||||
|
})
|
||||||
|
.map((u) => ({ text: u.username, value: u.id, subtext: u.type }))
|
||||||
|
},
|
||||||
|
isExpired() {
|
||||||
|
if (!this.apiKey || !this.apiKey.expiresAt) return false
|
||||||
|
|
||||||
|
return new Date(this.apiKey.expiresAt).getTime() < Date.now()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submitForm() {
|
||||||
|
if (!this.newApiKey.name) {
|
||||||
|
this.$toast.error(this.$strings.ToastNameRequired)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.newApiKey.userId) {
|
||||||
|
this.$toast.error(this.$strings.ToastNewApiKeyUserError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNew) {
|
||||||
|
this.submitCreateApiKey()
|
||||||
|
} else {
|
||||||
|
this.submitUpdateApiKey()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submitUpdateApiKey() {
|
||||||
|
if (this.newApiKey.isActive === this.apiKey.isActive && this.newApiKey.userId === this.apiKey.userId) {
|
||||||
|
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
|
this.show = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKey = {
|
||||||
|
isActive: this.newApiKey.isActive,
|
||||||
|
userId: this.newApiKey.userId
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processing = true
|
||||||
|
this.$axios
|
||||||
|
.$patch(`/api/api-keys/${this.apiKey.id}`, apiKey)
|
||||||
|
.then((data) => {
|
||||||
|
this.processing = false
|
||||||
|
if (data.error) {
|
||||||
|
this.$toast.error(`${this.$strings.ToastFailedToUpdate}: ${data.error}`)
|
||||||
|
} else {
|
||||||
|
this.show = false
|
||||||
|
this.$emit('updated', data.apiKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.processing = false
|
||||||
|
console.error('Failed to update apiKey', error)
|
||||||
|
var errMsg = error.response ? error.response.data || '' : ''
|
||||||
|
this.$toast.error(errMsg || this.$strings.ToastFailedToUpdate)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitCreateApiKey() {
|
||||||
|
const apiKey = { ...this.newApiKey }
|
||||||
|
|
||||||
|
if (this.newApiKey.expiresIn) {
|
||||||
|
apiKey.expiresIn = parseInt(this.newApiKey.expiresIn)
|
||||||
|
} else {
|
||||||
|
delete apiKey.expiresIn
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processing = true
|
||||||
|
this.$axios
|
||||||
|
.$post('/api/api-keys', apiKey)
|
||||||
|
.then((data) => {
|
||||||
|
this.processing = false
|
||||||
|
if (data.error) {
|
||||||
|
this.$toast.error(this.$strings.ToastFailedToCreate + ': ' + data.error)
|
||||||
|
} else {
|
||||||
|
this.show = false
|
||||||
|
this.$emit('created', data.apiKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.processing = false
|
||||||
|
console.error('Failed to create apiKey', error)
|
||||||
|
var errMsg = error.response ? error.response.data || '' : ''
|
||||||
|
this.$toast.error(errMsg || this.$strings.ToastFailedToCreate)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.isNew = !this.apiKey
|
||||||
|
|
||||||
|
if (this.apiKey) {
|
||||||
|
this.newApiKey = {
|
||||||
|
name: this.apiKey.name,
|
||||||
|
isActive: this.apiKey.isActive,
|
||||||
|
userId: this.apiKey.userId
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.newApiKey = {
|
||||||
|
name: null,
|
||||||
|
expiresIn: null,
|
||||||
|
isActive: true,
|
||||||
|
userId: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -79,10 +79,10 @@ export default {
|
|||||||
return !this.bookmarks.find((bm) => Math.abs(this.currentTime - bm.time) < 1)
|
return !this.bookmarks.find((bm) => Math.abs(this.currentTime - bm.time) < 1)
|
||||||
},
|
},
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.$store.state.serverSettings.dateFormat
|
return this.$store.getters['getServerSetting']('dateFormat')
|
||||||
},
|
},
|
||||||
timeFormat() {
|
timeFormat() {
|
||||||
return this.$store.state.serverSettings.timeFormat
|
return this.$store.getters['getServerSetting']('timeFormat')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<ui-text-input-with-label ref="sequenceInput" v-model="selectedSeries.sequence" :label="$strings.LabelSequence" />
|
<ui-text-input-with-label ref="sequenceInput" v-model="selectedSeries.sequence" :label="$strings.LabelSequence" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="error" class="text-error text-sm mt-2 p-1">{{ error }}</div>
|
||||||
<div class="flex justify-end mt-2 p-1">
|
<div class="flex justify-end mt-2 p-1">
|
||||||
<ui-btn type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
<ui-btn type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,12 +35,17 @@ export default {
|
|||||||
existingSeriesNames: {
|
existingSeriesNames: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
},
|
||||||
|
originalSeriesSequence: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
el: null,
|
el: null,
|
||||||
content: null
|
content: null,
|
||||||
|
error: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -85,10 +91,17 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
submitSeriesForm() {
|
submitSeriesForm() {
|
||||||
|
this.error = null
|
||||||
|
|
||||||
if (this.$refs.newSeriesSelect) {
|
if (this.$refs.newSeriesSelect) {
|
||||||
this.$refs.newSeriesSelect.blur()
|
this.$refs.newSeriesSelect.blur()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.selectedSeries.sequence !== this.originalSeriesSequence && this.selectedSeries.sequence.includes(' ')) {
|
||||||
|
this.error = this.$strings.MessageSeriesSequenceCannotContainSpaces
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.$emit('submit')
|
this.$emit('submit')
|
||||||
},
|
},
|
||||||
clickClose() {
|
clickClose() {
|
||||||
@@ -100,6 +113,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
setShow() {
|
setShow() {
|
||||||
|
this.error = null
|
||||||
if (!this.el || !this.content) {
|
if (!this.el || !this.content) {
|
||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="w-full md:w-1/3">
|
<div class="w-full md:w-1/3">
|
||||||
<p v-if="!isMediaItemShareSession" class="font-semibold uppercase text-xs text-gray-400 tracking-wide mb-2 mt-6 md:mt-0">{{ $strings.LabelUser }}</p>
|
<p v-if="!isMediaItemShareSession" class="font-semibold uppercase text-xs text-gray-400 tracking-wide mb-2 mt-6 md:mt-0">{{ $strings.LabelUser }}</p>
|
||||||
<p v-if="!isMediaItemShareSession" class="mb-1 text-xs">{{ _session.userId }}</p>
|
<p v-if="!isMediaItemShareSession" class="mb-1 text-xs">{{ username }}</p>
|
||||||
|
|
||||||
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">{{ $strings.LabelMediaPlayer }}</p>
|
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">{{ $strings.LabelMediaPlayer }}</p>
|
||||||
<p class="mb-1">{{ playMethodName }}</p>
|
<p class="mb-1">{{ playMethodName }}</p>
|
||||||
@@ -132,6 +132,9 @@ export default {
|
|||||||
_session() {
|
_session() {
|
||||||
return this.session || {}
|
return this.session || {}
|
||||||
},
|
},
|
||||||
|
username() {
|
||||||
|
return this._session.user?.username || this._session.userId || ''
|
||||||
|
},
|
||||||
deviceInfo() {
|
deviceInfo() {
|
||||||
return this._session.deviceInfo || {}
|
return this._session.deviceInfo || {}
|
||||||
},
|
},
|
||||||
@@ -159,10 +162,10 @@ export default {
|
|||||||
return 'Unknown'
|
return 'Unknown'
|
||||||
},
|
},
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.$store.state.serverSettings.dateFormat
|
return this.$store.getters['getServerSetting']('dateFormat')
|
||||||
},
|
},
|
||||||
timeFormat() {
|
timeFormat() {
|
||||||
return this.$store.state.serverSettings.timeFormat
|
return this.$store.getters['getServerSetting']('timeFormat')
|
||||||
},
|
},
|
||||||
isOpenSession() {
|
isOpenSession() {
|
||||||
return !!this._session.open
|
return !!this._session.open
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default {
|
|||||||
processing: Boolean,
|
processing: Boolean,
|
||||||
persistent: {
|
persistent: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: false
|
||||||
},
|
},
|
||||||
width: {
|
width: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
@@ -99,7 +99,7 @@ export default {
|
|||||||
this.preventClickoutside = false
|
this.preventClickoutside = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.processing && this.persistent) return
|
if (this.processing || this.persistent) return
|
||||||
if (ev.srcElement && ev.srcElement.classList.contains('modal-bg')) {
|
if (ev.srcElement && ev.srcElement.classList.contains('modal-bg')) {
|
||||||
this.show = false
|
this.show = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export default {
|
|||||||
expirationDateString() {
|
expirationDateString() {
|
||||||
if (!this.expireDurationSeconds) return this.$strings.LabelPermanent
|
if (!this.expireDurationSeconds) return this.$strings.LabelPermanent
|
||||||
const dateMs = Date.now() + this.expireDurationSeconds * 1000
|
const dateMs = Date.now() + this.expireDurationSeconds * 1000
|
||||||
return this.$formatDatetime(dateMs, this.$store.state.serverSettings.dateFormat, this.$store.state.serverSettings.timeFormat)
|
return this.$formatDatetime(dateMs, this.$store.getters['getServerSetting']('dateFormat'), this.$store.getters['getServerSetting']('timeFormat'))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.$store.state.serverSettings.dateFormat
|
return this.$store.getters['getServerSetting']('dateFormat')
|
||||||
},
|
},
|
||||||
releasesToShow() {
|
releasesToShow() {
|
||||||
return this.versionData?.releasesToShow || []
|
return this.versionData?.releasesToShow || []
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ export default {
|
|||||||
media() {
|
media() {
|
||||||
return this.libraryItem.media || {}
|
return this.libraryItem.media || {}
|
||||||
},
|
},
|
||||||
userToken() {
|
|
||||||
return this.$store.getters['user/getToken']
|
|
||||||
},
|
|
||||||
userCanUpdate() {
|
userCanUpdate() {
|
||||||
return this.$store.getters['user/getUserCanUpdate']
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -77,8 +77,8 @@
|
|||||||
<ui-checkbox v-model="selectedMatchUsage.author" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.author" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="grow ml-4">
|
<div class="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/60">
|
<p v-if="mediaMetadata.authorName || (isPodcast && mediaMetadata.author)" class="text-xs ml-1 text-white/60">
|
||||||
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" 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', isPodcast ? mediaMetadata.author : mediaMetadata.authorName)">{{ isPodcast ? mediaMetadata.author : mediaMetadata.authorName }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -400,7 +400,9 @@ export default {
|
|||||||
this.$toast.warning(this.$strings.ToastTitleRequired)
|
this.$toast.warning(this.$strings.ToastTitleRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.persistProvider()
|
if (!this.isPodcast) {
|
||||||
|
this.persistProvider()
|
||||||
|
}
|
||||||
this.runSearch()
|
this.runSearch()
|
||||||
},
|
},
|
||||||
async runSearch() {
|
async runSearch() {
|
||||||
|
|||||||
@@ -74,19 +74,12 @@ export default {
|
|||||||
mediaTracks() {
|
mediaTracks() {
|
||||||
return this.media.tracks || []
|
return this.media.tracks || []
|
||||||
},
|
},
|
||||||
isSingleM4b() {
|
|
||||||
return this.mediaTracks.length === 1 && this.mediaTracks[0].metadata.ext.toLowerCase() === '.m4b'
|
|
||||||
},
|
|
||||||
chapters() {
|
chapters() {
|
||||||
return this.media.chapters || []
|
return this.media.chapters || []
|
||||||
},
|
},
|
||||||
showM4bDownload() {
|
showM4bDownload() {
|
||||||
if (!this.mediaTracks.length) return false
|
if (!this.mediaTracks.length) return false
|
||||||
return !this.isSingleM4b
|
return true
|
||||||
},
|
|
||||||
showMp3Split() {
|
|
||||||
if (!this.mediaTracks.length) return false
|
|
||||||
return this.isSingleM4b && this.chapters.length
|
|
||||||
},
|
},
|
||||||
queuedEmbedLIds() {
|
queuedEmbedLIds() {
|
||||||
return this.$store.state.tasks.queuedEmbedLIds || []
|
return this.$store.state.tasks.queuedEmbedLIds || []
|
||||||
|
|||||||
@@ -10,6 +10,12 @@
|
|||||||
<form @submit.prevent="submit" class="flex grow">
|
<form @submit.prevent="submit" class="flex grow">
|
||||||
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="grow mr-2 text-sm md:text-base" />
|
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="grow mr-2 text-sm md:text-base" />
|
||||||
</form>
|
</form>
|
||||||
|
<ui-btn :padding-x="4" @click="toggleSort">
|
||||||
|
<span class="pr-4">{{ $strings.LabelSortPubDate }}</span>
|
||||||
|
<span class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
|
<span class="material-symbols text-xl" :aria-label="sortDescending ? $strings.LabelSortDescending : $strings.LabelSortAscending">{{ sortDescending ? 'expand_more' : 'expand_less' }}</span>
|
||||||
|
</span>
|
||||||
|
</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
<div ref="episodeContainer" id="episodes-scroll" class="w-full overflow-x-hidden overflow-y-auto">
|
<div ref="episodeContainer" id="episodes-scroll" class="w-full overflow-x-hidden overflow-y-auto">
|
||||||
<div v-for="(episode, index) in episodesList" :key="index" class="relative" :class="episode.isDownloaded || episode.isDownloading ? 'bg-primary/40' : selectedEpisodes[episode.cleanUrl] ? 'cursor-pointer bg-success/10' : index % 2 == 0 ? 'cursor-pointer bg-primary/25 hover:bg-primary/40' : 'cursor-pointer bg-primary/5 hover:bg-primary/25'" @click="toggleSelectEpisode(episode)">
|
<div v-for="(episode, index) in episodesList" :key="index" class="relative" :class="episode.isDownloaded || episode.isDownloading ? 'bg-primary/40' : selectedEpisodes[episode.cleanUrl] ? 'cursor-pointer bg-success/10' : index % 2 == 0 ? 'cursor-pointer bg-primary/25 hover:bg-primary/40' : 'cursor-pointer bg-primary/5 hover:bg-primary/25'" @click="toggleSelectEpisode(episode)">
|
||||||
@@ -29,7 +35,14 @@
|
|||||||
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
||||||
</div>
|
</div>
|
||||||
<p v-if="episode.subtitle" class="mb-1 text-sm text-gray-300 line-clamp-2">{{ episode.subtitle }}</p>
|
<p v-if="episode.subtitle" class="mb-1 text-sm text-gray-300 line-clamp-2">{{ episode.subtitle }}</p>
|
||||||
<p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
|
<div class="flex items-center space-x-2">
|
||||||
|
<!-- published -->
|
||||||
|
<p class="text-xs text-gray-300 w-40">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
|
||||||
|
<!-- duration -->
|
||||||
|
<p v-if="episode.durationSeconds && !isNaN(episode.durationSeconds)" class="text-xs text-gray-300 min-w-28">{{ $strings.LabelDuration }}: {{ $elapsedPretty(episode.durationSeconds) }}</p>
|
||||||
|
<!-- size -->
|
||||||
|
<p v-if="episode.enclosure?.length && !isNaN(episode.enclosure.length) && Number(episode.enclosure.length) > 0" class="text-xs text-gray-300">{{ $strings.LabelSize }}: {{ $bytesPretty(Number(episode.enclosure.length)) }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,7 +86,8 @@ export default {
|
|||||||
searchTimeout: null,
|
searchTimeout: null,
|
||||||
searchText: null,
|
searchText: null,
|
||||||
downloadedEpisodeGuidMap: {},
|
downloadedEpisodeGuidMap: {},
|
||||||
downloadedEpisodeUrlMap: {}
|
downloadedEpisodeUrlMap: {},
|
||||||
|
sortDescending: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -141,6 +155,17 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
toggleSort() {
|
||||||
|
this.sortDescending = !this.sortDescending
|
||||||
|
this.episodesCleaned = this.episodesCleaned.toSorted((a, b) => {
|
||||||
|
if (this.sortDescending) {
|
||||||
|
return a.publishedAt < b.publishedAt ? 1 : -1
|
||||||
|
}
|
||||||
|
return a.publishedAt > b.publishedAt ? 1 : -1
|
||||||
|
})
|
||||||
|
this.selectedEpisodes = {}
|
||||||
|
this.selectAll = false
|
||||||
|
},
|
||||||
getIsEpisodeDownloaded(episode) {
|
getIsEpisodeDownloaded(episode) {
|
||||||
if (episode.guid && !!this.downloadedEpisodeGuidMap[episode.guid]) {
|
if (episode.guid && !!this.downloadedEpisodeGuidMap[episode.guid]) {
|
||||||
return true
|
return true
|
||||||
@@ -226,8 +251,8 @@ export default {
|
|||||||
const sizeInMb = payloadSize / 1024 / 1024
|
const sizeInMb = payloadSize / 1024 / 1024
|
||||||
const sizeInMbPretty = sizeInMb.toFixed(2) + 'MB'
|
const sizeInMbPretty = sizeInMb.toFixed(2) + 'MB'
|
||||||
console.log('Request size', sizeInMb)
|
console.log('Request size', sizeInMb)
|
||||||
if (sizeInMb > 4.99) {
|
if (sizeInMb > 9.99) {
|
||||||
return this.$toast.error(`Request is too large (${sizeInMbPretty}) should be < 5Mb`)
|
return this.$toast.error(`Request is too large (${sizeInMbPretty}) should be < 10Mb`)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{{ $getString('MessageConfirmRemoveEpisode', [episodeTitle]) }}
|
{{ $getString('MessageConfirmRemoveEpisode', [episodeTitle]) }}
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="text-lg text-gray-200 mb-4">{{ $getString('MessageConfirmRemoveEpisodes', [episodes.length]) }}</p>
|
<p v-else class="text-lg text-gray-200 mb-4">{{ $getString('MessageConfirmRemoveEpisodes', [episodes.length]) }}</p>
|
||||||
<p class="text-xs font-semibold text-warning/90">Note: This does not delete the audio file unless toggling "Hard delete file"</p>
|
<p class="text-xs font-semibold text-warning/90">{{ $strings.MessageConfirmRemoveEpisodeNote }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center pt-4">
|
<div class="flex justify-between items-center pt-4">
|
||||||
<ui-checkbox v-model="hardDeleteFile" :label="$strings.LabelHardDeleteFile" check-color="error" checkbox-bg="bg" small label-class="text-base text-gray-200 pl-3" />
|
<ui-checkbox v-model="hardDeleteFile" :label="$strings.LabelHardDeleteFile" check-color="error" checkbox-bg="bg" small label-class="text-base text-gray-200 pl-3" />
|
||||||
@@ -94,7 +94,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.$toast.success(`${this.episodes.length} episode${this.episodes.length > 1 ? 's' : ''} removed`)
|
|
||||||
this.show = false
|
this.show = false
|
||||||
this.$emit('clearSelected')
|
this.$emit('clearSelected')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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 less-spacing" v-html="description" />
|
<div v-if="description" dir="auto" class="default-style less-spacing" @click="handleDescriptionClick" 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="w-full h-px bg-white/5 my-4" />
|
||||||
@@ -34,6 +34,12 @@
|
|||||||
{{ audioFileSize }}
|
{{ audioFileSize }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grow">
|
||||||
|
<p class="font-semibold text-xs mb-1">{{ $strings.LabelDuration }}</p>
|
||||||
|
<p class="mb-2 text-xs">
|
||||||
|
{{ audioFileDuration }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
@@ -68,7 +74,7 @@ export default {
|
|||||||
return this.episode.title || 'No Episode Title'
|
return this.episode.title || 'No Episode Title'
|
||||||
},
|
},
|
||||||
description() {
|
description() {
|
||||||
return this.episode.description || ''
|
return this.parseDescription(this.episode.description || '')
|
||||||
},
|
},
|
||||||
media() {
|
media() {
|
||||||
return this.libraryItem?.media || {}
|
return this.libraryItem?.media || {}
|
||||||
@@ -90,11 +96,49 @@ export default {
|
|||||||
|
|
||||||
return this.$bytesPretty(size)
|
return this.$bytesPretty(size)
|
||||||
},
|
},
|
||||||
|
audioFileDuration() {
|
||||||
|
const duration = this.episode.duration || 0
|
||||||
|
return this.$elapsedPretty(duration)
|
||||||
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
methods: {
|
||||||
|
handleDescriptionClick(e) {
|
||||||
|
if (e.target.matches('span.time-marker')) {
|
||||||
|
const time = parseInt(e.target.dataset.time)
|
||||||
|
if (!isNaN(time)) {
|
||||||
|
this.$eventBus.$emit('play-item', {
|
||||||
|
episodeId: this.episodeId,
|
||||||
|
libraryItemId: this.libraryItem.id,
|
||||||
|
startTime: time
|
||||||
|
})
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseDescription(description) {
|
||||||
|
const timeMarkerLinkRegex = /<a href="#([^"]*?\b\d{1,2}:\d{1,2}(?::\d{1,2})?)">(.*?)<\/a>/g
|
||||||
|
const timeMarkerRegex = /\b\d{1,2}:\d{1,2}(?::\d{1,2})?\b/g
|
||||||
|
|
||||||
|
function convertToSeconds(time) {
|
||||||
|
const timeParts = time.split(':').map(Number)
|
||||||
|
return timeParts.reduce((acc, part, index) => acc * 60 + part, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return description
|
||||||
|
.replace(timeMarkerLinkRegex, (match, href, displayTime) => {
|
||||||
|
const time = displayTime.match(timeMarkerRegex)[0]
|
||||||
|
const seekTimeInSeconds = convertToSeconds(time)
|
||||||
|
return `<span class="time-marker cursor-pointer text-blue-400 hover:text-blue-300" data-time="${seekTimeInSeconds}">${displayTime}</span>`
|
||||||
|
})
|
||||||
|
.replace(timeMarkerRegex, (match) => {
|
||||||
|
const seekTimeInSeconds = convertToSeconds(match)
|
||||||
|
return `<span class="time-marker cursor-pointer text-blue-400 hover:text-blue-300" data-time="${seekTimeInSeconds}">${match}</span>`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export default {
|
|||||||
.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatePayload)
|
.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatePayload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
this.$toast.success('Podcast episode updated')
|
this.$toast.success(this.$strings.ToastPodcastEpisodeUpdated)
|
||||||
this.$emit('selectTab', 'details')
|
this.$emit('selectTab', 'details')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ export default {
|
|||||||
currentChapterStart() {
|
currentChapterStart() {
|
||||||
if (!this.currentChapter) return 0
|
if (!this.currentChapter) return 0
|
||||||
return this.currentChapter.start
|
return this.currentChapter.start
|
||||||
|
},
|
||||||
|
isMobile() {
|
||||||
|
return this.$store.state.globals.isMobile
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -145,6 +148,9 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
mousemoveTrack(e) {
|
mousemoveTrack(e) {
|
||||||
|
if (this.isMobile) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const offsetX = e.offsetX
|
const offsetX = e.offsetX
|
||||||
|
|
||||||
const baseTime = this.useChapterTrack ? this.currentChapterStart : 0
|
const baseTime = this.useChapterTrack ? this.currentChapterStart : 0
|
||||||
@@ -198,6 +204,7 @@ export default {
|
|||||||
setTrackWidth() {
|
setTrackWidth() {
|
||||||
if (this.$refs.track) {
|
if (this.$refs.track) {
|
||||||
this.trackWidth = this.$refs.track.clientWidth
|
this.trackWidth = this.$refs.track.clientWidth
|
||||||
|
this.trackOffsetLeft = this.$refs.track.getBoundingClientRect().left
|
||||||
} else {
|
} else {
|
||||||
console.error('Track not loaded', this.$refs)
|
console.error('Track not loaded', this.$refs)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,9 +129,6 @@ export default {
|
|||||||
return `${hoursRounded}h`
|
return `${hoursRounded}h`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
token() {
|
|
||||||
return this.$store.getters['user/getToken']
|
|
||||||
},
|
|
||||||
timeRemaining() {
|
timeRemaining() {
|
||||||
if (this.useChapterTrack && this.currentChapter) {
|
if (this.useChapterTrack && this.currentChapter) {
|
||||||
var currChapTime = this.currentTime - this.currentChapter.start
|
var currChapTime = this.currentTime - this.currentChapter.start
|
||||||
|
|||||||
@@ -104,9 +104,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
userToken() {
|
|
||||||
return this.$store.getters['user/getToken']
|
|
||||||
},
|
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem?.id
|
return this.libraryItem?.id
|
||||||
},
|
},
|
||||||
@@ -234,10 +231,7 @@ export default {
|
|||||||
async extract() {
|
async extract() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
var buff = await this.$axios.$get(this.ebookUrl, {
|
var buff = await this.$axios.$get(this.ebookUrl, {
|
||||||
responseType: 'blob',
|
responseType: 'blob'
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${this.userToken}`
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
const archive = await Archive.open(buff)
|
const archive = await Archive.open(buff)
|
||||||
const originalFilesObject = await archive.getFilesObject()
|
const originalFilesObject = await archive.getFilesObject()
|
||||||
|
|||||||
@@ -57,9 +57,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
userToken() {
|
|
||||||
return this.$store.getters['user/getToken']
|
|
||||||
},
|
|
||||||
/** @returns {string} */
|
/** @returns {string} */
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem?.id
|
return this.libraryItem?.id
|
||||||
@@ -97,27 +94,37 @@ export default {
|
|||||||
},
|
},
|
||||||
ebookUrl() {
|
ebookUrl() {
|
||||||
if (this.fileId) {
|
if (this.fileId) {
|
||||||
return `${this.$config.routerBasePath}/api/items/${this.libraryItemId}/ebook/${this.fileId}`
|
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
|
||||||
}
|
}
|
||||||
return `${this.$config.routerBasePath}/api/items/${this.libraryItemId}/ebook`
|
return `/api/items/${this.libraryItemId}/ebook`
|
||||||
},
|
},
|
||||||
themeRules() {
|
themeRules() {
|
||||||
const isDark = this.ereaderSettings.theme === 'dark'
|
const theme = this.ereaderSettings.theme
|
||||||
const fontColor = isDark ? '#fff' : '#000'
|
const isDark = theme === 'dark'
|
||||||
const backgroundColor = isDark ? 'rgb(35 35 35)' : 'rgb(255, 255, 255)'
|
const isSepia = theme === 'sepia'
|
||||||
|
|
||||||
|
const fontColor = isDark
|
||||||
|
? '#fff'
|
||||||
|
: isSepia
|
||||||
|
? '#5b4636'
|
||||||
|
: '#000'
|
||||||
|
|
||||||
|
const backgroundColor = isDark
|
||||||
|
? 'rgb(35 35 35)'
|
||||||
|
: isSepia
|
||||||
|
? 'rgb(244, 236, 216)'
|
||||||
|
: 'rgb(255, 255, 255)'
|
||||||
|
|
||||||
const lineSpacing = this.ereaderSettings.lineSpacing / 100
|
const lineSpacing = this.ereaderSettings.lineSpacing / 100
|
||||||
|
const fontScale = this.ereaderSettings.fontScale / 100
|
||||||
const fontScale = this.ereaderSettings.fontScale / 100
|
const textStroke = this.ereaderSettings.textStroke / 100
|
||||||
|
|
||||||
const textStroke = this.ereaderSettings.textStroke / 100
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'*': {
|
'*': {
|
||||||
color: `${fontColor}!important`,
|
color: `${fontColor}!important`,
|
||||||
'background-color': `${backgroundColor}!important`,
|
'background-color': `${backgroundColor}!important`,
|
||||||
'line-height': lineSpacing * fontScale + 'rem!important',
|
'line-height': `${lineSpacing * fontScale}rem!important`,
|
||||||
'-webkit-text-stroke': textStroke + 'px ' + fontColor + '!important'
|
'-webkit-text-stroke': `${textStroke}px ${fontColor}!important`
|
||||||
},
|
},
|
||||||
a: {
|
a: {
|
||||||
color: `${fontColor}!important`
|
color: `${fontColor}!important`
|
||||||
@@ -309,14 +316,24 @@ export default {
|
|||||||
/** @type {EpubReader} */
|
/** @type {EpubReader} */
|
||||||
const reader = this
|
const reader = this
|
||||||
|
|
||||||
|
// Use axios to make request because we have token refresh logic in interceptor
|
||||||
|
const customRequest = async (url) => {
|
||||||
|
try {
|
||||||
|
return this.$axios.$get(url, {
|
||||||
|
responseType: 'arraybuffer'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('EpubReader.initEpub customRequest failed:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {ePub.Book} */
|
/** @type {ePub.Book} */
|
||||||
reader.book = new ePub(reader.ebookUrl, {
|
reader.book = new ePub(reader.ebookUrl, {
|
||||||
width: this.readerWidth,
|
width: this.readerWidth,
|
||||||
height: this.readerHeight - 50,
|
height: this.readerHeight - 50,
|
||||||
openAs: 'epub',
|
openAs: 'epub',
|
||||||
requestHeaders: {
|
requestMethod: customRequest
|
||||||
Authorization: `Bearer ${this.userToken}`
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/** @type {ePub.Rendition} */
|
/** @type {ePub.Rendition} */
|
||||||
@@ -337,29 +354,33 @@ export default {
|
|||||||
this.applyTheme()
|
this.applyTheme()
|
||||||
})
|
})
|
||||||
|
|
||||||
reader.book.ready.then(() => {
|
reader.book.ready
|
||||||
// set up event listeners
|
.then(() => {
|
||||||
reader.rendition.on('relocated', reader.relocated)
|
// set up event listeners
|
||||||
reader.rendition.on('keydown', reader.keyUp)
|
reader.rendition.on('relocated', reader.relocated)
|
||||||
|
reader.rendition.on('keydown', reader.keyUp)
|
||||||
|
|
||||||
reader.rendition.on('touchstart', (event) => {
|
reader.rendition.on('touchstart', (event) => {
|
||||||
this.$emit('touchstart', event)
|
this.$emit('touchstart', event)
|
||||||
})
|
|
||||||
reader.rendition.on('touchend', (event) => {
|
|
||||||
this.$emit('touchend', event)
|
|
||||||
})
|
|
||||||
|
|
||||||
// load ebook cfi locations
|
|
||||||
const savedLocations = this.loadLocations()
|
|
||||||
if (savedLocations) {
|
|
||||||
reader.book.locations.load(savedLocations)
|
|
||||||
} else {
|
|
||||||
reader.book.locations.generate().then(() => {
|
|
||||||
this.checkSaveLocations(reader.book.locations.save())
|
|
||||||
})
|
})
|
||||||
}
|
reader.rendition.on('touchend', (event) => {
|
||||||
this.getChapters()
|
this.$emit('touchend', event)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// load ebook cfi locations
|
||||||
|
const savedLocations = this.loadLocations()
|
||||||
|
if (savedLocations) {
|
||||||
|
reader.book.locations.load(savedLocations)
|
||||||
|
} else {
|
||||||
|
reader.book.locations.generate().then(() => {
|
||||||
|
this.checkSaveLocations(reader.book.locations.save())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.getChapters()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('EpubReader.initEpub failed:', error)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
getChapters() {
|
getChapters() {
|
||||||
// Load the list of chapters in the book. See https://github.com/futurepress/epub.js/issues/759
|
// Load the list of chapters in the book. See https://github.com/futurepress/epub.js/issues/759
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ export default {
|
|||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
userToken() {
|
|
||||||
return this.$store.getters['user/getToken']
|
|
||||||
},
|
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem?.id
|
return this.libraryItem?.id
|
||||||
},
|
},
|
||||||
@@ -96,11 +93,8 @@ export default {
|
|||||||
},
|
},
|
||||||
async initMobi() {
|
async initMobi() {
|
||||||
// Fetch mobi file as blob
|
// Fetch mobi file as blob
|
||||||
var buff = await this.$axios.$get(this.ebookUrl, {
|
const buff = await this.$axios.$get(this.ebookUrl, {
|
||||||
responseType: 'blob',
|
responseType: 'blob'
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${this.userToken}`
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
var reader = new FileReader()
|
var reader = new FileReader()
|
||||||
reader.onload = async (event) => {
|
reader.onload = async (event) => {
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ export default {
|
|||||||
loadedRatio: 0,
|
loadedRatio: 0,
|
||||||
page: 1,
|
page: 1,
|
||||||
numPages: 0,
|
numPages: 0,
|
||||||
pdfDocInitParams: null
|
pdfDocInitParams: null,
|
||||||
|
isRefreshing: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -152,7 +153,34 @@ export default {
|
|||||||
this.page++
|
this.page++
|
||||||
this.updateProgress()
|
this.updateProgress()
|
||||||
},
|
},
|
||||||
error(err) {
|
async refreshToken() {
|
||||||
|
if (this.isRefreshing) return
|
||||||
|
this.isRefreshing = true
|
||||||
|
const newAccessToken = await this.$store.dispatch('user/refreshToken').catch((error) => {
|
||||||
|
console.error('Failed to refresh token', error)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
if (!newAccessToken) {
|
||||||
|
// Redirect to login on failed refresh
|
||||||
|
this.$router.push('/login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force Vue to re-render the PDF component by creating a new object
|
||||||
|
this.pdfDocInitParams = {
|
||||||
|
url: this.ebookUrl,
|
||||||
|
httpHeaders: {
|
||||||
|
Authorization: `Bearer ${newAccessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.isRefreshing = false
|
||||||
|
},
|
||||||
|
async error(err) {
|
||||||
|
if (err && err.status === 401) {
|
||||||
|
console.log('Received 401 error, refreshing token')
|
||||||
|
await this.refreshToken()
|
||||||
|
return
|
||||||
|
}
|
||||||
console.error(err)
|
console.error(err)
|
||||||
},
|
},
|
||||||
resize() {
|
resize() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="show" id="reader" :data-theme="ereaderTheme" class="group absolute top-0 left-0 w-full z-60 data-[theme=dark]:bg-primary data-[theme=dark]:text-white data-[theme=light]:bg-white data-[theme=light]:text-black" :class="{ 'reader-player-open': !!streamLibraryItem }">
|
<div v-if="show" id="reader" :data-theme="ereaderTheme" class="group absolute top-0 left-0 w-full z-60 data-[theme=dark]:bg-primary data-[theme=dark]:text-white data-[theme=light]:bg-white data-[theme=light]:text-black data-[theme=sepia]:bg-[rgb(244,236,216)] data-[theme=sepia]:text-[#5b4636]" :class="{ 'reader-player-open': !!streamLibraryItem }">
|
||||||
<div class="absolute top-4 left-4 z-20 flex items-center">
|
<div class="absolute top-4 left-4 z-20 flex items-center">
|
||||||
<button v-if="isEpub" @click="toggleToC" type="button" aria-label="Table of contents menu" class="inline-flex opacity-80 hover:opacity-100">
|
<button v-if="isEpub" @click="toggleToC" type="button" aria-label="Table of contents menu" class="inline-flex opacity-80 hover:opacity-100">
|
||||||
<span class="material-symbols text-2xl">menu</span>
|
<span class="material-symbols text-2xl">menu</span>
|
||||||
@@ -27,7 +27,12 @@
|
|||||||
|
|
||||||
<!-- TOC side nav -->
|
<!-- TOC side nav -->
|
||||||
<div v-if="tocOpen" class="w-full h-full overflow-y-scroll absolute inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div>
|
<div v-if="tocOpen" class="w-full h-full overflow-y-scroll absolute inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div>
|
||||||
<div v-if="isEpub" class="w-96 h-full max-h-full absolute top-0 left-0 shadow-xl transition-transform z-30 group-data-[theme=dark]:bg-primary group-data-[theme=dark]:text-white group-data-[theme=light]:bg-white group-data-[theme=light]:text-black" :class="tocOpen ? 'translate-x-0' : '-translate-x-96'" @click.stop.prevent>
|
<div
|
||||||
|
v-if="isEpub"
|
||||||
|
class="w-96 h-full max-h-full absolute top-0 left-0 shadow-xl transition-transform z-30 group-data-[theme=dark]:bg-primary group-data-[theme=dark]:text-white group-data-[theme=light]:bg-white group-data-[theme=light]:text-black group-data-[theme=sepia]:bg-[rgb(244,236,216)] group-data-[theme=sepia]:text-[#5b4636]"
|
||||||
|
:class="tocOpen ? 'translate-x-0' : '-translate-x-96'"
|
||||||
|
@click.stop.prevent
|
||||||
|
>
|
||||||
<div class="flex flex-col p-4 h-full">
|
<div class="flex flex-col p-4 h-full">
|
||||||
<div class="flex items-center mb-2">
|
<div class="flex items-center mb-2">
|
||||||
<button @click.stop.prevent="toggleToC" type="button" aria-label="Close table of contents" class="inline-flex opacity-80 hover:opacity-100">
|
<button @click.stop.prevent="toggleToC" type="button" aria-label="Close table of contents" class="inline-flex opacity-80 hover:opacity-100">
|
||||||
@@ -37,7 +42,7 @@
|
|||||||
<p class="text-lg font-semibold ml-2">{{ $strings.HeaderTableOfContents }}</p>
|
<p class="text-lg font-semibold ml-2">{{ $strings.HeaderTableOfContents }}</p>
|
||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="searchBook" @click.stop.prevent>
|
<form @submit.prevent="searchBook" @click.stop.prevent>
|
||||||
<ui-text-input clearable ref="input" @clear="searchBook" v-model="searchQuery" :placeholder="$strings.PlaceholderSearch" class="h-8 w-full text-sm flex mb-2" />
|
<ui-text-input clearable ref="input" @clear="searchBook" v-model="searchQuery" :placeholder="$strings.PlaceholderSearch" custom-input-class="text-inherit !bg-inherit" class="h-8 w-full text-sm flex mb-2" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="overflow-y-auto">
|
<div class="overflow-y-auto">
|
||||||
@@ -181,6 +186,10 @@ export default {
|
|||||||
text: this.$strings.LabelThemeDark,
|
text: this.$strings.LabelThemeDark,
|
||||||
value: 'dark'
|
value: 'dark'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelThemeSepia,
|
||||||
|
value: 'sepia'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.LabelThemeLight,
|
text: this.$strings.LabelThemeLight,
|
||||||
value: 'light'
|
value: 'light'
|
||||||
@@ -266,9 +275,6 @@ export default {
|
|||||||
isComic() {
|
isComic() {
|
||||||
return this.ebookFormat == 'cbz' || this.ebookFormat == 'cbr'
|
return this.ebookFormat == 'cbz' || this.ebookFormat == 'cbr'
|
||||||
},
|
},
|
||||||
userToken() {
|
|
||||||
return this.$store.getters['user/getToken']
|
|
||||||
},
|
|
||||||
keepProgress() {
|
keepProgress() {
|
||||||
return this.$store.state.ereaderKeepProgress
|
return this.$store.state.ereaderKeepProgress
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ export default {
|
|||||||
|
|
||||||
this.showingTooltipIndex = index
|
this.showingTooltipIndex = index
|
||||||
this.tooltipEl.style.display = 'block'
|
this.tooltipEl.style.display = 'block'
|
||||||
this.tooltipTextEl.innerHTML = block.value ? `<strong>${this.$elapsedPretty(block.value, true)} listening</strong> on ${block.datePretty}` : `No listening sessions on ${block.datePretty}`
|
this.tooltipTextEl.innerHTML = block.value ? this.$getString('MessageHeatmapListeningTimeTooltip', [this.$elapsedPrettyLocalized(block.value, true), block.datePretty]) : this.$getString('MessageHeatmapNoListeningSessions', [block.datePretty])
|
||||||
|
|
||||||
const calculateRect = this.tooltipEl.getBoundingClientRect()
|
const calculateRect = this.tooltipEl.getBoundingClientRect()
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap justify-center mt-6">
|
<div class="flex flex-wrap justify-center mt-6">
|
||||||
<div class="flex p-2">
|
<div class="flex p-2">
|
||||||
<svg class="h-14 w-14" viewBox="0 0 24 24">
|
<span class="material-symbols text-5xl py-1">newsstand</span>
|
||||||
<path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" />
|
|
||||||
</svg>
|
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalItems) }}</p>
|
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalItems) }}</p>
|
||||||
<p class="text-xs md:text-sm text-white/80">{{ $strings.LabelStatsItemsInLibrary }}</p>
|
<p class="text-xs md:text-sm text-white/80">{{ $strings.LabelStatsItemsInLibrary }}</p>
|
||||||
@@ -19,9 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isBookLibrary" class="flex p-2">
|
<div v-if="isBookLibrary" class="flex p-2">
|
||||||
<svg class="h-14 w-14" viewBox="0 0 24 24">
|
<span class="material-symbols text-5xl py-1">person</span>
|
||||||
<path fill="currentColor" d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" />
|
|
||||||
</svg>
|
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalAuthors) }}</p>
|
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalAuthors) }}</p>
|
||||||
<p class="text-xs md:text-sm text-white/80">{{ $strings.LabelStatsAuthors }}</p>
|
<p class="text-xs md:text-sm text-white/80">{{ $strings.LabelStatsAuthors }}</p>
|
||||||
|
|||||||
@@ -164,14 +164,15 @@ export default {
|
|||||||
beforeMount() {
|
beforeMount() {
|
||||||
this.yearInReviewYear = new Date().getFullYear()
|
this.yearInReviewYear = new Date().getFullYear()
|
||||||
|
|
||||||
// When not December show previous year
|
this.availableYears = this.getAvailableYears()
|
||||||
if (new Date().getMonth() < 11) {
|
const availableYearValues = this.availableYears.map((y) => y.value)
|
||||||
|
|
||||||
|
// When not December show previous year if data is available
|
||||||
|
if (new Date().getMonth() < 11 && availableYearValues.includes(this.yearInReviewYear - 1)) {
|
||||||
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 {
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="text-center">
|
||||||
|
<table v-if="apiKeys.length > 0" id="api-keys">
|
||||||
|
<tr>
|
||||||
|
<th>{{ $strings.LabelName }}</th>
|
||||||
|
<th class="w-44">{{ $strings.LabelApiKeyUser }}</th>
|
||||||
|
<th class="w-32">{{ $strings.LabelExpiresAt }}</th>
|
||||||
|
<th class="w-32">{{ $strings.LabelCreatedAt }}</th>
|
||||||
|
<th class="w-32"></th>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="apiKey in apiKeys" :key="apiKey.id" :class="apiKey.isActive ? '' : 'bg-error/10!'">
|
||||||
|
<td>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<p class="pl-2 truncate">{{ apiKey.name }}</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-xs">
|
||||||
|
<nuxt-link v-if="apiKey.user" :to="`/config/users/${apiKey.user.id}`" class="text-xs hover:underline">
|
||||||
|
{{ apiKey.user.username }}
|
||||||
|
</nuxt-link>
|
||||||
|
<p v-else class="text-xs">Error</p>
|
||||||
|
</td>
|
||||||
|
<td class="text-xs">
|
||||||
|
<p v-if="apiKey.expiresAt" class="text-xs" :title="apiKey.expiresAt">{{ getExpiresAtText(apiKey) }}</p>
|
||||||
|
<p v-else class="text-xs">{{ $strings.LabelExpiresNever }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="text-xs font-mono">
|
||||||
|
<ui-tooltip direction="top" :text="$formatJsDatetime(new Date(apiKey.createdAt), dateFormat, timeFormat)">
|
||||||
|
{{ $formatJsDate(new Date(apiKey.createdAt), dateFormat) }}
|
||||||
|
</ui-tooltip>
|
||||||
|
</td>
|
||||||
|
<td class="py-0">
|
||||||
|
<div class="w-full flex justify-left">
|
||||||
|
<div class="h-8 w-8 flex items-center justify-center text-white/50 hover:text-white/100 cursor-pointer" @click.stop="editApiKey(apiKey)">
|
||||||
|
<button type="button" :aria-label="$strings.ButtonEdit" class="material-symbols text-base">edit</button>
|
||||||
|
</div>
|
||||||
|
<div class="h-8 w-8 flex items-center justify-center text-white/50 hover:text-error cursor-pointer" @click.stop="deleteApiKeyClick(apiKey)">
|
||||||
|
<button type="button" :aria-label="$strings.ButtonDelete" class="material-symbols text-base">delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p v-else class="text-base text-gray-300 py-4">{{ $strings.LabelNoApiKeys }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
apiKeys: [],
|
||||||
|
isDeletingApiKey: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.$store.state.serverSettings.timeFormat
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getExpiresAtText(apiKey) {
|
||||||
|
if (new Date(apiKey.expiresAt).getTime() < Date.now()) {
|
||||||
|
return this.$strings.LabelExpired
|
||||||
|
}
|
||||||
|
return this.$formatJsDatetime(new Date(apiKey.expiresAt), this.dateFormat, this.timeFormat)
|
||||||
|
},
|
||||||
|
deleteApiKeyClick(apiKey) {
|
||||||
|
if (this.isDeletingApiKey) return
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
message: this.$getString('MessageConfirmDeleteApiKey', [apiKey.name]),
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.deleteApiKey(apiKey)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
deleteApiKey(apiKey) {
|
||||||
|
this.isDeletingApiKey = true
|
||||||
|
this.$axios
|
||||||
|
.$delete(`/api/api-keys/${apiKey.id}`)
|
||||||
|
.then((data) => {
|
||||||
|
if (data.error) {
|
||||||
|
this.$toast.error(data.error)
|
||||||
|
} else {
|
||||||
|
this.removeApiKey(apiKey.id)
|
||||||
|
this.$emit('numApiKeys', this.apiKeys.length)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to delete apiKey', error)
|
||||||
|
this.$toast.error(this.$strings.ToastFailedToDelete)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.isDeletingApiKey = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
editApiKey(apiKey) {
|
||||||
|
this.$emit('edit', apiKey)
|
||||||
|
},
|
||||||
|
addApiKey(apiKey) {
|
||||||
|
this.apiKeys.push(apiKey)
|
||||||
|
},
|
||||||
|
removeApiKey(apiKeyId) {
|
||||||
|
this.apiKeys = this.apiKeys.filter((a) => a.id !== apiKeyId)
|
||||||
|
},
|
||||||
|
updateApiKey(apiKey) {
|
||||||
|
this.apiKeys = this.apiKeys.map((a) => (a.id === apiKey.id ? apiKey : a))
|
||||||
|
},
|
||||||
|
loadApiKeys() {
|
||||||
|
this.$axios
|
||||||
|
.$get('/api/api-keys')
|
||||||
|
.then((res) => {
|
||||||
|
this.apiKeys = res.apiKeys.sort((a, b) => {
|
||||||
|
return a.createdAt - b.createdAt
|
||||||
|
})
|
||||||
|
this.$emit('numApiKeys', this.apiKeys.length)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to load apiKeys', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadApiKeys()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#api-keys {
|
||||||
|
table-layout: fixed;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid #474747;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#api-keys td,
|
||||||
|
#api-keys th {
|
||||||
|
/* border: 1px solid #2e2e2e; */
|
||||||
|
padding: 8px 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#api-keys td.py-0 {
|
||||||
|
padding: 0px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#api-keys tr:nth-child(even) {
|
||||||
|
background-color: #373838;
|
||||||
|
}
|
||||||
|
|
||||||
|
#api-keys tr:nth-child(odd) {
|
||||||
|
background-color: #2f2f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
#api-keys tr:hover {
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
#api-keys th {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
background-color: #272727;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -78,10 +78,10 @@ export default {
|
|||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
},
|
},
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.$store.state.serverSettings.dateFormat
|
return this.$store.getters['getServerSetting']('dateFormat')
|
||||||
},
|
},
|
||||||
timeFormat() {
|
timeFormat() {
|
||||||
return this.$store.state.serverSettings.timeFormat
|
return this.$store.getters['getServerSetting']('timeFormat')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -49,9 +49,6 @@ export default {
|
|||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem.id
|
return this.libraryItem.id
|
||||||
},
|
},
|
||||||
userToken() {
|
|
||||||
return this.$store.getters['user/getToken']
|
|
||||||
},
|
|
||||||
userCanDownload() {
|
userCanDownload() {
|
||||||
return this.$store.getters['user/getUserCanDownload']
|
return this.$store.getters['user/getUserCanDownload']
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,9 +53,6 @@ export default {
|
|||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem.id
|
return this.libraryItem.id
|
||||||
},
|
},
|
||||||
userToken() {
|
|
||||||
return this.$store.getters['user/getToken']
|
|
||||||
},
|
|
||||||
userCanDownload() {
|
userCanDownload() {
|
||||||
return this.$store.getters['user/getUserCanDownload']
|
return this.$store.getters['user/getUserCanDownload']
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -76,10 +76,10 @@ export default {
|
|||||||
return usermap
|
return usermap
|
||||||
},
|
},
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.$store.state.serverSettings.dateFormat
|
return this.$store.getters['getServerSetting']('dateFormat')
|
||||||
},
|
},
|
||||||
timeFormat() {
|
timeFormat() {
|
||||||
return this.$store.state.serverSettings.timeFormat
|
return this.$store.getters['getServerSetting']('timeFormat')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export default {
|
|||||||
return this.episode?.publishedAt
|
return this.episode?.publishedAt
|
||||||
},
|
},
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.store.state.serverSettings.dateFormat
|
return this.store.getters['getServerSetting']('dateFormat')
|
||||||
},
|
},
|
||||||
itemProgress() {
|
itemProgress() {
|
||||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episodeId)
|
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episodeId)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="lazy-episodes-table" class="w-full py-6">
|
<div id="lazy-episodes-table" class="w-full py-6">
|
||||||
<div class="flex flex-wrap flex-col md:flex-row md:items-center mb-4">
|
<div class="flex flex-wrap flex-col md:flex-row md:items-center mb-4">
|
||||||
@@ -176,6 +175,13 @@ export default {
|
|||||||
return episodeProgress && !episodeProgress.isFinished
|
return episodeProgress && !episodeProgress.isFinished
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
|
// Swap values if sort descending
|
||||||
|
if (this.sortDesc) {
|
||||||
|
const temp = a
|
||||||
|
a = b
|
||||||
|
b = temp
|
||||||
|
}
|
||||||
|
|
||||||
let aValue
|
let aValue
|
||||||
let bValue
|
let bValue
|
||||||
|
|
||||||
@@ -194,10 +200,23 @@ export default {
|
|||||||
if (!bValue) bValue = Number.MAX_VALUE
|
if (!bValue) bValue = Number.MAX_VALUE
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.sortDesc) {
|
const primaryCompare = String(aValue).localeCompare(String(bValue), undefined, { numeric: true, sensitivity: 'base' })
|
||||||
return String(bValue).localeCompare(String(aValue), undefined, { numeric: true, sensitivity: 'base' })
|
if (primaryCompare !== 0 || this.sortKey === 'publishedAt') return primaryCompare
|
||||||
|
|
||||||
|
// When sorting by season, secondary sort is by episode number
|
||||||
|
if (this.sortKey === 'season') {
|
||||||
|
const aEpisode = a.episode || ''
|
||||||
|
const bEpisode = b.episode || ''
|
||||||
|
|
||||||
|
const secondaryCompare = String(aEpisode).localeCompare(String(bEpisode), undefined, { numeric: true, sensitivity: 'base' })
|
||||||
|
if (secondaryCompare !== 0) return secondaryCompare
|
||||||
}
|
}
|
||||||
return String(aValue).localeCompare(String(bValue), undefined, { numeric: true, sensitivity: 'base' })
|
|
||||||
|
// Final sort by publishedAt
|
||||||
|
let aPubDate = a.publishedAt || Number.MAX_VALUE
|
||||||
|
let bPubDate = b.publishedAt || Number.MAX_VALUE
|
||||||
|
|
||||||
|
return String(aPubDate).localeCompare(String(bPubDate), undefined, { numeric: true, sensitivity: 'base' })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
episodesList() {
|
episodesList() {
|
||||||
@@ -220,10 +239,10 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.$store.state.serverSettings.dateFormat
|
return this.$store.getters['getServerSetting']('dateFormat')
|
||||||
},
|
},
|
||||||
timeFormat() {
|
timeFormat() {
|
||||||
return this.$store.state.serverSettings.timeFormat
|
return this.$store.getters['getServerSetting']('timeFormat')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -85,9 +85,6 @@ export default {
|
|||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
userToken() {
|
|
||||||
return this.$store.getters['user/getToken']
|
|
||||||
},
|
|
||||||
wrapperClass() {
|
wrapperClass() {
|
||||||
var classes = []
|
var classes = []
|
||||||
if (this.disabled) classes.push('bg-black-300')
|
if (this.disabled) classes.push('bg-black-300')
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<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">
|
<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 relative">
|
||||||
<svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
|
<span v-if="isRead" class="material-symbols fill text-xl text-success">beenhere</span>
|
||||||
<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" />
|
<span v-else class="material-symbols text-xl text-white">beenhere</span>
|
||||||
</svg>
|
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<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-7 19.6l-7-4.66V3h14v12.93l-7 4.67zm-2.01-7.42l-2.58-2.59L6 12l4 4 8-8-1.42-1.42z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative w-full">
|
<div class="relative w-full">
|
||||||
<p v-if="label" class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
<p v-if="label && !labelHidden" class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
||||||
<button ref="buttonWrapper" type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded-sm shadow-xs pl-3 pr-8 py-2 text-left sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
<button ref="buttonWrapper" type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded-sm shadow-xs pl-3 pr-8 py-2 text-left sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @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, 'text-gray-400': !selectedText }">{{ selectedText || placeholder }}</span>
|
||||||
<span v-if="selectedSubtext">: </span>
|
<span v-if="selectedSubtext">: </span>
|
||||||
<span v-if="selectedSubtext" class="font-normal block truncate font-sans text-sm text-gray-400">{{ selectedSubtext }}</span>
|
<span v-if="selectedSubtext" class="font-normal block truncate font-sans text-sm text-gray-400">{{ selectedSubtext }}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -36,10 +36,15 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
labelHidden: Boolean,
|
||||||
items: {
|
items: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
small: Boolean,
|
small: Boolean,
|
||||||
menuMaxHeight: {
|
menuMaxHeight: {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
|
<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
|
||||||
</label>
|
</label>
|
||||||
</slot>
|
</slot>
|
||||||
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :show-copy="showCopy" class="w-full" :class="inputClass" :trim-whitespace="trimWhitespace" @blur="inputBlurred" />
|
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :min="min" :show-copy="showCopy" class="w-full" :class="inputClass" :trim-whitespace="trimWhitespace" @blur="inputBlurred" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'text'
|
default: 'text'
|
||||||
},
|
},
|
||||||
|
min: [String, Number],
|
||||||
readonly: Boolean,
|
readonly: Boolean,
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
inputClass: String,
|
inputClass: String,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="inline-flex toggle-btn-wrapper shadow-md">
|
<div class="inline-flex toggle-btn-wrapper shadow-md">
|
||||||
<button v-for="item in items" :key="item.value" type="button" class="toggle-btn outline-hidden relative border border-gray-600 px-4 py-1" :class="{ selected: item.value === value }" @click.stop="clickBtn(item.value)">
|
<button v-for="item in items" :key="item.value" type="button" :disabled="disabled" class="toggle-btn outline-hidden relative border border-gray-600 px-4 py-1" :class="{ selected: item.value === value }" @click.stop="clickBtn(item.value)">
|
||||||
{{ item.text }}
|
{{ item.text }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -9,13 +9,17 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: String,
|
value: [String, Number],
|
||||||
/**
|
/**
|
||||||
* [{ "text", "", "value": "" }]
|
* [{ "text", "", "value": "" }]
|
||||||
*/
|
*/
|
||||||
items: {
|
items: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: Object
|
default: Object
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -76,10 +80,19 @@ export default {
|
|||||||
.toggle-btn.selected {
|
.toggle-btn.selected {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
.toggle-btn.selected:disabled {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
.toggle-btn.selected::before {
|
.toggle-btn.selected::before {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
button.toggle-btn.selected:disabled::before {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
button.toggle-btn:disabled::before {
|
button.toggle-btn:disabled::before {
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
button.toggle-btn:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</trix-toolbar>
|
</trix-toolbar>
|
||||||
<trix-editor :toolbar="toolbarId" :contenteditable="!disabledEditor" :class="['trix-content']" ref="trix" :input="computedId" :placeholder="placeholder" @trix-change="handleContentChange" @trix-initialize="handleInitialize" @trix-focus="processTrixFocus" @trix-blur="processTrixBlur" />
|
<trix-editor :toolbar="toolbarId" :contenteditable="!disabledEditor" :class="['trix-content']" ref="trix" :input="computedId" :placeholder="placeholder" @trix-change="handleContentChange" @trix-initialize="handleInitialize" @trix-focus="processTrixFocus" @trix-blur="processTrixBlur" @trix-attachment-add="handleAttachmentAdd" />
|
||||||
<input type="hidden" :name="inputName" :id="computedId" :value="editorContent" />
|
<input type="hidden" :name="inputName" :id="computedId" :value="editorContent" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -316,6 +316,10 @@ export default {
|
|||||||
if (this.$refs.trix && this.$refs.trix.blur) {
|
if (this.$refs.trix && this.$refs.trix.blur) {
|
||||||
this.$refs.trix.blur()
|
this.$refs.trix.blur()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
handleAttachmentAdd(event) {
|
||||||
|
// Prevent pasting in images/any files from the browser
|
||||||
|
event.attachment.remove()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export default {
|
|||||||
nextRun() {
|
nextRun() {
|
||||||
if (!this.cronExpression) return ''
|
if (!this.cronExpression) return ''
|
||||||
const parsed = this.$getNextScheduledDate(this.cronExpression)
|
const parsed = this.$getNextScheduledDate(this.cronExpression)
|
||||||
return this.$formatJsDatetime(parsed, this.$store.state.serverSettings.dateFormat, this.$store.state.serverSettings.timeFormat) || ''
|
return this.$formatJsDatetime(parsed, this.$store.getters['getServerSetting']('dateFormat'), this.$store.getters['getServerSetting']('timeFormat')) || ''
|
||||||
},
|
},
|
||||||
description() {
|
description() {
|
||||||
if ((this.selectedInterval !== 'custom' || !this.selectedWeekdays.length) && this.selectedInterval !== 'daily') return ''
|
if ((this.selectedInterval !== 'custom' || !this.selectedWeekdays.length) && this.selectedInterval !== 'daily') return ''
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full py-2">
|
||||||
|
<div class="flex -mb-px">
|
||||||
|
<button type="button" :disabled="disabled" class="w-1/2 h-8 rounded-tl-md relative border border-black-200 flex items-center justify-center disabled:cursor-not-allowed" :class="!showAdvancedView ? 'text-white bg-bg hover:bg-bg/60 border-b-bg' : 'text-gray-400 hover:text-gray-300 bg-primary/70 hover:bg-primary/60'" @click="showAdvancedView = false">
|
||||||
|
<p class="text-sm">{{ $strings.HeaderPresets }}</p>
|
||||||
|
</button>
|
||||||
|
<button type="button" :disabled="disabled" class="w-1/2 h-8 rounded-tr-md relative border border-black-200 flex items-center justify-center -ml-px disabled:cursor-not-allowed" :class="showAdvancedView ? 'text-white bg-bg hover:bg-bg/60 border-b-bg' : 'text-gray-400 hover:text-gray-300 bg-primary/70 hover:bg-primary/60'" @click="showAdvancedView = true">
|
||||||
|
<p class="text-sm">{{ $strings.HeaderAdvanced }}</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 md:p-8 border border-black-200 rounded-b-md mr-px bg-bg">
|
||||||
|
<template v-if="!showAdvancedView">
|
||||||
|
<div class="flex flex-wrap gap-4 sm:gap-8 justify-start sm:justify-center">
|
||||||
|
<div class="flex flex-col items-start gap-2">
|
||||||
|
<p class="text-sm w-40">{{ $strings.LabelCodec }}</p>
|
||||||
|
<ui-toggle-btns v-model="selectedCodec" :items="codecItems" :disabled="disabled" />
|
||||||
|
<p class="text-xs text-gray-300">
|
||||||
|
{{ $strings.LabelCurrently }} <span class="text-white">{{ currentCodec }}</span> <span v-if="isCodecsDifferent" class="text-warning">(mixed)</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-start gap-2">
|
||||||
|
<p class="text-sm w-40">{{ $strings.LabelBitrate }}</p>
|
||||||
|
<ui-toggle-btns v-model="selectedBitrate" :items="bitrateItems" :disabled="disabled" />
|
||||||
|
<p class="text-xs text-gray-300">
|
||||||
|
{{ $strings.LabelCurrently }} <span class="text-white">{{ currentBitrate }} KB/s</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-start gap-2">
|
||||||
|
<p class="text-sm w-40">{{ $strings.LabelChannels }}</p>
|
||||||
|
<ui-toggle-btns v-model="selectedChannels" :items="channelsItems" :disabled="disabled" />
|
||||||
|
<p class="text-xs text-gray-300">
|
||||||
|
{{ $strings.LabelCurrently }} <span class="text-white">{{ currentChannels }} ({{ currentChanelLayout }})</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-wrap gap-4 sm:gap-8 justify-start sm:justify-center mb-4">
|
||||||
|
<div class="w-40">
|
||||||
|
<ui-text-input-with-label v-model="customCodec" :label="$strings.LabelAudioCodec" :disabled="disabled" @input="customCodecChanged" />
|
||||||
|
</div>
|
||||||
|
<div class="w-40">
|
||||||
|
<ui-text-input-with-label v-model="customBitrate" :label="$strings.LabelAudioBitrate" :disabled="disabled" @input="customBitrateChanged" />
|
||||||
|
</div>
|
||||||
|
<div class="w-40">
|
||||||
|
<ui-text-input-with-label v-model="customChannels" :label="$strings.LabelAudioChannels" type="number" :disabled="disabled" @input="customChannelsChanged" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs sm:text-sm text-warning sm:text-center">{{ $strings.LabelEncodingWarningAdvancedSettings }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
audioTracks: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showAdvancedView: false,
|
||||||
|
selectedCodec: 'aac',
|
||||||
|
selectedBitrate: '128k',
|
||||||
|
selectedChannels: 2,
|
||||||
|
customCodec: 'aac',
|
||||||
|
customBitrate: '128k',
|
||||||
|
customChannels: 2,
|
||||||
|
currentCodec: '',
|
||||||
|
currentBitrate: '',
|
||||||
|
currentChannels: '',
|
||||||
|
currentChanelLayout: '',
|
||||||
|
isCodecsDifferent: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
codecItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: 'Copy',
|
||||||
|
value: 'copy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'AAC',
|
||||||
|
value: 'aac'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'OPUS',
|
||||||
|
value: 'opus'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
bitrateItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: '32k',
|
||||||
|
value: '32k'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '64k',
|
||||||
|
value: '64k'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '128k',
|
||||||
|
value: '128k'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '192k',
|
||||||
|
value: '192k'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
channelsItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: '1 (mono)',
|
||||||
|
value: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '2 (stereo)',
|
||||||
|
value: 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
customBitrateChanged(val) {
|
||||||
|
localStorage.setItem('embedMetadataBitrate', val)
|
||||||
|
},
|
||||||
|
customChannelsChanged(val) {
|
||||||
|
localStorage.setItem('embedMetadataChannels', val)
|
||||||
|
},
|
||||||
|
customCodecChanged(val) {
|
||||||
|
localStorage.setItem('embedMetadataCodec', val)
|
||||||
|
},
|
||||||
|
getEncodingOptions() {
|
||||||
|
if (this.showAdvancedView) {
|
||||||
|
return {
|
||||||
|
codec: this.customCodec || this.selectedCodec || 'aac',
|
||||||
|
bitrate: this.customBitrate || this.selectedBitrate || '128k',
|
||||||
|
channels: this.customChannels || this.selectedChannels || 2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
codec: this.selectedCodec || 'aac',
|
||||||
|
bitrate: this.selectedBitrate || '128k',
|
||||||
|
channels: this.selectedChannels || 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setPreset() {
|
||||||
|
// If already AAC and not mixed, set copy
|
||||||
|
if (this.currentCodec === 'aac' && !this.isCodecsDifferent) {
|
||||||
|
this.selectedCodec = 'copy'
|
||||||
|
} else {
|
||||||
|
this.selectedCodec = 'aac'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.currentBitrate) {
|
||||||
|
this.selectedBitrate = '128k'
|
||||||
|
} else {
|
||||||
|
// Find closest bitrate rounding up
|
||||||
|
const bitratesToMatch = [32, 64, 128, 192]
|
||||||
|
const closestBitrate = bitratesToMatch.find((bitrate) => bitrate >= this.currentBitrate) || 192
|
||||||
|
this.selectedBitrate = closestBitrate + 'k'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.currentChannels || isNaN(this.currentChannels)) {
|
||||||
|
this.selectedChannels = 2
|
||||||
|
} else {
|
||||||
|
// Either 1 or 2
|
||||||
|
this.selectedChannels = Math.max(Math.min(Number(this.currentChannels), 2), 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setCurrentValues() {
|
||||||
|
if (this.audioTracks.length === 0) return
|
||||||
|
|
||||||
|
this.currentChannels = this.audioTracks[0].channels
|
||||||
|
this.currentChanelLayout = this.audioTracks[0].channelLayout
|
||||||
|
this.currentCodec = this.audioTracks[0].codec
|
||||||
|
|
||||||
|
let totalBitrate = 0
|
||||||
|
for (const track of this.audioTracks) {
|
||||||
|
const trackBitrate = !isNaN(track.bitRate) ? track.bitRate : 0
|
||||||
|
totalBitrate += trackBitrate
|
||||||
|
|
||||||
|
if (track.channels > this.currentChannels) this.currentChannels = track.channels
|
||||||
|
if (track.codec !== this.currentCodec) {
|
||||||
|
console.warn('Audio track codec is different from the first track', track.codec)
|
||||||
|
this.isCodecsDifferent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentBitrate = Math.round(totalBitrate / this.audioTracks.length / 1000)
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.customBitrate = localStorage.getItem('embedMetadataBitrate') || '128k'
|
||||||
|
this.customChannels = localStorage.getItem('embedMetadataChannels') || 2
|
||||||
|
this.customCodec = localStorage.getItem('embedMetadataCodec') || 'aac'
|
||||||
|
|
||||||
|
this.setCurrentValues()
|
||||||
|
|
||||||
|
this.setPreset()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,40 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<ui-tooltip :text="$strings.LabelExplicit" direction="top">
|
<ui-tooltip :text="$strings.LabelExplicit" direction="top">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12px" height="12px" viewBox="0 0 512 512" class="ml-1">
|
<span class="material-symbols fill text-sm ml-1 !block">explicit</span>
|
||||||
<path
|
|
||||||
fill="white"
|
|
||||||
d="M 89.00,40.12
|
|
||||||
C 89.00,40.12 127.00,40.12 127.00,40.12
|
|
||||||
127.00,40.12 198.00,40.12 198.00,40.12
|
|
||||||
198.00,40.12 416.00,40.12 416.00,40.12
|
|
||||||
446.58,40.05 472.95,66.42 473.00,97.00
|
|
||||||
473.00,97.00 473.00,303.00 473.00,303.00
|
|
||||||
473.00,303.00 473.00,418.00 473.00,418.00
|
|
||||||
472.65,447.55 445.06,472.95 416.00,473.00
|
|
||||||
416.00,473.00 210.00,473.00 210.00,473.00
|
|
||||||
210.00,473.00 95.00,473.00 95.00,473.00
|
|
||||||
65.45,472.65 40.05,445.06 40.00,416.00
|
|
||||||
40.00,416.00 40.00,136.00 40.00,136.00
|
|
||||||
40.00,136.00 40.00,109.00 40.00,109.00
|
|
||||||
40.00,109.00 40.00,96.00 40.00,96.00
|
|
||||||
40.07,81.58 46.89,67.14 57.01,57.01
|
|
||||||
61.17,52.86 64.86,50.13 70.00,47.31
|
|
||||||
77.25,43.33 81.02,42.18 89.00,40.12 Z
|
|
||||||
M 337.00,121.00
|
|
||||||
C 337.00,121.00 175.00,121.00 175.00,121.00
|
|
||||||
175.00,121.00 175.00,392.00 175.00,392.00
|
|
||||||
175.00,392.00 337.00,392.00 337.00,392.00
|
|
||||||
337.00,392.00 337.00,349.00 337.00,349.00
|
|
||||||
337.00,349.00 226.00,349.00 226.00,349.00
|
|
||||||
226.00,349.00 226.00,274.00 226.00,274.00
|
|
||||||
226.00,274.00 332.00,274.00 332.00,274.00
|
|
||||||
332.00,274.00 332.00,232.00 332.00,232.00
|
|
||||||
332.00,232.00 226.00,232.00 226.00,232.00
|
|
||||||
226.00,232.00 226.00,164.00 226.00,164.00
|
|
||||||
226.00,164.00 337.00,164.00 337.00,164.00
|
|
||||||
337.00,164.00 337.00,121.00 337.00,121.00 Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -248,4 +248,4 @@ export default {
|
|||||||
transform: scale(0);
|
transform: scale(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<ui-multi-select-query-input v-model="seriesItems" text-key="displayName" :label="$strings.LabelSeries" :disabled="disabled" readonly show-edit @edit="editSeriesItem" @add="addNewSeries" />
|
<ui-multi-select-query-input v-model="seriesItems" text-key="displayName" :label="$strings.LabelSeries" :disabled="disabled" readonly show-edit @edit="editSeriesItem" @add="addNewSeries" />
|
||||||
|
|
||||||
<modals-edit-series-input-inner-modal v-model="showSeriesForm" :selected-series="selectedSeries" :existing-series-names="existingSeriesNames" @submit="submitSeriesForm" />
|
<modals-edit-series-input-inner-modal v-model="showSeriesForm" :selected-series="selectedSeries" :existing-series-names="existingSeriesNames" :original-series-sequence="originalSeriesSequence" @submit="submitSeriesForm" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedSeries: null,
|
selectedSeries: null,
|
||||||
|
originalSeriesSequence: null,
|
||||||
showSeriesForm: false
|
showSeriesForm: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -59,6 +60,7 @@ export default {
|
|||||||
..._series
|
..._series
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.originalSeriesSequence = _series.sequence
|
||||||
this.showSeriesForm = true
|
this.showSeriesForm = true
|
||||||
},
|
},
|
||||||
addNewSeries() {
|
addNewSeries() {
|
||||||
@@ -68,6 +70,7 @@ export default {
|
|||||||
sequence: ''
|
sequence: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.originalSeriesSequence = null
|
||||||
this.showSeriesForm = true
|
this.showSeriesForm = true
|
||||||
},
|
},
|
||||||
submitSeriesForm() {
|
submitSeriesForm() {
|
||||||
@@ -106,4 +109,4 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ describe('LazySeriesCard', () => {
|
|||||||
},
|
},
|
||||||
$store: {
|
$store: {
|
||||||
getters: {
|
getters: {
|
||||||
|
getServerSetting: () => 'MM/dd/yyyy',
|
||||||
'user/getUserCanUpdate': true,
|
'user/getUserCanUpdate': true,
|
||||||
'user/getUserMediaProgress': (id) => null,
|
'user/getUserMediaProgress': (id) => null,
|
||||||
'user/getSizeMultiplier': 1,
|
'user/getSizeMultiplier': 1,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
socket: null,
|
socket: null,
|
||||||
isSocketConnected: false,
|
isSocketConnected: false,
|
||||||
|
isSocketAuthenticated: false,
|
||||||
isFirstSocketConnection: true,
|
isFirstSocketConnection: true,
|
||||||
socketConnectionToastId: null,
|
socketConnectionToastId: null,
|
||||||
currentLang: null,
|
currentLang: null,
|
||||||
@@ -81,9 +82,28 @@ export default {
|
|||||||
document.body.classList.add('app-bar')
|
document.body.classList.add('app-bar')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
tokenRefreshed(newAccessToken) {
|
||||||
|
if (this.isSocketConnected && !this.isSocketAuthenticated) {
|
||||||
|
console.log('[SOCKET] Re-authenticating socket after token refresh')
|
||||||
|
this.socket.emit('auth', newAccessToken)
|
||||||
|
}
|
||||||
|
},
|
||||||
updateSocketConnectionToast(content, type, timeout) {
|
updateSocketConnectionToast(content, type, timeout) {
|
||||||
if (this.socketConnectionToastId !== null && this.socketConnectionToastId !== undefined) {
|
if (this.socketConnectionToastId !== null && this.socketConnectionToastId !== undefined) {
|
||||||
this.$toast.update(this.socketConnectionToastId, { content: content, options: { timeout: timeout, type: type, closeButton: false, position: 'bottom-center', onClose: () => null, closeOnClick: timeout !== null } }, false)
|
const toastUpdateOptions = {
|
||||||
|
content: content,
|
||||||
|
options: {
|
||||||
|
timeout: timeout,
|
||||||
|
type: type,
|
||||||
|
closeButton: false,
|
||||||
|
position: 'bottom-center',
|
||||||
|
onClose: () => {
|
||||||
|
this.socketConnectionToastId = null
|
||||||
|
},
|
||||||
|
closeOnClick: timeout !== null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$toast.update(this.socketConnectionToastId, toastUpdateOptions, false)
|
||||||
} else {
|
} else {
|
||||||
this.socketConnectionToastId = this.$toast[type](content, { position: 'bottom-center', timeout: timeout, closeButton: false, closeOnClick: timeout !== null })
|
this.socketConnectionToastId = this.$toast[type](content, { position: 'bottom-center', timeout: timeout, closeButton: false, closeOnClick: timeout !== null })
|
||||||
}
|
}
|
||||||
@@ -109,7 +129,7 @@ export default {
|
|||||||
this.updateSocketConnectionToast(this.$strings.ToastSocketDisconnected, 'error', null)
|
this.updateSocketConnectionToast(this.$strings.ToastSocketDisconnected, 'error', null)
|
||||||
},
|
},
|
||||||
reconnect() {
|
reconnect() {
|
||||||
console.error('[SOCKET] reconnected')
|
console.log('[SOCKET] reconnected')
|
||||||
},
|
},
|
||||||
reconnectAttempt(val) {
|
reconnectAttempt(val) {
|
||||||
console.log(`[SOCKET] reconnect attempt ${val}`)
|
console.log(`[SOCKET] reconnect attempt ${val}`)
|
||||||
@@ -120,6 +140,10 @@ export default {
|
|||||||
reconnectFailed() {
|
reconnectFailed() {
|
||||||
console.error('[SOCKET] reconnect failed')
|
console.error('[SOCKET] reconnect failed')
|
||||||
},
|
},
|
||||||
|
authFailed(payload) {
|
||||||
|
console.error('[SOCKET] auth failed', payload.message)
|
||||||
|
this.isSocketAuthenticated = false
|
||||||
|
},
|
||||||
init(payload) {
|
init(payload) {
|
||||||
console.log('Init Payload', payload)
|
console.log('Init Payload', payload)
|
||||||
|
|
||||||
@@ -127,7 +151,7 @@ export default {
|
|||||||
this.$store.commit('users/setUsersOnline', payload.usersOnline)
|
this.$store.commit('users/setUsersOnline', payload.usersOnline)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$eventBus.$emit('socket_init')
|
this.isSocketAuthenticated = true
|
||||||
},
|
},
|
||||||
streamOpen(stream) {
|
streamOpen(stream) {
|
||||||
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamOpen(stream)
|
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamOpen(stream)
|
||||||
@@ -175,7 +199,7 @@ export default {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('User has no more accessible libraries')
|
console.error('User has no more accessible libraries')
|
||||||
this.$store.commit('libraries/setCurrentLibrary', null)
|
this.$store.commit('libraries/setCurrentLibrary', { id: null })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -354,6 +378,15 @@ export default {
|
|||||||
this.$store.commit('scanners/removeCustomMetadataProvider', provider)
|
this.$store.commit('scanners/removeCustomMetadataProvider', provider)
|
||||||
},
|
},
|
||||||
initializeSocket() {
|
initializeSocket() {
|
||||||
|
if (this.$root.socket) {
|
||||||
|
// Can happen in dev due to hot reload
|
||||||
|
console.warn('Socket already initialized')
|
||||||
|
this.socket = this.$root.socket
|
||||||
|
this.isSocketConnected = this.$root.socket?.connected
|
||||||
|
this.isFirstSocketConnection = false
|
||||||
|
this.socketConnectionToastId = null
|
||||||
|
return
|
||||||
|
}
|
||||||
this.socket = this.$nuxtSocket({
|
this.socket = this.$nuxtSocket({
|
||||||
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
|
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
|
||||||
persist: 'main',
|
persist: 'main',
|
||||||
@@ -364,6 +397,7 @@ export default {
|
|||||||
path: `${this.$config.routerBasePath}/socket.io`
|
path: `${this.$config.routerBasePath}/socket.io`
|
||||||
})
|
})
|
||||||
this.$root.socket = this.socket
|
this.$root.socket = this.socket
|
||||||
|
this.isSocketAuthenticated = false
|
||||||
console.log('Socket initialized')
|
console.log('Socket initialized')
|
||||||
|
|
||||||
// Pre-defined socket events
|
// Pre-defined socket events
|
||||||
@@ -377,6 +411,7 @@ export default {
|
|||||||
|
|
||||||
// Event received after authorizing socket
|
// Event received after authorizing socket
|
||||||
this.socket.on('init', this.init)
|
this.socket.on('init', this.init)
|
||||||
|
this.socket.on('auth_failed', this.authFailed)
|
||||||
|
|
||||||
// Stream Listeners
|
// Stream Listeners
|
||||||
this.socket.on('stream_open', this.streamOpen)
|
this.socket.on('stream_open', this.streamOpen)
|
||||||
@@ -571,6 +606,7 @@ export default {
|
|||||||
this.updateBodyClass()
|
this.updateBodyClass()
|
||||||
this.resize()
|
this.resize()
|
||||||
this.$eventBus.$on('change-lang', this.changeLanguage)
|
this.$eventBus.$on('change-lang', this.changeLanguage)
|
||||||
|
this.$eventBus.$on('token_refreshed', this.tokenRefreshed)
|
||||||
window.addEventListener('resize', this.resize)
|
window.addEventListener('resize', this.resize)
|
||||||
window.addEventListener('keydown', this.keyDown)
|
window.addEventListener('keydown', this.keyDown)
|
||||||
|
|
||||||
@@ -594,6 +630,7 @@ export default {
|
|||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$eventBus.$off('change-lang', this.changeLanguage)
|
this.$eventBus.$off('change-lang', this.changeLanguage)
|
||||||
|
this.$eventBus.$off('token_refreshed', this.tokenRefreshed)
|
||||||
window.removeEventListener('resize', this.resize)
|
window.removeEventListener('resize', this.resize)
|
||||||
window.removeEventListener('keydown', this.keyDown)
|
window.removeEventListener('keydown', this.keyDown)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import LazyBookCard from '@/components/cards/LazyBookCard'
|
|||||||
import LazySeriesCard from '@/components/cards/LazySeriesCard'
|
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 AuthorCard from '@/components/cards/AuthorCard'
|
import AuthorCard from '@/components/cards/AuthorCard'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -20,7 +19,6 @@ export default {
|
|||||||
if (this.entityName === 'series') return Vue.extend(LazySeriesCard)
|
if (this.entityName === 'series') return Vue.extend(LazySeriesCard)
|
||||||
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 === 'authors') return Vue.extend(AuthorCard)
|
if (this.entityName === 'authors') return Vue.extend(AuthorCard)
|
||||||
return Vue.extend(LazyBookCard)
|
return Vue.extend(LazyBookCard)
|
||||||
},
|
},
|
||||||
@@ -28,7 +26,6 @@ export default {
|
|||||||
if (this.entityName === 'series') return 'cards-lazy-series-card'
|
if (this.entityName === 'series') return 'cards-lazy-series-card'
|
||||||
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 === 'authors') return 'cards-author-card'
|
if (this.entityName === 'authors') return 'cards-author-card'
|
||||||
return 'cards-lazy-book-card'
|
return 'cards-lazy-book-card'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ module.exports = {
|
|||||||
|
|
||||||
// Axios module configuration: https://go.nuxtjs.dev/config-axios
|
// Axios module configuration: https://go.nuxtjs.dev/config-axios
|
||||||
axios: {
|
axios: {
|
||||||
baseURL: routerBasePath
|
baseURL: routerBasePath,
|
||||||
|
progress: false
|
||||||
},
|
},
|
||||||
|
|
||||||
// nuxt/pwa https://pwa.nuxtjs.org
|
// nuxt/pwa https://pwa.nuxtjs.org
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.20.0",
|
"version": "2.29.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.20.0",
|
"version": "2.29.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.20.0",
|
"version": "2.29.0",
|
||||||
"buildNumber": 1,
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|||||||
+11
-10
@@ -182,18 +182,19 @@ export default {
|
|||||||
password: this.password,
|
password: this.password,
|
||||||
newPassword: this.newPassword
|
newPassword: this.newPassword
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then(() => {
|
||||||
if (res.success) {
|
this.$toast.success(this.$strings.ToastUserPasswordChangeSuccess)
|
||||||
this.$toast.success(this.$strings.ToastUserPasswordChangeSuccess)
|
this.resetForm()
|
||||||
this.resetForm()
|
|
||||||
} else {
|
|
||||||
this.$toast.error(res.error || this.$strings.ToastUnknownError)
|
|
||||||
}
|
|
||||||
this.changingPassword = false
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error)
|
console.error('Failed to change password', error)
|
||||||
this.$toast.error(this.$strings.ToastUnknownError)
|
let errorMessage = this.$strings.ToastUnknownError
|
||||||
|
if (error.response?.data && typeof error.response.data === 'string') {
|
||||||
|
errorMessage = error.response.data
|
||||||
|
}
|
||||||
|
this.$toast.error(errorMessage)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
this.changingPassword = false
|
this.changingPassword = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="page-wrapper" class="bg-bg page overflow-y-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
|
<div id="page-wrapper" class="bg-bg page overflow-y-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="flex items-center py-4 px-2 md:px-0 max-w-7xl mx-auto">
|
<div class="flex items-center py-4 px-4 max-w-7xl mx-auto">
|
||||||
<nuxt-link :to="`/item/${libraryItem.id}`" class="hover:underline">
|
<nuxt-link :to="`/item/${libraryItem.id}`" class="hover:underline">
|
||||||
<h1 class="text-lg lg:text-xl">{{ title }}</h1>
|
<h1 class="text-lg lg:text-xl">{{ title }}</h1>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<p class="text-base font-mono ml-4 hidden md:block">{{ $secondsToTimestamp(mediaDurationRounded) }}</p>
|
<p class="text-base font-mono ml-4 hidden md:block">{{ $secondsToTimestamp(mediaDurationRounded) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap-reverse justify-center py-4 px-2">
|
<div class="flex flex-wrap-reverse lg:flex-nowrap justify-center py-4 px-4">
|
||||||
<div class="w-full max-w-3xl py-4">
|
<div class="w-full max-w-3xl py-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="w-12 hidden lg:block" />
|
<div class="w-12 hidden lg:block" />
|
||||||
@@ -23,8 +23,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center mb-3 py-1 -mx-1">
|
<div class="flex items-center mb-3 py-1 -mx-1">
|
||||||
<div class="w-12 hidden lg:block" />
|
<div class="w-12 hidden lg:block" />
|
||||||
<ui-btn v-if="chapters.length" color="bg-primary" small class="mx-1" @click.stop="removeAllChaptersClick">{{ $strings.ButtonRemoveAll }}</ui-btn>
|
<ui-btn v-if="chapters.length" color="bg-primary" small class="mx-1 whitespace-nowrap" @click.stop="removeAllChaptersClick">{{ $strings.ButtonRemoveAll }}</ui-btn>
|
||||||
<ui-btn v-if="newChapters.length > 1" :color="showShiftTimes ? 'bg' : 'primary'" class="mx-1" small @click="showShiftTimes = !showShiftTimes">{{ $strings.ButtonShiftTimes }}</ui-btn>
|
<ui-btn v-if="newChapters.length > 1" :color="showShiftTimes ? 'bg-bg' : 'bg-primary'" class="mx-1 whitespace-nowrap" small @click="showShiftTimes = !showShiftTimes">{{ $strings.ButtonShiftTimes }}</ui-btn>
|
||||||
<ui-btn color="bg-primary" small :class="{ 'mx-1': newChapters.length > 1 }" @click="showFindChaptersModal = true">{{ $strings.ButtonLookup }}</ui-btn>
|
<ui-btn color="bg-primary" small :class="{ 'mx-1': newChapters.length > 1 }" @click="showFindChaptersModal = true">{{ $strings.ButtonLookup }}</ui-btn>
|
||||||
<div class="grow" />
|
<div class="grow" />
|
||||||
<ui-btn v-if="hasChanges" small class="mx-1" @click.stop="resetChapters">{{ $strings.ButtonReset }}</ui-btn>
|
<ui-btn v-if="hasChanges" small class="mx-1" @click.stop="resetChapters">{{ $strings.ButtonReset }}</ui-btn>
|
||||||
@@ -53,51 +53,101 @@
|
|||||||
|
|
||||||
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
|
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
|
||||||
<div class="w-8 min-w-8 md:w-12 md:min-w-12"></div>
|
<div class="w-8 min-w-8 md:w-12 md:min-w-12"></div>
|
||||||
<div class="w-24 min-w-24 md:w-32 md:min-w-32 px-2">{{ $strings.LabelStart }}</div>
|
<div class="w-38 min-w-38 md:w-40 md:min-w-40 px-1 pl-8">{{ $strings.LabelStart }}</div>
|
||||||
<div class="grow px-2">{{ $strings.LabelTitle }}</div>
|
<div class="grow px-1 min-w-54">{{ $strings.LabelTitle }}</div>
|
||||||
|
<div class="w-7 min-w-7 px-1 flex items-center justify-center">
|
||||||
|
<ui-tooltip :text="allChaptersLocked ? $strings.TooltipUnlockAllChapters : $strings.TooltipLockAllChapters" direction="bottom">
|
||||||
|
<button class="w-7 h-7 rounded-full flex items-center justify-center cursor-pointer transition-colors duration-150" :class="allChaptersLocked ? 'text-orange-400 hover:text-orange-300' : 'text-gray-300 hover:text-white'" @click="toggleAllChaptersLock">
|
||||||
|
<span class="material-symbols text-xl">{{ allChaptersLocked ? 'lock' : 'lock_open' }}</span>
|
||||||
|
</button>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
<div class="w-32"></div>
|
<div class="w-32"></div>
|
||||||
</div>
|
</div>
|
||||||
<template v-for="chapter in newChapters">
|
<div v-for="chapter in newChapters" :key="chapter.id" class="flex py-1">
|
||||||
<div :key="chapter.id" class="flex py-1">
|
<div class="w-8 min-w-8 md:w-12 md:min-w-12">#{{ chapter.id + 1 }}</div>
|
||||||
<div class="w-8 min-w-8 md:w-12 md:min-w-12">#{{ chapter.id + 1 }}</div>
|
<div class="w-38 min-w-38 md:w-40 md:min-w-40 px-1">
|
||||||
<div class="w-24 min-w-24 md:w-32 md:min-w-32 px-1">
|
<div class="flex items-center gap-1">
|
||||||
<ui-text-input v-if="showSecondInputs" v-model="chapter.start" type="number" class="text-xs" @change="checkChapters" />
|
<ui-tooltip :text="$strings.TooltipSubtractOneSecond" direction="bottom">
|
||||||
<ui-time-picker v-else class="text-xs" v-model="chapter.start" :show-three-digit-hour="mediaDuration >= 360000" @change="checkChapters" />
|
<button
|
||||||
</div>
|
class="w-6 h-6 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150 flex-shrink-0"
|
||||||
<div class="grow px-1">
|
:class="{ 'opacity-50 cursor-not-allowed': chapter.id === 0 && chapter.start - timeIncrementAmount < 0 }"
|
||||||
<ui-text-input v-model="chapter.title" @change="checkChapters" class="text-xs" />
|
@click="incrementChapterTime(chapter, -timeIncrementAmount)"
|
||||||
</div>
|
:disabled="chapter.id === 0 && chapter.start - timeIncrementAmount < 0"
|
||||||
<div class="w-32 min-w-32 px-2 py-1">
|
>
|
||||||
<div class="flex items-center">
|
<span class="material-symbols text-sm">remove</span>
|
||||||
<ui-tooltip :text="$strings.MessageRemoveChapter" direction="bottom">
|
</button>
|
||||||
<button v-if="newChapters.length > 1" class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-error transform hover:scale-110 duration-150" @click="removeChapter(chapter)">
|
</ui-tooltip>
|
||||||
<span class="material-symbols text-base">remove</span>
|
|
||||||
</button>
|
|
||||||
</ui-tooltip>
|
|
||||||
|
|
||||||
<ui-tooltip :text="$strings.MessageInsertChapterBelow" direction="bottom">
|
<div class="flex-1 min-w-0">
|
||||||
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-success transform hover:scale-110 duration-150" @click="addChapter(chapter)">
|
<ui-text-input v-if="showSecondInputs" v-model="chapter.start" type="number" class="text-xs" @change="checkChapters" />
|
||||||
<span class="material-symbols text-lg">add</span>
|
<ui-time-picker v-else class="text-xs" v-model="chapter.start" :show-three-digit-hour="mediaDuration >= 360000" @change="checkChapters" />
|
||||||
</button>
|
|
||||||
</ui-tooltip>
|
|
||||||
|
|
||||||
<ui-tooltip :text="selectedChapterId === chapter.id && isPlayingChapter ? $strings.MessagePauseChapter : $strings.MessagePlayChapter" direction="bottom">
|
|
||||||
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150" @click="playChapter(chapter)">
|
|
||||||
<widgets-loading-spinner v-if="selectedChapterId === chapter.id && isLoadingChapter" />
|
|
||||||
<span v-else-if="selectedChapterId === chapter.id && isPlayingChapter" class="material-symbols text-base">pause</span>
|
|
||||||
<span v-else class="material-symbols text-base">play_arrow</span>
|
|
||||||
</button>
|
|
||||||
</ui-tooltip>
|
|
||||||
|
|
||||||
<ui-tooltip v-if="chapter.error" :text="chapter.error" direction="left">
|
|
||||||
<button class="w-7 h-7 rounded-full flex items-center justify-center text-error">
|
|
||||||
<span class="material-symbols text-lg">error_outline</span>
|
|
||||||
</button>
|
|
||||||
</ui-tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ui-tooltip :text="$strings.TooltipAddOneSecond" direction="bottom">
|
||||||
|
<button class="w-6 h-6 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150 flex-shrink-0" :class="{ 'opacity-50 cursor-not-allowed': chapter.start + timeIncrementAmount >= mediaDuration }" @click="incrementChapterTime(chapter, timeIncrementAmount)" :disabled="chapter.start + timeIncrementAmount >= mediaDuration">
|
||||||
|
<span class="material-symbols text-sm">add</span>
|
||||||
|
</button>
|
||||||
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div class="grow px-1">
|
||||||
|
<ui-text-input v-model="chapter.title" @change="checkChapters" class="text-xs min-w-52" />
|
||||||
|
</div>
|
||||||
|
<div class="w-7 min-w-7 px-1 py-1">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<ui-tooltip :text="lockedChapters.has(chapter.id) ? $strings.TooltipUnlockChapter : $strings.TooltipLockChapter" direction="bottom">
|
||||||
|
<button class="w-7 h-7 rounded-full flex items-center justify-center transform hover:scale-110 duration-150 flex-shrink-0" :class="lockedChapters.has(chapter.id) ? 'text-orange-400 hover:text-orange-300' : 'text-gray-300 hover:text-white'" @click="toggleChapterLock(chapter, $event)">
|
||||||
|
<span class="material-symbols text-base">{{ lockedChapters.has(chapter.id) ? 'lock' : 'lock_open' }}</span>
|
||||||
|
</button>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-32 min-w-32 px-2 py-1">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<ui-tooltip :text="$strings.MessageRemoveChapter" direction="bottom">
|
||||||
|
<button v-if="newChapters.length > 1" class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-error transform hover:scale-110 duration-150" @click="removeChapter(chapter)">
|
||||||
|
<span class="material-symbols text-base">delete</span>
|
||||||
|
</button>
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
|
<ui-tooltip :text="$strings.MessageInsertChapterBelow" direction="bottom">
|
||||||
|
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-success transform hover:scale-110 duration-150" @click="addChapter(chapter)">
|
||||||
|
<span class="material-symbols text-lg">add_row_below</span>
|
||||||
|
</button>
|
||||||
|
</ui-tooltip>
|
||||||
|
<ui-tooltip :text="selectedChapterId === chapter.id && isPlayingChapter ? $strings.MessagePauseChapter : $strings.MessagePlayChapter" direction="bottom">
|
||||||
|
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150" @click="playChapter(chapter)">
|
||||||
|
<widgets-loading-spinner v-if="selectedChapterId === chapter.id && isLoadingChapter" />
|
||||||
|
<span v-else-if="selectedChapterId === chapter.id && isPlayingChapter" class="material-symbols text-base">pause</span>
|
||||||
|
<span v-else class="material-symbols text-base">play_arrow</span>
|
||||||
|
</button>
|
||||||
|
</ui-tooltip>
|
||||||
|
<ui-tooltip v-if="selectedChapterId === chapter.id && (isPlayingChapter || isLoadingChapter)" :text="$strings.TooltipAdjustChapterStart" direction="bottom">
|
||||||
|
<div class="ml-2 text-xs text-gray-300 font-mono min-w-10 cursor-pointer hover:text-white transition-colors duration-150" @click="adjustChapterStartTime(chapter)">{{ elapsedTime }}s</div>
|
||||||
|
</ui-tooltip>
|
||||||
|
<ui-tooltip v-if="chapter.error" :text="chapter.error" direction="left">
|
||||||
|
<button class="w-7 h-7 rounded-full flex items-center justify-center text-error">
|
||||||
|
<span class="material-symbols text-lg">error_outline</span>
|
||||||
|
</button>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mt-4 mb-2">
|
||||||
|
<div class="w-8 min-w-8 md:w-12 md:min-w-12"></div>
|
||||||
|
<div class="w-38 min-w-38 md:w-40 md:min-w-40 px-1"></div>
|
||||||
|
<div class="flex items-center gap-2 grow px-1">
|
||||||
|
<ui-text-input v-model="bulkChapterInput" :placeholder="$strings.PlaceholderBulkChapterInput" class="text-xs grow min-w-52" @keyup.enter="handleBulkChapterAdd" />
|
||||||
|
</div>
|
||||||
|
<div class="w-39 min-w-39 px-1 py-1">
|
||||||
|
<ui-tooltip :text="$strings.TooltipAddChapters" direction="bottom" class="inline-block align-middle">
|
||||||
|
<button class="w-5 h-5 rounded-full flex items-center justify-center text-gray-300 hover:text-success transform hover:scale-110 duration-150 flex-shrink-0" :aria-label="$strings.TooltipAddChapters" :class="{ 'opacity-50 cursor-not-allowed': !bulkChapterInput.trim() }" :disabled="!bulkChapterInput.trim()" @click="handleBulkChapterAdd">
|
||||||
|
<span class="material-symbols text-lg">add</span>
|
||||||
|
</button>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full max-w-xl py-4 px-2">
|
<div class="w-full max-w-xl py-4 px-2">
|
||||||
@@ -114,19 +164,15 @@
|
|||||||
<div class="w-20">{{ $strings.LabelDuration }}</div>
|
<div class="w-20">{{ $strings.LabelDuration }}</div>
|
||||||
<div class="w-20 hidden md:block text-center">{{ $strings.HeaderChapters }}</div>
|
<div class="w-20 hidden md:block text-center">{{ $strings.HeaderChapters }}</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-for="track in audioTracks">
|
<div v-for="track in audioTracks" :key="track.ino" class="flex items-center py-2" :class="currentTrackIndex === track.index && isPlayingChapter ? 'bg-success/10' : ''">
|
||||||
<div :key="track.ino" class="flex items-center py-2" :class="currentTrackIndex === track.index && isPlayingChapter ? 'bg-success/10' : ''">
|
<div class="grow max-w-[calc(100%-80px)] pr-2">
|
||||||
<div class="grow max-w-[calc(100%-80px)] pr-2">
|
<p class="text-xs truncate max-w-sm">{{ track.metadata.filename }}</p>
|
||||||
<p class="text-xs truncate max-w-sm">{{ track.metadata.filename }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="w-20" style="min-width: 80px">
|
|
||||||
<p class="text-xs font-mono text-gray-200">{{ $secondsToTimestamp(Math.round(track.duration), false, true) }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="w-20 hidden md:flex justify-center" style="min-width: 80px">
|
|
||||||
<span v-if="(track.chapters || []).length" class="material-symbols text-success text-sm">check</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div class="w-20" style="min-width: 80px">
|
||||||
|
<p class="text-xs font-mono text-gray-200">{{ $secondsToTimestamp(Math.round(track.duration), false, true) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-20 hidden md:flex justify-center" style="min-width: 80px"><span v-if="(track.chapters || []).length" class="material-symbols text-success text-sm">check</span></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -134,6 +180,7 @@
|
|||||||
<ui-loading-indicator />
|
<ui-loading-indicator />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- audible chapter lookup modal -->
|
||||||
<modals-modal v-model="showFindChaptersModal" name="edit-book" :width="500" :processing="findingChapters">
|
<modals-modal v-model="showFindChaptersModal" name="edit-book" :width="500" :processing="findingChapters">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
||||||
@@ -141,18 +188,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="w-full h-full max-h-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative">
|
<div class="w-full h-full max-h-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative">
|
||||||
<div v-if="!chapterData" class="flex p-20">
|
<div v-if="!chapterData" class="flex flex-col items-center justify-center p-20">
|
||||||
<ui-text-input-with-label v-model.trim="asinInput" label="ASIN" />
|
<div class="relative">
|
||||||
<ui-dropdown v-model="regionInput" :label="$strings.LabelRegion" small :items="audibleRegions" class="w-32 mx-1" />
|
<div class="flex items-end space-x-2">
|
||||||
<ui-btn small color="bg-primary" class="mt-5" @click="findChapters">{{ $strings.ButtonSearch }}</ui-btn>
|
<ui-text-input-with-label v-model.trim="asinInput" label="ASIN" class="flex-grow" />
|
||||||
|
<ui-dropdown v-model="regionInput" :label="$strings.LabelRegion" small :items="audibleRegions" class="w-20 max-w-20" />
|
||||||
|
<ui-btn color="bg-primary" @click="findChapters">{{ $strings.ButtonSearch }}</ui-btn>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<ui-checkbox v-model="removeBranding" :label="$strings.LabelRemoveAudibleBranding" small checkbox-bg="bg" label-class="pl-2 text-base text-sm" @click="toggleRemoveBranding" />
|
||||||
|
</div>
|
||||||
|
<div class="absolute left-0 mt-1.5 text-error text-s h-5">
|
||||||
|
<p v-if="asinError">{{ asinError }}</p>
|
||||||
|
<p v-if="asinError">{{ $strings.MessageAsinCheck }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="invisible mt-1 text-xs"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full p-4">
|
<div v-else class="w-full p-4">
|
||||||
<div class="flex justify-between mb-4">
|
<div class="flex mb-4">
|
||||||
|
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-white flex-shrink-0" :aria-label="$strings.ButtonBack" @click="resetChapterLookupData">
|
||||||
|
<span class="material-symbols text-lg">arrow_back</span>
|
||||||
|
</button>
|
||||||
<p>
|
<p>
|
||||||
{{ $strings.LabelDurationFound }} <span class="font-semibold">{{ $secondsToTimestamp(chapterData.runtimeLengthSec) }}</span
|
{{ $strings.LabelDurationFound }} <span class="font-semibold">{{ $secondsToTimestamp(chapterData.runtimeLengthSec) }}</span>
|
||||||
><br />
|
<br />
|
||||||
<span class="font-semibold" :class="{ 'text-warning': chapters.length !== chapterData.chapters.length }">{{ chapterData.chapters.length }}</span> {{ $strings.LabelChaptersFound }}
|
<span class="font-semibold" :class="{ 'text-warning': chapters.length !== chapterData.chapters.length }">{{ chapterData.chapters.length }}</span> {{ $strings.LabelChaptersFound }}
|
||||||
</p>
|
</p>
|
||||||
|
<div class="grow" />
|
||||||
<p>
|
<p>
|
||||||
{{ $strings.LabelYourAudiobookDuration }}: <span class="font-semibold">{{ $secondsToTimestamp(mediaDurationRounded) }}</span
|
{{ $strings.LabelYourAudiobookDuration }}: <span class="font-semibold">{{ $secondsToTimestamp(mediaDurationRounded) }}</span
|
||||||
><br />
|
><br />
|
||||||
@@ -186,17 +249,49 @@
|
|||||||
<p class="pl-2">{{ $strings.MessageChapterStartIsAfter }}</p>
|
<p class="pl-2">{{ $strings.MessageChapterStartIsAfter }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center pt-2">
|
<div class="flex items-center pt-2 justify-between">
|
||||||
<ui-btn small color="bg-primary" class="mr-1" @click="applyChapterNamesOnly">{{ $strings.ButtonMapChapterTitles }}</ui-btn>
|
<div class="flex items-center gap-2">
|
||||||
<ui-tooltip :text="$strings.MessageMapChapterTitles" direction="top" class="flex items-center">
|
<ui-btn small color="bg-primary" @click="applyChapterNamesOnly">{{ $strings.ButtonMapChapterTitles }}</ui-btn>
|
||||||
<span class="material-symbols text-xl text-gray-200">info</span>
|
<ui-tooltip :text="$strings.MessageMapChapterTitles" direction="top" class="flex items-center">
|
||||||
</ui-tooltip>
|
<span class="material-symbols text-xl text-gray-200">info</span>
|
||||||
<div class="grow" />
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
<ui-btn small color="bg-success" @click="applyChapterData">{{ $strings.ButtonApplyChapters }}</ui-btn>
|
<ui-btn small color="bg-success" @click="applyChapterData">{{ $strings.ButtonApplyChapters }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
|
|
||||||
|
<!-- create bulk chapters modal -->
|
||||||
|
<modals-modal v-model="showBulkChapterModal" name="bulk-chapters" :width="400">
|
||||||
|
<template #outer>
|
||||||
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
||||||
|
<p class="text-3xl text-white truncate pointer-events-none">{{ $strings.HeaderBulkChapterModal }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="w-full h-full max-h-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative p-6">
|
||||||
|
<div class="flex flex-col space-y-8">
|
||||||
|
<p class="text-base">{{ $strings.MessageBulkChapterPattern }}</p>
|
||||||
|
|
||||||
|
<div v-if="detectedPattern" class="text-sm text-gray-400 bg-gray-800 p-2 rounded">
|
||||||
|
<strong>{{ $strings.LabelDetectedPattern }}</strong> "{{ detectedPattern.before }}{{ formatNumberWithPadding(detectedPattern.startingNumber, detectedPattern) }}{{ detectedPattern.after }}"
|
||||||
|
<br />
|
||||||
|
<strong>{{ $strings.LabelNextChapters }}</strong>
|
||||||
|
"{{ detectedPattern.before }}{{ formatNumberWithPadding(detectedPattern.startingNumber + 1, detectedPattern) }}{{ detectedPattern.after }}", "{{ detectedPattern.before }}{{ formatNumberWithPadding(detectedPattern.startingNumber + 2, detectedPattern) }}{{ detectedPattern.after }}", etc.
|
||||||
|
</div>
|
||||||
|
<div class="flex px-1 items-center">
|
||||||
|
<label class="text-base font-medium">{{ $strings.LabelNumberOfChapters }}</label>
|
||||||
|
<div class="grow" />
|
||||||
|
<ui-text-input v-model="bulkChapterCount" type="number" min="1" max="50" class="w-14" :style="{ height: `2em` }" @keyup.enter="addBulkChapters" />
|
||||||
|
</div>
|
||||||
|
<div class="flex px-1 items-center">
|
||||||
|
<ui-btn small @click="showBulkChapterModal = false">{{ $strings.ButtonCancel }}</ui-btn>
|
||||||
|
<div class="grow" />
|
||||||
|
<ui-btn small color="bg-success" @click="addBulkChapters">{{ $strings.ButtonAddChapters }}</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modals-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -249,9 +344,21 @@ export default {
|
|||||||
findingChapters: false,
|
findingChapters: false,
|
||||||
showFindChaptersModal: false,
|
showFindChaptersModal: false,
|
||||||
chapterData: null,
|
chapterData: null,
|
||||||
|
asinError: null,
|
||||||
|
removeBranding: false,
|
||||||
showSecondInputs: false,
|
showSecondInputs: false,
|
||||||
audibleRegions: ['US', 'CA', 'UK', 'AU', 'FR', 'DE', 'JP', 'IT', 'IN', 'ES'],
|
audibleRegions: ['US', 'CA', 'UK', 'AU', 'FR', 'DE', 'JP', 'IT', 'IN', 'ES'],
|
||||||
hasChanges: false
|
hasChanges: false,
|
||||||
|
timeIncrementAmount: 1,
|
||||||
|
elapsedTime: 0,
|
||||||
|
playStartTime: null,
|
||||||
|
elapsedTimeInterval: null,
|
||||||
|
lockedChapters: new Set(),
|
||||||
|
lastSelectedLockIndex: null,
|
||||||
|
bulkChapterInput: '',
|
||||||
|
showBulkChapterModal: false,
|
||||||
|
bulkChapterCount: 1,
|
||||||
|
detectedPattern: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -290,9 +397,18 @@ export default {
|
|||||||
},
|
},
|
||||||
selectedChapterId() {
|
selectedChapterId() {
|
||||||
return this.selectedChapter ? this.selectedChapter.id : null
|
return this.selectedChapter ? this.selectedChapter.id : null
|
||||||
|
},
|
||||||
|
allChaptersLocked() {
|
||||||
|
return this.newChapters.length > 0 && this.newChapters.every((chapter) => this.lockedChapters.has(chapter.id))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
formatNumberWithPadding(number, pattern) {
|
||||||
|
if (!pattern || !pattern.hasLeadingZeros || !pattern.originalPadding) {
|
||||||
|
return number.toString()
|
||||||
|
}
|
||||||
|
return number.toString().padStart(pattern.originalPadding, '0')
|
||||||
|
},
|
||||||
setChaptersFromTracks() {
|
setChaptersFromTracks() {
|
||||||
let currentStartTime = 0
|
let currentStartTime = 0
|
||||||
let index = 0
|
let index = 0
|
||||||
@@ -307,9 +423,12 @@ export default {
|
|||||||
currentStartTime += track.duration
|
currentStartTime += track.duration
|
||||||
}
|
}
|
||||||
this.newChapters = chapters
|
this.newChapters = chapters
|
||||||
|
this.lockedChapters = new Set()
|
||||||
this.checkChapters()
|
this.checkChapters()
|
||||||
},
|
},
|
||||||
|
toggleRemoveBranding() {
|
||||||
|
this.removeBranding = !this.removeBranding
|
||||||
|
},
|
||||||
shiftChapterTimes() {
|
shiftChapterTimes() {
|
||||||
if (!this.shiftAmount || isNaN(this.shiftAmount) || this.newChapters.length <= 1) {
|
if (!this.shiftAmount || isNaN(this.shiftAmount) || this.newChapters.length <= 1) {
|
||||||
return
|
return
|
||||||
@@ -317,19 +436,22 @@ export default {
|
|||||||
|
|
||||||
const amount = Number(this.shiftAmount)
|
const amount = Number(this.shiftAmount)
|
||||||
|
|
||||||
const lastChapter = this.newChapters[this.newChapters.length - 1]
|
// Check if any unlocked chapters would be affected negatively
|
||||||
if (lastChapter.start + amount > this.mediaDurationRounded) {
|
const unlockedChapters = this.newChapters.filter((chap) => !this.lockedChapters.has(chap.id))
|
||||||
this.$toast.error('Invalid shift amount. Last chapter start time would extend beyond the duration of this audiobook.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.newChapters[0].end + amount <= 0) {
|
if (unlockedChapters.length === 0) {
|
||||||
this.$toast.error('Invalid shift amount. First chapter would have zero or negative length.')
|
this.$toast.warning(this.$strings.ToastChaptersAllLocked)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < this.newChapters.length; i++) {
|
for (let i = 0; i < this.newChapters.length; i++) {
|
||||||
const chap = this.newChapters[i]
|
const chap = this.newChapters[i]
|
||||||
|
|
||||||
|
// Skip locked chapters
|
||||||
|
if (this.lockedChapters.has(chap.id)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
chap.end = Math.min(chap.end + amount, this.mediaDuration)
|
chap.end = Math.min(chap.end + amount, this.mediaDuration)
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
chap.start = Math.max(0, chap.start + amount)
|
chap.start = Math.max(0, chap.start + amount)
|
||||||
@@ -337,6 +459,83 @@ export default {
|
|||||||
}
|
}
|
||||||
this.checkChapters()
|
this.checkChapters()
|
||||||
},
|
},
|
||||||
|
incrementChapterTime(chapter, amount) {
|
||||||
|
if (chapter.id === 0 && chapter.start + amount < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (chapter.start + amount >= this.mediaDuration) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chapter.start = Math.max(0, chapter.start + amount)
|
||||||
|
this.checkChapters()
|
||||||
|
},
|
||||||
|
adjustChapterStartTime(chapter) {
|
||||||
|
const newStartTime = chapter.start + this.elapsedTime
|
||||||
|
chapter.start = newStartTime
|
||||||
|
this.checkChapters()
|
||||||
|
this.$toast.success(this.$strings.ToastChapterStartTimeAdjusted.replace('{0}', this.elapsedTime))
|
||||||
|
|
||||||
|
this.destroyAudioEl()
|
||||||
|
},
|
||||||
|
startElapsedTimeTracking() {
|
||||||
|
this.elapsedTime = 0
|
||||||
|
this.playStartTime = Date.now()
|
||||||
|
this.elapsedTimeInterval = setInterval(() => {
|
||||||
|
this.elapsedTime = Math.floor((Date.now() - this.playStartTime) / 1000)
|
||||||
|
}, 100)
|
||||||
|
},
|
||||||
|
stopElapsedTimeTracking() {
|
||||||
|
if (this.elapsedTimeInterval) {
|
||||||
|
clearInterval(this.elapsedTimeInterval)
|
||||||
|
this.elapsedTimeInterval = null
|
||||||
|
}
|
||||||
|
this.elapsedTime = 0
|
||||||
|
this.playStartTime = null
|
||||||
|
},
|
||||||
|
toggleChapterLock(chapter, event) {
|
||||||
|
const chapterId = chapter.id
|
||||||
|
|
||||||
|
if (event.shiftKey && this.lastSelectedLockIndex !== null) {
|
||||||
|
const startIndex = Math.min(this.lastSelectedLockIndex, chapterId)
|
||||||
|
const endIndex = Math.max(this.lastSelectedLockIndex, chapterId)
|
||||||
|
const shouldLock = !this.lockedChapters.has(chapterId)
|
||||||
|
|
||||||
|
for (let i = startIndex; i <= endIndex; i++) {
|
||||||
|
if (shouldLock) {
|
||||||
|
this.lockedChapters.add(i)
|
||||||
|
} else {
|
||||||
|
this.lockedChapters.delete(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.lockedChapters.has(chapterId)) {
|
||||||
|
this.lockedChapters.delete(chapterId)
|
||||||
|
} else {
|
||||||
|
this.lockedChapters.add(chapterId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastSelectedLockIndex = chapterId
|
||||||
|
this.lockedChapters = new Set(this.lockedChapters)
|
||||||
|
},
|
||||||
|
lockAllChapters() {
|
||||||
|
this.newChapters.forEach((chapter) => {
|
||||||
|
this.lockedChapters.add(chapter.id)
|
||||||
|
})
|
||||||
|
this.lockedChapters = new Set(this.lockedChapters)
|
||||||
|
},
|
||||||
|
unlockAllChapters() {
|
||||||
|
this.lockedChapters.clear()
|
||||||
|
this.lockedChapters = new Set(this.lockedChapters)
|
||||||
|
},
|
||||||
|
toggleAllChaptersLock() {
|
||||||
|
if (this.allChaptersLocked) {
|
||||||
|
this.unlockAllChapters()
|
||||||
|
} else {
|
||||||
|
this.lockAllChapters()
|
||||||
|
}
|
||||||
|
},
|
||||||
editItem() {
|
editItem() {
|
||||||
this.$store.commit('showEditModal', this.libraryItem)
|
this.$store.commit('showEditModal', this.libraryItem)
|
||||||
},
|
},
|
||||||
@@ -351,6 +550,10 @@ export default {
|
|||||||
this.checkChapters()
|
this.checkChapters()
|
||||||
},
|
},
|
||||||
removeChapter(chapter) {
|
removeChapter(chapter) {
|
||||||
|
if (this.lockedChapters.has(chapter.id)) {
|
||||||
|
this.$toast.warning(this.$strings.ToastChapterLocked)
|
||||||
|
return
|
||||||
|
}
|
||||||
this.newChapters = this.newChapters.filter((ch) => ch.id !== chapter.id)
|
this.newChapters = this.newChapters.filter((ch) => ch.id !== chapter.id)
|
||||||
this.checkChapters()
|
this.checkChapters()
|
||||||
},
|
},
|
||||||
@@ -434,6 +637,7 @@ export default {
|
|||||||
console.log('Audio playing')
|
console.log('Audio playing')
|
||||||
this.isLoadingChapter = false
|
this.isLoadingChapter = false
|
||||||
this.isPlayingChapter = true
|
this.isPlayingChapter = true
|
||||||
|
this.startElapsedTimeTracking()
|
||||||
})
|
})
|
||||||
audioEl.addEventListener('ended', () => {
|
audioEl.addEventListener('ended', () => {
|
||||||
console.log('Audio ended')
|
console.log('Audio ended')
|
||||||
@@ -456,6 +660,10 @@ export default {
|
|||||||
this.selectedChapter = null
|
this.selectedChapter = null
|
||||||
this.isPlayingChapter = false
|
this.isPlayingChapter = false
|
||||||
this.isLoadingChapter = false
|
this.isLoadingChapter = false
|
||||||
|
this.stopElapsedTimeTracking()
|
||||||
|
},
|
||||||
|
resetChapterLookupData() {
|
||||||
|
this.chapterData = null
|
||||||
},
|
},
|
||||||
saveChapters() {
|
saveChapters() {
|
||||||
this.checkChapters()
|
this.checkChapters()
|
||||||
@@ -506,7 +714,7 @@ export default {
|
|||||||
},
|
},
|
||||||
applyChapterNamesOnly() {
|
applyChapterNamesOnly() {
|
||||||
this.newChapters.forEach((chapter, index) => {
|
this.newChapters.forEach((chapter, index) => {
|
||||||
if (this.chapterData.chapters[index]) {
|
if (this.chapterData.chapters[index] && !this.lockedChapters.has(chapter.id)) {
|
||||||
chapter.title = this.chapterData.chapters[index].title
|
chapter.title = this.chapterData.chapters[index].title
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -518,7 +726,7 @@ export default {
|
|||||||
},
|
},
|
||||||
applyChapterData() {
|
applyChapterData() {
|
||||||
let index = 0
|
let index = 0
|
||||||
this.newChapters = this.chapterData.chapters
|
const audibleChapters = this.chapterData.chapters
|
||||||
.filter((chap) => chap.startOffsetSec < this.mediaDuration)
|
.filter((chap) => chap.startOffsetSec < this.mediaDuration)
|
||||||
.map((chap) => {
|
.map((chap) => {
|
||||||
return {
|
return {
|
||||||
@@ -528,6 +736,21 @@ export default {
|
|||||||
title: chap.title
|
title: chap.title
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const merged = []
|
||||||
|
let audibleIdx = 0
|
||||||
|
for (let i = 0; i < Math.max(this.newChapters.length, audibleChapters.length); i++) {
|
||||||
|
const isLocked = this.lockedChapters.has(i)
|
||||||
|
if (isLocked && this.newChapters[i]) {
|
||||||
|
merged.push({ ...this.newChapters[i], id: i })
|
||||||
|
} else if (audibleChapters[audibleIdx]) {
|
||||||
|
merged.push({ ...audibleChapters[audibleIdx], id: i })
|
||||||
|
audibleIdx++
|
||||||
|
} else if (this.newChapters[i]) {
|
||||||
|
merged.push({ ...this.newChapters[i], id: i })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.newChapters = merged
|
||||||
this.showFindChaptersModal = false
|
this.showFindChaptersModal = false
|
||||||
this.chapterData = null
|
this.chapterData = null
|
||||||
|
|
||||||
@@ -546,17 +769,17 @@ export default {
|
|||||||
|
|
||||||
this.findingChapters = true
|
this.findingChapters = true
|
||||||
this.chapterData = null
|
this.chapterData = null
|
||||||
|
this.asinError = null // used to show warning about audible vs amazon ASIN
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/search/chapters?asin=${this.asinInput}®ion=${this.regionInput}`)
|
.$get(`/api/search/chapters?asin=${this.asinInput}®ion=${this.regionInput}`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.findingChapters = false
|
this.findingChapters = false
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
this.$toast.error(data.error)
|
this.asinError = this.$getString(data.stringKey)
|
||||||
this.showFindChaptersModal = false
|
|
||||||
} else {
|
} else {
|
||||||
console.log('Chapter data', data)
|
console.log('Chapter data', { ...data })
|
||||||
this.chapterData = data
|
this.chapterData = this.removeBranding ? this.removeBrandingFromData(data) : data
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -566,6 +789,42 @@ export default {
|
|||||||
this.showFindChaptersModal = false
|
this.showFindChaptersModal = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
removeBrandingFromData(data) {
|
||||||
|
if (!data) return data
|
||||||
|
try {
|
||||||
|
const introDuration = data.brandIntroDurationMs
|
||||||
|
const outroDuration = data.brandOutroDurationMs
|
||||||
|
|
||||||
|
for (let i = 0; i < data.chapters.length; i++) {
|
||||||
|
const chapter = data.chapters[i]
|
||||||
|
if (chapter.startOffsetMs < introDuration) {
|
||||||
|
// This should never happen, as the intro is not longer than the first chapter
|
||||||
|
// If this happens set to the next second
|
||||||
|
// Will be 0 for the first chapter anayways
|
||||||
|
chapter.startOffsetMs = i * 1000
|
||||||
|
chapter.startOffsetSec = i
|
||||||
|
} else {
|
||||||
|
chapter.startOffsetMs -= introDuration
|
||||||
|
chapter.startOffsetSec = Math.floor(chapter.startOffsetMs / 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastChapter = data.chapters[data.chapters.length - 1]
|
||||||
|
// If there is an outro that's in the outro duration, remove it
|
||||||
|
if (lastChapter && lastChapter.lengthMs <= outroDuration) {
|
||||||
|
data.chapters.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove Branding durations from Runtime totals
|
||||||
|
data.runtimeLengthMs -= introDuration + outroDuration
|
||||||
|
data.runtimeLengthSec = Math.floor(data.runtimeLengthMs / 1000)
|
||||||
|
console.log('Brandless Chapter data', data)
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
},
|
||||||
resetChapters() {
|
resetChapters() {
|
||||||
const payload = {
|
const payload = {
|
||||||
message: this.$strings.MessageResetChaptersConfirm,
|
message: this.$strings.MessageResetChaptersConfirm,
|
||||||
@@ -590,6 +849,7 @@ export default {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
this.lockedChapters = new Set()
|
||||||
this.checkChapters()
|
this.checkChapters()
|
||||||
},
|
},
|
||||||
removeAllChaptersClick() {
|
removeAllChaptersClick() {
|
||||||
@@ -631,6 +891,91 @@ export default {
|
|||||||
this.saving = false
|
this.saving = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
handleBulkChapterAdd() {
|
||||||
|
const input = this.bulkChapterInput.trim()
|
||||||
|
if (!input) return
|
||||||
|
|
||||||
|
const numberMatch = input.match(/(\d+)/)
|
||||||
|
|
||||||
|
if (numberMatch) {
|
||||||
|
// Extract the base pattern and number, preserving zero-padding
|
||||||
|
const originalNumberString = numberMatch[1]
|
||||||
|
const foundNumber = parseInt(originalNumberString)
|
||||||
|
const numberIndex = numberMatch.index
|
||||||
|
const beforeNumber = input.substring(0, numberIndex)
|
||||||
|
const afterNumber = input.substring(numberIndex + originalNumberString.length)
|
||||||
|
|
||||||
|
this.detectedPattern = {
|
||||||
|
before: beforeNumber,
|
||||||
|
after: afterNumber,
|
||||||
|
startingNumber: foundNumber,
|
||||||
|
originalPadding: originalNumberString.length,
|
||||||
|
hasLeadingZeros: originalNumberString.length > 1 && originalNumberString.startsWith('0')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bulkChapterCount = 1
|
||||||
|
this.showBulkChapterModal = true
|
||||||
|
} else {
|
||||||
|
this.addSingleChapterFromInput(input)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addSingleChapterFromInput(title) {
|
||||||
|
// Find the last chapter to determine where to add the new one
|
||||||
|
const lastChapter = this.newChapters[this.newChapters.length - 1]
|
||||||
|
const newStart = lastChapter ? lastChapter.end : 0
|
||||||
|
const newEnd = Math.min(newStart + 300, this.mediaDuration)
|
||||||
|
|
||||||
|
const newChapter = {
|
||||||
|
id: this.newChapters.length,
|
||||||
|
start: newStart,
|
||||||
|
end: newEnd,
|
||||||
|
title: title
|
||||||
|
}
|
||||||
|
|
||||||
|
this.newChapters.push(newChapter)
|
||||||
|
this.bulkChapterInput = ''
|
||||||
|
this.checkChapters()
|
||||||
|
},
|
||||||
|
|
||||||
|
addBulkChapters() {
|
||||||
|
const count = parseInt(this.bulkChapterCount)
|
||||||
|
if (!count || count < 1 || count > 150) {
|
||||||
|
this.$toast.error(this.$strings.ToastBulkChapterInvalidCount)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { before, after, startingNumber, originalPadding, hasLeadingZeros } = this.detectedPattern
|
||||||
|
const lastChapter = this.newChapters[this.newChapters.length - 1]
|
||||||
|
const baseStart = lastChapter ? lastChapter.start + 1 : 0
|
||||||
|
|
||||||
|
// Add multiple chapters with the detected pattern
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const chapterNumber = startingNumber + i
|
||||||
|
let formattedNumber = chapterNumber.toString()
|
||||||
|
|
||||||
|
// Apply zero-padding if the original had leading zeros
|
||||||
|
if (hasLeadingZeros && originalPadding > 1) {
|
||||||
|
formattedNumber = chapterNumber.toString().padStart(originalPadding, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
const newStart = baseStart + i
|
||||||
|
const newEnd = Math.min(newStart + i + i, this.mediaDuration)
|
||||||
|
|
||||||
|
const newChapter = {
|
||||||
|
id: this.newChapters.length,
|
||||||
|
start: newStart,
|
||||||
|
end: newEnd,
|
||||||
|
title: `${before}${formattedNumber}${after}`
|
||||||
|
}
|
||||||
|
|
||||||
|
this.newChapters.push(newChapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bulkChapterInput = ''
|
||||||
|
this.showBulkChapterModal = false
|
||||||
|
this.detectedPattern = null
|
||||||
|
this.checkChapters()
|
||||||
|
},
|
||||||
libraryItemUpdated(libraryItem) {
|
libraryItemUpdated(libraryItem) {
|
||||||
if (libraryItem.id === this.libraryItem.id) {
|
if (libraryItem.id === this.libraryItem.id) {
|
||||||
if (!!libraryItem.media.metadata.asin && this.mediaMetadata.asin !== libraryItem.media.metadata.asin) {
|
if (!!libraryItem.media.metadata.asin && this.mediaMetadata.asin !== libraryItem.media.metadata.asin) {
|
||||||
|
|||||||
@@ -2,7 +2,14 @@
|
|||||||
<div id="page-wrapper" class="bg-bg page p-8 overflow-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
|
<div id="page-wrapper" class="bg-bg page p-8 overflow-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="flex items-center justify-center mb-6">
|
<div class="flex items-center justify-center mb-6">
|
||||||
<div class="w-full max-w-2xl">
|
<div class="w-full max-w-2xl">
|
||||||
<p class="text-2xl mb-2">{{ $strings.HeaderAudiobookTools }}</p>
|
<div class="flex items-center mb-4">
|
||||||
|
<nuxt-link :to="`/item/${libraryItem.id}`" class="hover:underline">
|
||||||
|
<h1 class="text-lg lg:text-xl">{{ mediaMetadata.title }}</h1>
|
||||||
|
</nuxt-link>
|
||||||
|
<button class="w-7 h-7 flex items-center justify-center mx-4 hover:scale-110 duration-100 transform text-gray-200 hover:text-white" @click="editItem">
|
||||||
|
<span class="material-symbols text-base">edit</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full max-w-2xl">
|
<div class="w-full max-w-2xl">
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
@@ -13,43 +20,43 @@
|
|||||||
|
|
||||||
<div class="flex justify-center mb-2">
|
<div class="flex justify-center mb-2">
|
||||||
<div class="w-full max-w-2xl">
|
<div class="w-full max-w-2xl">
|
||||||
<p class="text-xl">{{ $strings.HeaderMetadataToEmbed }}</p>
|
<p class="text-lg">{{ $strings.HeaderMetadataToEmbed }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full max-w-2xl"></div>
|
<div class="w-full max-w-2xl"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center flex-wrap">
|
<div class="flex justify-center flex-wrap lg:flex-nowrap gap-4">
|
||||||
<div class="w-full max-w-2xl border border-white/10 bg-bg mx-2">
|
<div class="w-full max-w-2xl border border-white/10 bg-bg">
|
||||||
<div class="flex py-2 px-4">
|
<div class="flex py-2 px-4">
|
||||||
<div class="w-1/3 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelMetaTag }}</div>
|
<div class="w-28 min-w-28 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelMetaTag }}</div>
|
||||||
<div class="w-2/3 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelValue }}</div>
|
<div class="grow text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelValue }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full max-h-72 overflow-auto">
|
<div class="w-full max-h-72 overflow-auto">
|
||||||
<template v-for="(value, key, index) in metadataObject">
|
<template v-for="(value, key, index) in metadataObject">
|
||||||
<div :key="key" class="flex py-1 px-4 text-sm" :class="index % 2 === 0 ? 'bg-primary/25' : ''">
|
<div :key="key" class="flex py-1 px-4 text-sm" :class="index % 2 === 0 ? 'bg-primary/25' : ''">
|
||||||
<div class="w-1/3 font-semibold">{{ key }}</div>
|
<div class="w-28 min-w-28 font-semibold">{{ key }}</div>
|
||||||
<div class="w-2/3">
|
<div class="grow">
|
||||||
{{ value }}
|
{{ value }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full max-w-2xl border border-white/10 bg-bg mx-2">
|
<div class="w-full max-w-2xl border border-white/10 bg-bg">
|
||||||
<div class="flex py-2 px-4 bg-primary/25">
|
<div class="flex py-2 px-4 bg-primary/25">
|
||||||
<div class="grow text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelChapterTitle }}</div>
|
<div class="grow text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelChapterTitle }}</div>
|
||||||
<div class="w-24 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelStart }}</div>
|
<div class="w-16 min-w-16 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelStart }}</div>
|
||||||
<div class="w-24 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelEnd }}</div>
|
<div class="w-16 min-w-16 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelEnd }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full max-h-72 overflow-auto">
|
<div class="w-full max-h-72 overflow-auto">
|
||||||
<p v-if="!metadataChapters.length" class="py-5 text-center text-gray-200">{{ $strings.MessageNoChapters }}</p>
|
<p v-if="!metadataChapters.length" class="py-5 text-center text-gray-200">{{ $strings.MessageNoChapters }}</p>
|
||||||
<template v-for="(chapter, index) in metadataChapters">
|
<template v-for="(chapter, index) in metadataChapters">
|
||||||
<div :key="index" class="flex py-1 px-4 text-sm" :class="index % 2 === 1 ? 'bg-primary/25' : ''">
|
<div :key="index" class="flex py-1 px-4 text-sm" :class="index % 2 === 1 ? 'bg-primary/25' : ''">
|
||||||
<div class="grow font-semibold">{{ chapter.title }}</div>
|
<div class="grow font-semibold">{{ chapter.title }}</div>
|
||||||
<div class="w-24">
|
<div class="w-16 min-w-16">
|
||||||
{{ $secondsToTimestamp(chapter.start) }}
|
{{ $secondsToTimestamp(chapter.start) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="w-24">
|
<div class="w-16 min-w-16">
|
||||||
{{ $secondsToTimestamp(chapter.end) }}
|
{{ $secondsToTimestamp(chapter.end) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,10 +84,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- m4b embed action buttons -->
|
<!-- m4b embed action buttons -->
|
||||||
<div v-else class="w-full flex items-center mb-4">
|
<div v-else class="w-full flex items-center mb-4">
|
||||||
<button :disabled="processing" class="text-sm uppercase text-gray-200 flex items-center pt-px pl-1 pr-2 hover:bg-white/5 rounded-md" @click="showEncodeOptions = !showEncodeOptions">
|
|
||||||
<span class="material-symbols text-xl">{{ showEncodeOptions || usingCustomEncodeOptions ? 'check_box' : 'check_box_outline_blank' }}</span> <span class="pl-1">{{ $strings.LabelUseAdvancedOptions }}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="grow" />
|
<div class="grow" />
|
||||||
|
|
||||||
<ui-btn v-if="!isTaskFinished && processing" color="bg-error" :loading="isCancelingEncode" class="mr-2" @click.stop="cancelEncodeClick">{{ $strings.ButtonCancelEncode }}</ui-btn>
|
<ui-btn v-if="!isTaskFinished && processing" color="bg-error" :loading="isCancelingEncode" class="mr-2" @click.stop="cancelEncodeClick">{{ $strings.ButtonCancelEncode }}</ui-btn>
|
||||||
@@ -89,18 +92,16 @@
|
|||||||
<p v-else class="text-success text-lg font-semibold">{{ $strings.MessageM4BFinished }}</p>
|
<p v-else class="text-success text-lg font-semibold">{{ $strings.MessageM4BFinished }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- advanced encoding options -->
|
<!-- show encoding options for running task -->
|
||||||
<div v-if="isM4BTool" class="overflow-hidden">
|
<div v-if="encodeTaskHasEncodingOptions" class="mb-4 pb-4 border-b border-white/10">
|
||||||
<transition name="slide">
|
<div class="flex flex-wrap -mx-2">
|
||||||
<div v-if="showEncodeOptions || usingCustomEncodeOptions" class="mb-4 pb-4 border-b border-white/10">
|
<ui-text-input-with-label ref="bitrateInput" v-model="encodingOptions.bitrate" readonly :label="$strings.LabelAudioBitrate" class="m-2 max-w-40" @input="bitrateChanged" />
|
||||||
<div class="flex flex-wrap -mx-2">
|
<ui-text-input-with-label ref="channelsInput" v-model="encodingOptions.channels" readonly :label="$strings.LabelAudioChannels" class="m-2 max-w-40" @input="channelsChanged" />
|
||||||
<ui-text-input-with-label ref="bitrateInput" v-model="encodingOptions.bitrate" :disabled="processing || isTaskFinished" :label="$strings.LabelAudioBitrate" class="m-2 max-w-40" @input="bitrateChanged" />
|
<ui-text-input-with-label ref="codecInput" v-model="encodingOptions.codec" readonly :label="$strings.LabelAudioCodec" class="m-2 max-w-40" @input="codecChanged" />
|
||||||
<ui-text-input-with-label ref="channelsInput" v-model="encodingOptions.channels" :disabled="processing || isTaskFinished" :label="$strings.LabelAudioChannels" class="m-2 max-w-40" @input="channelsChanged" />
|
</div>
|
||||||
<ui-text-input-with-label ref="codecInput" v-model="encodingOptions.codec" :disabled="processing || isTaskFinished" :label="$strings.LabelAudioCodec" class="m-2 max-w-40" @input="codecChanged" />
|
</div>
|
||||||
</div>
|
<div v-else-if="isM4BTool" class="mb-4">
|
||||||
<p class="text-sm text-warning">{{ $strings.LabelEncodingWarningAdvancedSettings }}</p>
|
<widgets-encoder-options-card ref="encoderOptionsCard" :audio-tracks="audioFiles" :disabled="processing || isTaskFinished" />
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
@@ -146,19 +147,29 @@
|
|||||||
<div class="flex py-2 px-4 bg-primary/25">
|
<div class="flex py-2 px-4 bg-primary/25">
|
||||||
<div class="w-10 text-xs font-semibold text-gray-200">#</div>
|
<div class="w-10 text-xs font-semibold text-gray-200">#</div>
|
||||||
<div class="grow text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelFilename }}</div>
|
<div class="grow text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelFilename }}</div>
|
||||||
|
<div class="w-20 text-xs font-semibold uppercase text-gray-200 hidden lg:block">{{ $strings.LabelChannels }}</div>
|
||||||
|
<div class="w-16 text-xs font-semibold uppercase text-gray-200 hidden md:block">{{ $strings.LabelCodec }}</div>
|
||||||
|
<div class="w-16 text-xs font-semibold uppercase text-gray-200 hidden md:block">{{ $strings.LabelBitrate }}</div>
|
||||||
<div class="w-16 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelSize }}</div>
|
<div class="w-16 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelSize }}</div>
|
||||||
<div class="w-24"></div>
|
<div class="w-24"></div>
|
||||||
</div>
|
</div>
|
||||||
<template v-for="file in audioFiles">
|
<template v-for="file in audioFiles">
|
||||||
<div :key="file.index" class="flex py-2 px-4 text-sm" :class="file.index % 2 === 0 ? 'bg-primary/25' : ''">
|
<div :key="file.index" class="flex py-2 px-4 text-xs sm:text-sm" :class="file.index % 2 === 0 ? 'bg-primary/25' : ''">
|
||||||
<div class="w-10">{{ file.index }}</div>
|
<div class="w-10 min-w-10">{{ file.index }}</div>
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
{{ file.metadata.filename }}
|
{{ file.metadata.filename }}
|
||||||
</div>
|
</div>
|
||||||
<div class="w-16 font-mono text-gray-200">
|
<div class="w-20 min-w-20 text-gray-200 hidden lg:block">{{ file.channels || 'unknown' }} ({{ file.channelLayout || 'unknown' }})</div>
|
||||||
|
<div class="w-16 min-w-16 text-gray-200 hidden md:block">
|
||||||
|
{{ file.codec || 'unknown' }}
|
||||||
|
</div>
|
||||||
|
<div class="w-16 min-w-16 text-gray-200 hidden md:block">
|
||||||
|
{{ $bytesPretty(file.bitRate || 0, 0) }}
|
||||||
|
</div>
|
||||||
|
<div class="w-16 min-w-16 text-gray-200">
|
||||||
{{ $bytesPretty(file.metadata.size) }}
|
{{ $bytesPretty(file.metadata.size) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="w-24">
|
<div class="w-24 min-w-24">
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<span v-if="audioFilesFinished[file.ino]" class="material-symbols text-xl text-success leading-none">check_circle</span>
|
<span v-if="audioFilesFinished[file.ino]" class="material-symbols text-xl text-success leading-none">check_circle</span>
|
||||||
<div v-else-if="audioFilesEncoding[file.ino]">
|
<div v-else-if="audioFilesEncoding[file.ino]">
|
||||||
@@ -214,7 +225,6 @@ export default {
|
|||||||
metadataObject: null,
|
metadataObject: null,
|
||||||
selectedTool: 'embed',
|
selectedTool: 'embed',
|
||||||
isCancelingEncode: false,
|
isCancelingEncode: false,
|
||||||
showEncodeOptions: false,
|
|
||||||
shouldBackupAudioFiles: true,
|
shouldBackupAudioFiles: true,
|
||||||
encodingOptions: {
|
encodingOptions: {
|
||||||
bitrate: '128k',
|
bitrate: '128k',
|
||||||
@@ -263,9 +273,6 @@ export default {
|
|||||||
audioFiles() {
|
audioFiles() {
|
||||||
return (this.media.audioFiles || []).filter((af) => !af.exclude)
|
return (this.media.audioFiles || []).filter((af) => !af.exclude)
|
||||||
},
|
},
|
||||||
isSingleM4b() {
|
|
||||||
return this.audioFiles.length === 1 && this.audioFiles[0].metadata.ext.toLowerCase() === '.m4b'
|
|
||||||
},
|
|
||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
},
|
},
|
||||||
@@ -273,14 +280,10 @@ export default {
|
|||||||
return this.media.chapters || []
|
return this.media.chapters || []
|
||||||
},
|
},
|
||||||
availableTools() {
|
availableTools() {
|
||||||
if (this.isSingleM4b) {
|
return [
|
||||||
return [{ value: 'embed', text: this.$strings.LabelToolsEmbedMetadata }]
|
{ value: 'embed', text: this.$strings.LabelToolsEmbedMetadata },
|
||||||
} else {
|
{ value: 'm4b', text: this.$strings.LabelToolsM4bEncoder }
|
||||||
return [
|
]
|
||||||
{ value: 'embed', text: this.$strings.LabelToolsEmbedMetadata },
|
|
||||||
{ value: 'm4b', text: this.$strings.LabelToolsM4bEncoder }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
taskFailed() {
|
taskFailed() {
|
||||||
return this.isTaskFinished && this.task.isFailed
|
return this.isTaskFinished && this.task.isFailed
|
||||||
@@ -314,8 +317,8 @@ export default {
|
|||||||
isMetadataEmbedQueued() {
|
isMetadataEmbedQueued() {
|
||||||
return this.queuedEmbedLIds.some((lid) => lid === this.libraryItemId)
|
return this.queuedEmbedLIds.some((lid) => lid === this.libraryItemId)
|
||||||
},
|
},
|
||||||
usingCustomEncodeOptions() {
|
encodeTaskHasEncodingOptions() {
|
||||||
return this.isM4BTool && this.encodeTask && this.encodeTask.data.encodeOptions && Object.keys(this.encodeTask.data.encodeOptions).length > 0
|
return this.isM4BTool && !!this.encodeTask?.data.encodeOptions && Object.keys(this.encodeTask.data.encodeOptions).length > 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -351,19 +354,15 @@ export default {
|
|||||||
if (this.$refs.channelsInput) this.$refs.channelsInput.blur()
|
if (this.$refs.channelsInput) this.$refs.channelsInput.blur()
|
||||||
if (this.$refs.codecInput) this.$refs.codecInput.blur()
|
if (this.$refs.codecInput) this.$refs.codecInput.blur()
|
||||||
|
|
||||||
let queryStr = ''
|
const encodeOptions = this.$refs.encoderOptionsCard.getEncodingOptions()
|
||||||
if (this.showEncodeOptions) {
|
|
||||||
const options = []
|
this.encodingOptions = encodeOptions
|
||||||
if (this.encodingOptions.bitrate) options.push(`bitrate=${this.encodingOptions.bitrate}`)
|
|
||||||
if (this.encodingOptions.channels) options.push(`channels=${this.encodingOptions.channels}`)
|
const queryParams = new URLSearchParams(encodeOptions)
|
||||||
if (this.encodingOptions.codec) options.push(`codec=${this.encodingOptions.codec}`)
|
|
||||||
if (options.length) {
|
|
||||||
queryStr = `?${options.join('&')}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/tools/item/${this.libraryItemId}/encode-m4b${queryStr}`)
|
.$post(`/api/tools/item/${this.libraryItemId}/encode-m4b?${queryParams.toString()}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('Ab m4b merge started')
|
console.log('Ab m4b merge started')
|
||||||
})
|
})
|
||||||
@@ -416,14 +415,10 @@ export default {
|
|||||||
const shouldBackupAudioFiles = localStorage.getItem('embedMetadataShouldBackup')
|
const shouldBackupAudioFiles = localStorage.getItem('embedMetadataShouldBackup')
|
||||||
this.shouldBackupAudioFiles = shouldBackupAudioFiles != 0
|
this.shouldBackupAudioFiles = shouldBackupAudioFiles != 0
|
||||||
|
|
||||||
if (this.usingCustomEncodeOptions) {
|
if (this.encodeTaskHasEncodingOptions) {
|
||||||
if (this.encodeTask.data.encodeOptions.bitrate) this.encodingOptions.bitrate = this.encodeTask.data.encodeOptions.bitrate
|
if (this.encodeTask.data.encodeOptions.bitrate) this.encodingOptions.bitrate = this.encodeTask.data.encodeOptions.bitrate
|
||||||
if (this.encodeTask.data.encodeOptions.channels) this.encodingOptions.channels = this.encodeTask.data.encodeOptions.channels
|
if (this.encodeTask.data.encodeOptions.channels) this.encodingOptions.channels = this.encodeTask.data.encodeOptions.channels
|
||||||
if (this.encodeTask.data.encodeOptions.codec) this.encodingOptions.codec = this.encodeTask.data.encodeOptions.codec
|
if (this.encodeTask.data.encodeOptions.codec) this.encodingOptions.codec = this.encodeTask.data.encodeOptions.codec
|
||||||
} else {
|
|
||||||
this.encodingOptions.bitrate = localStorage.getItem('embedMetadataBitrate') || '128k'
|
|
||||||
this.encodingOptions.channels = localStorage.getItem('embedMetadataChannels') || '2'
|
|
||||||
this.encodingOptions.codec = localStorage.getItem('embedMetadataCodec') || 'aac'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetchMetadataEmbedObject() {
|
fetchMetadataEmbedObject() {
|
||||||
@@ -438,10 +433,24 @@ export default {
|
|||||||
},
|
},
|
||||||
taskUpdated(task) {
|
taskUpdated(task) {
|
||||||
this.processing = !task.isFinished
|
this.processing = !task.isFinished
|
||||||
|
},
|
||||||
|
editItem() {
|
||||||
|
this.$store.commit('showEditModal', this.libraryItem)
|
||||||
|
},
|
||||||
|
libraryItemUpdated(libraryItem) {
|
||||||
|
if (libraryItem.id === this.libraryItem.id) {
|
||||||
|
this.libraryItem = libraryItem
|
||||||
|
this.fetchMetadataEmbedObject()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.init()
|
this.init()
|
||||||
|
|
||||||
|
this.$eventBus.$on(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$eventBus.$off(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export default {
|
|||||||
else if (pageName === 'sessions') return this.$strings.HeaderListeningSessions
|
else if (pageName === 'sessions') return this.$strings.HeaderListeningSessions
|
||||||
else if (pageName === 'stats') return this.$strings.HeaderYourStats
|
else if (pageName === 'stats') return this.$strings.HeaderYourStats
|
||||||
else if (pageName === 'users') return this.$strings.HeaderUsers
|
else if (pageName === 'users') return this.$strings.HeaderUsers
|
||||||
|
else if (pageName === 'api-keys') return this.$strings.HeaderApiKeys
|
||||||
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
|
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
|
||||||
else if (pageName === 'rss-feeds') return this.$strings.HeaderRSSFeeds
|
else if (pageName === 'rss-feeds') return this.$strings.HeaderRSSFeeds
|
||||||
else if (pageName === 'email') return this.$strings.HeaderEmail
|
else if (pageName === 'email') return this.$strings.HeaderEmail
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<app-settings-content :header-text="$strings.HeaderApiKeys">
|
||||||
|
<template #header-items>
|
||||||
|
<div v-if="numApiKeys" class="mx-2 px-1.5 rounded-lg bg-primary/50 text-gray-300/90 text-sm inline-flex items-center justify-center">
|
||||||
|
<span>{{ numApiKeys }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||||
|
<a href="https://www.audiobookshelf.org/guides/api-keys" target="_blank" class="inline-flex">
|
||||||
|
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||||
|
</a>
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
|
<div class="grow" />
|
||||||
|
|
||||||
|
<ui-btn color="bg-primary" :disabled="loadingUsers || users.length === 0" small @click="setShowApiKeyModal()">{{ $strings.ButtonAddApiKey }}</ui-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<tables-api-keys-table ref="apiKeysTable" class="pt-2" @edit="setShowApiKeyModal" @numApiKeys="(count) => (numApiKeys = count)" />
|
||||||
|
</app-settings-content>
|
||||||
|
<modals-api-key-modal ref="apiKeyModal" v-model="showApiKeyModal" :api-key="selectedApiKey" :users="users" @created="apiKeyCreated" @updated="apiKeyUpdated" />
|
||||||
|
<modals-api-key-created-modal ref="apiKeyCreatedModal" v-model="showApiKeyCreatedModal" :api-key="selectedApiKey" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
asyncData({ store, redirect }) {
|
||||||
|
if (!store.getters['user/getIsAdminOrUp']) {
|
||||||
|
redirect('/')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loadingUsers: false,
|
||||||
|
selectedApiKey: null,
|
||||||
|
showApiKeyModal: false,
|
||||||
|
showApiKeyCreatedModal: false,
|
||||||
|
numApiKeys: 0,
|
||||||
|
users: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
apiKeyCreated(apiKey) {
|
||||||
|
this.numApiKeys++
|
||||||
|
this.selectedApiKey = apiKey
|
||||||
|
this.showApiKeyCreatedModal = true
|
||||||
|
if (this.$refs.apiKeysTable) {
|
||||||
|
this.$refs.apiKeysTable.addApiKey(apiKey)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
apiKeyUpdated(apiKey) {
|
||||||
|
if (this.$refs.apiKeysTable) {
|
||||||
|
this.$refs.apiKeysTable.updateApiKey(apiKey)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setShowApiKeyModal(selectedApiKey) {
|
||||||
|
this.selectedApiKey = selectedApiKey
|
||||||
|
this.showApiKeyModal = true
|
||||||
|
},
|
||||||
|
loadUsers() {
|
||||||
|
this.loadingUsers = true
|
||||||
|
this.$axios
|
||||||
|
.$get('/api/users')
|
||||||
|
.then((res) => {
|
||||||
|
this.users = res.users.sort((a, b) => {
|
||||||
|
return a.createdAt - b.createdAt
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loadingUsers = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadUsers()
|
||||||
|
},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -122,7 +122,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex items-center justify-end p-4">
|
<div class="w-full flex items-center justify-between p-4">
|
||||||
|
<p v-if="enableOpenIDAuth" class="text-sm text-warning">{{ $strings.MessageAuthenticationOIDCChangesRestart }}</p>
|
||||||
<ui-btn color="bg-success" :padding-x="8" small class="text-base" :loading="savingSettings" @click="saveSettings">{{ $strings.ButtonSave }}</ui-btn>
|
<ui-btn color="bg-success" :padding-x="8" small class="text-base" :loading="savingSettings" @click="saveSettings">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</app-settings-content>
|
</app-settings-content>
|
||||||
|
|||||||
@@ -131,35 +131,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grow py-2">
|
<div class="grow py-2">
|
||||||
<ui-dropdown :label="$strings.LabelSettingsDateFormat" v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-52" @input="(val) => updateSettingsKey('dateFormat', val)" />
|
<ui-dropdown :label="$strings.LabelSettingsDateFormat" v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-72" @input="(val) => updateSettingsKey('dateFormat', val)" />
|
||||||
<p class="text-xs ml-1 text-white/60">{{ $strings.LabelExample }}: {{ dateExample }}</p>
|
<p class="text-xs ml-1 text-white/60">{{ $strings.LabelExample }}: {{ dateExample }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grow py-2">
|
<div class="grow py-2">
|
||||||
<ui-dropdown :label="$strings.LabelSettingsTimeFormat" v-model="newServerSettings.timeFormat" :items="timeFormats" small class="max-w-52" @input="(val) => updateSettingsKey('timeFormat', val)" />
|
<ui-dropdown :label="$strings.LabelSettingsTimeFormat" v-model="newServerSettings.timeFormat" :items="timeFormats" small class="max-w-72" @input="(val) => updateSettingsKey('timeFormat', val)" />
|
||||||
<p class="text-xs ml-1 text-white/60">{{ $strings.LabelExample }}: {{ timeExample }}</p>
|
<p class="text-xs ml-1 text-white/60">{{ $strings.LabelExample }}: {{ timeExample }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<ui-dropdown :label="$strings.LabelLanguageDefaultServer" ref="langDropdown" v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-52" @input="updateServerLanguage" />
|
<ui-dropdown :label="$strings.LabelLanguageDefaultServer" ref="langDropdown" v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-72" @input="updateServerLanguage" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- old experimental features -->
|
<div class="pt-4">
|
||||||
<!-- <div class="pt-4">
|
<h2 class="font-semibold">{{ $strings.HeaderSettingsSecurity }}</h2>
|
||||||
<h2 class="font-semibold">{{ $strings.HeaderSettingsExperimental }}</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="py-2">
|
||||||
<ui-toggle-switch labeledBy="settings-experimental-features" v-model="showExperimentalFeatures" />
|
<ui-multi-select v-model="newServerSettings.allowedOrigins" :items="newServerSettings.allowedOrigins" :label="$strings.LabelCorsAllowed" class="max-w-72" @input="updateCorsOrigins" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsExperimentalFeaturesHelp">
|
</div>
|
||||||
<p class="pl-4">
|
|
||||||
<span id="settings-experimental-features">{{ $strings.LabelSettingsExperimentalFeatures }}</span>
|
|
||||||
<a :aria-label="$strings.LabelSettingsExperimentalFeaturesHelp" href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank">
|
|
||||||
<span class="material-symbols icon-text">info</span>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</ui-tooltip>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</app-settings-content>
|
</app-settings-content>
|
||||||
@@ -323,6 +314,27 @@ export default {
|
|||||||
updateServerLanguage(val) {
|
updateServerLanguage(val) {
|
||||||
this.updateSettingsKey('language', val)
|
this.updateSettingsKey('language', val)
|
||||||
},
|
},
|
||||||
|
updateCorsOrigins(val) {
|
||||||
|
const validOrigins = []
|
||||||
|
const invalidOrigins = []
|
||||||
|
|
||||||
|
val.forEach((origin) => {
|
||||||
|
const trimmedOrigin = origin.trim().toLowerCase()
|
||||||
|
try {
|
||||||
|
new URL(trimmedOrigin)
|
||||||
|
validOrigins.push(trimmedOrigin)
|
||||||
|
} catch {
|
||||||
|
invalidOrigins.push(trimmedOrigin)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (invalidOrigins.length > 0) {
|
||||||
|
this.$toast.error(this.$strings.ToastInvalidUrls)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.newServerSettings.allowedOrigins = validOrigins
|
||||||
|
this.updateSettingsKey('allowedOrigins', validOrigins)
|
||||||
|
},
|
||||||
updateSettingsKey(key, val) {
|
updateSettingsKey(key, val) {
|
||||||
if (key === 'scannerDisableWatcher') {
|
if (key === 'scannerDisableWatcher') {
|
||||||
this.newServerSettings.scannerDisableWatcher = val
|
this.newServerSettings.scannerDisableWatcher = val
|
||||||
@@ -352,6 +364,7 @@ export default {
|
|||||||
initServerSettings() {
|
initServerSettings() {
|
||||||
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
||||||
this.newServerSettings.sortingPrefixes = [...(this.newServerSettings.sortingPrefixes || [])]
|
this.newServerSettings.sortingPrefixes = [...(this.newServerSettings.sortingPrefixes || [])]
|
||||||
|
this.newServerSettings.allowedOrigins = [...(this.newServerSettings.allowedOrigins || [])]
|
||||||
this.scannerEnableWatcher = !this.newServerSettings.scannerDisableWatcher
|
this.scannerEnableWatcher = !this.newServerSettings.scannerDisableWatcher
|
||||||
|
|
||||||
this.homepageUseBookshelfView = this.newServerSettings.homeBookshelfView != this.$constants.BookshelfView.DETAIL
|
this.homepageUseBookshelfView = this.newServerSettings.homeBookshelfView != this.$constants.BookshelfView.DETAIL
|
||||||
|
|||||||
@@ -78,10 +78,10 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.$store.state.serverSettings.dateFormat
|
return this.$store.getters['getServerSetting']('dateFormat')
|
||||||
},
|
},
|
||||||
timeFormat() {
|
timeFormat() {
|
||||||
return this.$store.state.serverSettings.timeFormat
|
return this.$store.getters['getServerSetting']('timeFormat')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -6,80 +6,82 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="listeningSessions.length" class="block max-w-full relative">
|
<div v-if="listeningSessions.length" class="block max-w-full relative">
|
||||||
<table class="userSessionsTable">
|
<div class="overflow-x-auto">
|
||||||
<tr class="bg-primary/40">
|
<table class="userSessionsTable">
|
||||||
<th class="w-6 min-w-6 text-left hidden md:table-cell h-11">
|
<tr class="bg-primary/40">
|
||||||
<ui-checkbox v-model="isAllSelected" :partial="numSelected > 0 && !isAllSelected" small checkbox-bg="bg" />
|
<th class="w-6 min-w-6 text-left hidden md:table-cell h-11">
|
||||||
</th>
|
<ui-checkbox v-model="isAllSelected" :partial="numSelected > 0 && !isAllSelected" small checkbox-bg="bg" />
|
||||||
<th v-if="numSelected" class="grow text-left" :colspan="7">
|
</th>
|
||||||
<div class="flex items-center">
|
<th v-if="numSelected" class="grow text-left" :colspan="7">
|
||||||
<p>{{ $getString('MessageSelected', [numSelected]) }}</p>
|
<div class="flex items-center">
|
||||||
<div class="grow" />
|
<p>{{ $getString('MessageSelected', [numSelected]) }}</p>
|
||||||
<ui-btn small color="bg-error" :loading="deletingSessions" @click.stop="removeSessionsClick">{{ $strings.ButtonRemove }}</ui-btn>
|
<div class="grow" />
|
||||||
</div>
|
<ui-btn small color="bg-error" :loading="deletingSessions" @click.stop="removeSessionsClick">{{ $strings.ButtonRemove }}</ui-btn>
|
||||||
</th>
|
</div>
|
||||||
<th v-if="!numSelected" class="grow sm:grow-0 sm:w-48 sm:max-w-48 text-left group cursor-pointer" @click.stop="sortColumn('displayTitle')">
|
</th>
|
||||||
<div class="inline-flex items-center">
|
<th v-if="!numSelected" class="grow sm:grow-0 sm:w-48 sm:max-w-48 text-left group cursor-pointer" @click.stop="sortColumn('displayTitle')">
|
||||||
{{ $strings.LabelItem }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('displayTitle') }" class="material-symbols text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
|
<div class="inline-flex items-center">
|
||||||
</div>
|
{{ $strings.LabelItem }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('displayTitle') }" class="material-symbols text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
|
||||||
</th>
|
</div>
|
||||||
<th v-if="!numSelected" class="w-20 min-w-20 text-left hidden md:table-cell">{{ $strings.LabelUser }}</th>
|
</th>
|
||||||
<th v-if="!numSelected" class="w-26 min-w-26 text-left hidden md:table-cell group cursor-pointer" @click.stop="sortColumn('playMethod')">
|
<th v-if="!numSelected" class="w-20 min-w-20 text-left hidden md:table-cell">{{ $strings.LabelUser }}</th>
|
||||||
<div class="inline-flex items-center">
|
<th v-if="!numSelected" class="w-26 min-w-26 text-left hidden md:table-cell group cursor-pointer" @click.stop="sortColumn('playMethod')">
|
||||||
{{ $strings.LabelPlayMethod }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('playMethod') }" class="material-symbols text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
|
<div class="inline-flex items-center">
|
||||||
</div>
|
{{ $strings.LabelPlayMethod }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('playMethod') }" class="material-symbols text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
|
||||||
</th>
|
</div>
|
||||||
<th v-if="!numSelected" class="w-32 min-w-32 text-left hidden sm:table-cell">{{ $strings.LabelDeviceInfo }}</th>
|
</th>
|
||||||
<th v-if="!numSelected" class="w-24 min-w-24 sm:w-32 sm:min-w-32 group cursor-pointer" @click.stop="sortColumn('timeListening')">
|
<th v-if="!numSelected" class="w-32 min-w-32 text-left hidden sm:table-cell">{{ $strings.LabelDeviceInfo }}</th>
|
||||||
<div class="inline-flex items-center">
|
<th v-if="!numSelected" class="w-24 min-w-24 sm:w-32 sm:min-w-32 group cursor-pointer" @click.stop="sortColumn('timeListening')">
|
||||||
{{ $strings.LabelTimeListened }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('timeListening') }" class="material-symbols text-base pl-px hidden sm:inline-block">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
|
<div class="inline-flex items-center">
|
||||||
</div>
|
{{ $strings.LabelTimeListened }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('timeListening') }" class="material-symbols text-base pl-px hidden sm:inline-block">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
|
||||||
</th>
|
</div>
|
||||||
<th v-if="!numSelected" class="w-24 min-w-24 group cursor-pointer" @click.stop="sortColumn('currentTime')">
|
</th>
|
||||||
<div class="inline-flex items-center">
|
<th v-if="!numSelected" class="w-24 min-w-24 group cursor-pointer" @click.stop="sortColumn('currentTime')">
|
||||||
{{ $strings.LabelLastTime }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('currentTime') }" class="material-symbols text-base pl-px hidden sm:inline-block">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
|
<div class="inline-flex items-center">
|
||||||
</div>
|
{{ $strings.LabelLastTime }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('currentTime') }" class="material-symbols text-base pl-px hidden sm:inline-block">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
|
||||||
</th>
|
</div>
|
||||||
<th v-if="!numSelected" class="grow hidden sm:table-cell cursor-pointer group" @click.stop="sortColumn('updatedAt')">
|
</th>
|
||||||
<div class="inline-flex items-center">
|
<th v-if="!numSelected" class="grow hidden sm:table-cell cursor-pointer group" @click.stop="sortColumn('updatedAt')">
|
||||||
{{ $strings.LabelLastUpdate }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('updatedAt') }" class="material-symbols text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
|
<div class="inline-flex items-center">
|
||||||
</div>
|
{{ $strings.LabelLastUpdate }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('updatedAt') }" class="material-symbols text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
|
||||||
</th>
|
</div>
|
||||||
</tr>
|
</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<tr v-for="session in listeningSessions" :key="session.id" :class="{ selected: session.selected }" class="cursor-pointer" @click="clickSessionRow(session)">
|
<tr v-for="session in listeningSessions" :key="session.id" :class="{ selected: session.selected }" class="cursor-pointer" @click="clickSessionRow(session)">
|
||||||
<td class="hidden md:table-cell py-1 max-w-6 relative">
|
<td class="hidden md:table-cell py-1 max-w-6 relative">
|
||||||
<ui-checkbox v-model="session.selected" small checkbox-bg="bg" />
|
<ui-checkbox v-model="session.selected" small checkbox-bg="bg" />
|
||||||
<!-- overlay of the checkbox so that the entire box is clickable -->
|
<!-- overlay of the checkbox so that the entire box is clickable -->
|
||||||
<div class="absolute inset-0 w-full h-full" @click.stop="session.selected = !session.selected" />
|
<div class="absolute inset-0 w-full h-full" @click.stop="session.selected = !session.selected" />
|
||||||
</td>
|
</td>
|
||||||
<td class="py-1 grow sm:grow-0 sm:w-48 sm:max-w-48">
|
<td class="py-1 grow sm:grow-0 sm:w-48 sm:max-w-48">
|
||||||
<p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p>
|
<p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p>
|
||||||
<p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p>
|
<p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden md:table-cell w-20 min-w-20">
|
<td class="hidden md:table-cell w-20 min-w-20">
|
||||||
<p v-if="filteredUserUsername" class="text-xs">{{ filteredUserUsername }}</p>
|
<p v-if="filteredUserUsername" class="text-xs">{{ filteredUserUsername }}</p>
|
||||||
<p v-else class="text-xs">{{ session.user ? session.user.username : 'N/A' }}</p>
|
<p v-else class="text-xs">{{ session.user ? session.user.username : 'N/A' }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden md:table-cell w-26 min-w-26">
|
<td class="hidden md:table-cell w-26 min-w-26">
|
||||||
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden sm:table-cell max-w-32 min-w-32">
|
<td class="hidden sm:table-cell max-w-32 min-w-32">
|
||||||
<p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" />
|
<p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center w-24 min-w-24 sm:w-32 sm:min-w-32">
|
<td class="text-center w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||||
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
<p class="text-xs font-mono">{{ $elapsedPrettyLocalized(session.timeListening) }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center hover:underline w-24 min-w-24" @click.stop="clickCurrentTime(session)">
|
<td class="text-center hover:underline w-24 min-w-24" @click.stop="clickCurrentTime(session)">
|
||||||
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center hidden sm:table-cell">
|
<td class="text-center hidden sm:table-cell">
|
||||||
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDatetime(session.updatedAt, dateFormat, timeFormat)">
|
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDatetime(session.updatedAt, dateFormat, timeFormat)">
|
||||||
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
<!-- table bottom options -->
|
<!-- table bottom options -->
|
||||||
<div class="flex items-center my-2">
|
<div class="flex items-center my-2">
|
||||||
<div class="grow" />
|
<div class="grow" />
|
||||||
@@ -250,10 +252,10 @@ export default {
|
|||||||
return user?.username || null
|
return user?.username || null
|
||||||
},
|
},
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.$store.state.serverSettings.dateFormat
|
return this.$store.getters['getServerSetting']('dateFormat')
|
||||||
},
|
},
|
||||||
timeFormat() {
|
timeFormat() {
|
||||||
return this.$store.state.serverSettings.timeFormat
|
return this.$store.getters['getServerSetting']('timeFormat')
|
||||||
},
|
},
|
||||||
numSelected() {
|
numSelected() {
|
||||||
return this.listeningSessions.filter((s) => s.selected).length
|
return this.listeningSessions.filter((s) => s.selected).length
|
||||||
|
|||||||
@@ -13,8 +13,10 @@
|
|||||||
<widgets-online-indicator :value="!!userOnline" />
|
<widgets-online-indicator :value="!!userOnline" />
|
||||||
<h1 class="text-xl pl-2">{{ username }}</h1>
|
<h1 class="text-xl pl-2">{{ username }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="userToken" class="flex text-xs mt-4">
|
<div v-if="legacyToken" class="text-xs space-y-2 mt-4">
|
||||||
<ui-text-input-with-label :label="$strings.LabelApiToken" :value="userToken" readonly show-copy />
|
<ui-text-input-with-label label="Legacy API Token" :value="legacyToken" readonly show-copy />
|
||||||
|
|
||||||
|
<p class="text-warning" v-html="$strings.MessageAuthenticationLegacyTokenWarning" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full h-px bg-white/10 my-2" />
|
<div class="w-full h-px bg-white/10 my-2" />
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
@@ -100,9 +102,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
userToken() {
|
legacyToken() {
|
||||||
return this.user.token
|
return this.user.token
|
||||||
},
|
},
|
||||||
|
userToken() {
|
||||||
|
return this.user.accessToken
|
||||||
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
@@ -129,10 +134,10 @@ export default {
|
|||||||
return this.listeningSessions.sessions[0]
|
return this.listeningSessions.sessions[0]
|
||||||
},
|
},
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.$store.state.serverSettings.dateFormat
|
return this.$store.getters['getServerSetting']('dateFormat')
|
||||||
},
|
},
|
||||||
timeFormat() {
|
timeFormat() {
|
||||||
return this.$store.state.serverSettings.timeFormat
|
return this.$store.getters['getServerSetting']('timeFormat')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -19,39 +19,41 @@
|
|||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<h1 class="text-lg mb-2 text-white/90 px-2 sm:px-0">{{ $strings.HeaderListeningSessions }}</h1>
|
<h1 class="text-lg mb-2 text-white/90 px-2 sm:px-0">{{ $strings.HeaderListeningSessions }}</h1>
|
||||||
<div v-if="listeningSessions.length">
|
<div v-if="listeningSessions.length">
|
||||||
<table class="userSessionsTable">
|
<div class="overflow-x-auto">
|
||||||
<tr class="bg-primary/40">
|
<table class="userSessionsTable">
|
||||||
<th class="w-48 min-w-48 text-left">{{ $strings.LabelItem }}</th>
|
<tr class="bg-primary/40">
|
||||||
<th class="w-32 min-w-32 text-left hidden md:table-cell">{{ $strings.LabelPlayMethod }}</th>
|
<th class="w-48 min-w-48 text-left">{{ $strings.LabelItem }}</th>
|
||||||
<th class="w-32 min-w-32 text-left hidden sm:table-cell">{{ $strings.LabelDeviceInfo }}</th>
|
<th class="w-32 min-w-32 text-left hidden md:table-cell">{{ $strings.LabelPlayMethod }}</th>
|
||||||
<th class="w-32 min-w-32">{{ $strings.LabelTimeListened }}</th>
|
<th class="w-32 min-w-32 text-left hidden sm:table-cell">{{ $strings.LabelDeviceInfo }}</th>
|
||||||
<th class="w-16 min-w-16">{{ $strings.LabelLastTime }}</th>
|
<th class="w-32 min-w-32">{{ $strings.LabelTimeListened }}</th>
|
||||||
<th class="grow hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th>
|
<th class="w-16 min-w-16">{{ $strings.LabelLastTime }}</th>
|
||||||
</tr>
|
<th class="grow hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th>
|
||||||
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
|
</tr>
|
||||||
<td class="py-1 max-w-48">
|
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
|
||||||
<p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p>
|
<td class="py-1 max-w-48">
|
||||||
<p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p>
|
<p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p>
|
||||||
</td>
|
<p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p>
|
||||||
<td class="hidden md:table-cell">
|
</td>
|
||||||
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
<td class="hidden md:table-cell">
|
||||||
</td>
|
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
||||||
<td class="hidden sm:table-cell min-w-32 max-w-32">
|
</td>
|
||||||
<p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" />
|
<td class="hidden sm:table-cell min-w-32 max-w-32">
|
||||||
</td>
|
<p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||||
<td class="text-center">
|
</td>
|
||||||
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
<td class="text-center">
|
||||||
</td>
|
<p class="text-xs font-mono">{{ $elapsedPrettyLocalized(session.timeListening) }}</p>
|
||||||
<td class="text-center hover:underline" @click.stop="clickCurrentTime(session)">
|
</td>
|
||||||
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
<td class="text-center hover:underline" @click.stop="clickCurrentTime(session)">
|
||||||
</td>
|
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
||||||
<td class="text-center hidden sm:table-cell">
|
</td>
|
||||||
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDatetime(session.updatedAt, dateFormat, timeFormat)">
|
<td class="text-center hidden sm:table-cell">
|
||||||
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDatetime(session.updatedAt, dateFormat, timeFormat)">
|
||||||
</ui-tooltip>
|
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
||||||
</td>
|
</ui-tooltip>
|
||||||
</tr>
|
</td>
|
||||||
</table>
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
<div class="flex items-center justify-end py-1">
|
<div class="flex items-center justify-end py-1">
|
||||||
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
|
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
|
||||||
<p class="text-sm mx-1">{{ $getString('LabelPaginationPageXOfY', [currentPage + 1, numPages]) }}</p>
|
<p class="text-sm mx-1">{{ $getString('LabelPaginationPageXOfY', [currentPage + 1, numPages]) }}</p>
|
||||||
@@ -98,10 +100,10 @@ export default {
|
|||||||
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
||||||
},
|
},
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.$store.state.serverSettings.dateFormat
|
return this.$store.getters['getServerSetting']('dateFormat')
|
||||||
},
|
},
|
||||||
timeFormat() {
|
timeFormat() {
|
||||||
return this.$store.state.serverSettings.timeFormat
|
return this.$store.getters['getServerSetting']('timeFormat')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ export default {
|
|||||||
return `${process.env.serverUrl}/api/items/${this.libraryItemId}/download?token=${this.userToken}`
|
return `${process.env.serverUrl}/api/items/${this.libraryItemId}/download?token=${this.userToken}`
|
||||||
},
|
},
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.$store.state.serverSettings.dateFormat
|
return this.$store.getters['getServerSetting']('dateFormat')
|
||||||
},
|
},
|
||||||
userIsAdminOrUp() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
@@ -819,6 +819,17 @@ export default {
|
|||||||
-webkit-line-clamp: 4;
|
-webkit-line-clamp: 4;
|
||||||
max-height: calc(6 * 1lh);
|
max-height: calc(6 * 1lh);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Safari-specific fix for the description clamping */
|
||||||
|
@supports (-webkit-touch-callout: none) {
|
||||||
|
#item-description {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: calc(6 * 1lh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#item-description.show-full {
|
#item-description.show-full {
|
||||||
-webkit-line-clamp: unset;
|
-webkit-line-clamp: unset;
|
||||||
max-height: 999rem;
|
max-height: 999rem;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr v-for="narrator in narrators" :key="narrator.id">
|
<tr v-for="narrator in narrators" :key="narrator.id">
|
||||||
<td>
|
<td>
|
||||||
<p v-if="selectedNarrator?.id !== narrator.id" class="text-sm md:text-base text-gray-100">{{ narrator.name }}</p>
|
<nuxt-link v-if="selectedNarrator?.id !== narrator.id" :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${narrator.id}`" class="text-sm md:text-base text-gray-100 hover:underline">{{ narrator.name }}</nuxt-link>
|
||||||
<form v-else @submit.prevent="saveClick">
|
<form v-else @submit.prevent="saveClick">
|
||||||
<ui-text-input v-model="newNarratorName" />
|
<ui-text-input v-model="newNarratorName" />
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export default {
|
|||||||
return episodeIds
|
return episodeIds
|
||||||
},
|
},
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.$store.state.serverSettings.dateFormat
|
return this.$store.getters['getServerSetting']('dateFormat')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export default {
|
|||||||
})
|
})
|
||||||
results = {
|
results = {
|
||||||
podcasts: results?.podcast || [],
|
podcasts: results?.podcast || [],
|
||||||
|
episodes: results?.episodes || [],
|
||||||
books: results?.book || [],
|
books: results?.book || [],
|
||||||
authors: results?.authors || [],
|
authors: results?.authors || [],
|
||||||
series: results?.series || [],
|
series: results?.series || [],
|
||||||
@@ -61,6 +62,7 @@ export default {
|
|||||||
})
|
})
|
||||||
this.results = {
|
this.results = {
|
||||||
podcasts: results?.podcast || [],
|
podcasts: results?.podcast || [],
|
||||||
|
episodes: results?.episodes || [],
|
||||||
books: results?.book || [],
|
books: results?.book || [],
|
||||||
authors: results?.authors || [],
|
authors: results?.authors || [],
|
||||||
series: results?.series || [],
|
series: results?.series || [],
|
||||||
|
|||||||
+37
-6
@@ -40,6 +40,15 @@
|
|||||||
|
|
||||||
<p v-if="error" class="text-error text-center py-2">{{ error }}</p>
|
<p v-if="error" class="text-error text-center py-2">{{ error }}</p>
|
||||||
|
|
||||||
|
<div v-if="showNewAuthSystemMessage" class="mb-4">
|
||||||
|
<widgets-alert type="warning">
|
||||||
|
<div>
|
||||||
|
<p>{{ $strings.MessageAuthenticationSecurityMessage }}</p>
|
||||||
|
<a v-if="showNewAuthSystemAdminMessage" href="https://github.com/advplyr/audiobookshelf/discussions/4460" target="_blank" class="underline">{{ $strings.LabelMoreInfo }}</a>
|
||||||
|
</div>
|
||||||
|
</widgets-alert>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form v-show="login_local" @submit.prevent="submitForm">
|
<form v-show="login_local" @submit.prevent="submitForm">
|
||||||
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label>
|
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label>
|
||||||
<ui-text-input v-model.trim="username" :disabled="processing" class="mb-3 w-full" inputName="username" />
|
<ui-text-input v-model.trim="username" :disabled="processing" class="mb-3 w-full" inputName="username" />
|
||||||
@@ -85,7 +94,10 @@ export default {
|
|||||||
MetadataPath: '',
|
MetadataPath: '',
|
||||||
login_local: true,
|
login_local: true,
|
||||||
login_openid: false,
|
login_openid: false,
|
||||||
authFormData: null
|
authFormData: null,
|
||||||
|
// New JWT auth system re-login flags
|
||||||
|
showNewAuthSystemMessage: false,
|
||||||
|
showNewAuthSystemAdminMessage: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -177,13 +189,19 @@ export default {
|
|||||||
require('@/plugins/chromecast.js').default(this)
|
require('@/plugins/chromecast.js').default(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
|
this.$store.commit('libraries/setCurrentLibrary', { id: userDefaultLibraryId })
|
||||||
this.$store.commit('user/setUser', user)
|
this.$store.commit('user/setUser', user)
|
||||||
|
// Access token only returned from login, not authorize
|
||||||
|
if (user.accessToken) {
|
||||||
|
this.$store.commit('user/setAccessToken', user.accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
this.$store.dispatch('user/loadUserSettings')
|
this.$store.dispatch('user/loadUserSettings')
|
||||||
},
|
},
|
||||||
async submitForm() {
|
async submitForm() {
|
||||||
this.error = null
|
this.error = null
|
||||||
|
this.showNewAuthSystemMessage = false
|
||||||
|
this.showNewAuthSystemAdminMessage = false
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -210,6 +228,8 @@ export default {
|
|||||||
|
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
||||||
|
this.$store.commit('user/setAccessToken', token)
|
||||||
|
|
||||||
return this.$axios
|
return this.$axios
|
||||||
.$post('/api/authorize', null, {
|
.$post('/api/authorize', null, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -217,15 +237,25 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
// Force re-login if user is using an old token with no expiration
|
||||||
|
if (res.user.isOldToken) {
|
||||||
|
this.username = res.user.username
|
||||||
|
this.showNewAuthSystemMessage = true
|
||||||
|
// Admin user sees link to github discussion
|
||||||
|
this.showNewAuthSystemAdminMessage = res.user.type === 'admin' || res.user.type === 'root'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
this.setUser(res)
|
this.setUser(res)
|
||||||
this.processing = false
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Authorize error', error)
|
console.error('Authorize error', error)
|
||||||
this.processing = false
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
checkStatus() {
|
checkStatus() {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
@@ -280,8 +310,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
if (this.$route.query?.setToken) {
|
// Token passed as query parameter after successful oidc login
|
||||||
localStorage.setItem('token', this.$route.query.setToken)
|
if (this.$route.query?.accessToken) {
|
||||||
|
localStorage.setItem('token', this.$route.query.accessToken)
|
||||||
}
|
}
|
||||||
if (localStorage.getItem('token')) {
|
if (localStorage.getItem('token')) {
|
||||||
if (await this.checkAuth()) return // if valid user no need to check status
|
if (await this.checkAuth()) return // if valid user no need to check status
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-dvh max-h-dvh overflow-hidden" :style="{ backgroundColor: coverRgb }">
|
<div class="w-full max-w-full h-dvh max-h-dvh overflow-hidden" :style="{ backgroundColor: coverRgb }">
|
||||||
<div class="w-screen h-screen absolute inset-0 pointer-events-none" style="background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(38, 38, 38, 1) 80%)"></div>
|
<div class="w-screen h-screen absolute inset-0 pointer-events-none" style="background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(38, 38, 38, 1) 80%)"></div>
|
||||||
<div class="absolute inset-0 w-screen h-dvh flex items-center justify-center z-10">
|
<div class="absolute inset-0 w-screen h-dvh flex items-center justify-center z-10">
|
||||||
<div class="w-full p-2 sm:p-4 md:p-8">
|
<div class="w-full p-2 sm:p-4 md:p-8">
|
||||||
@@ -335,8 +335,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
resize() {
|
resize() {
|
||||||
this.windowWidth = window.innerWidth
|
setTimeout(() => {
|
||||||
this.windowHeight = window.innerHeight
|
this.windowWidth = window.innerWidth
|
||||||
|
this.windowHeight = window.innerHeight
|
||||||
|
this.$store.commit('globals/updateWindowSize', { width: window.innerWidth, height: window.innerHeight })
|
||||||
|
}, 100)
|
||||||
},
|
},
|
||||||
playerError(error) {
|
playerError(error) {
|
||||||
console.error('Player error', error)
|
console.error('Player error', error)
|
||||||
|
|||||||
@@ -316,9 +316,8 @@ export default {
|
|||||||
.$post('/api/upload', form)
|
.$post('/api/upload', form)
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed to upload item', error)
|
||||||
var errorMessage = error.response && error.response.data ? error.response.data : 'Oops, something went wrong...'
|
this.$toast.error(error.response?.data || 'Oops, something went wrong...')
|
||||||
this.$toast.error(errorMessage)
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -360,15 +359,14 @@ export default {
|
|||||||
// Check if path already exists before starting upload
|
// Check if path already exists before starting upload
|
||||||
// uploading fails if path already exists
|
// uploading fails if path already exists
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const filepath = Path.join(this.selectedFolder.fullPath, item.directory)
|
|
||||||
const exists = await this.$axios
|
const exists = await this.$axios
|
||||||
.$post(`/api/filesystem/pathexists`, { filepath, directory: item.directory, folderPath: this.selectedFolder.fullPath })
|
.$post(`/api/filesystem/pathexists`, { directory: item.directory, folderPath: this.selectedFolder.fullPath })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.exists) {
|
if (data.exists) {
|
||||||
if (data.libraryItemTitle) {
|
if (data.libraryItemTitle) {
|
||||||
this.$toast.error(this.$getString('ToastUploaderItemExistsInSubdirectoryError', [data.libraryItemTitle]))
|
this.$toast.error(this.$getString('ToastUploaderItemExistsInSubdirectoryError', [data.libraryItemTitle]))
|
||||||
} else {
|
} else {
|
||||||
this.$toast.error(this.$getString('ToastUploaderFilepathExistsError', [filepath]))
|
this.$toast.error(this.$getString('ToastUploaderFilepathExistsError', [Path.join(this.selectedFolder.fullPath, item.directory)]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return data.exists
|
return data.exists
|
||||||
@@ -382,13 +380,9 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let itemsUploaded = 0
|
|
||||||
let itemsFailed = 0
|
|
||||||
for (const item of itemsToUpload) {
|
for (const item of itemsToUpload) {
|
||||||
this.updateItemCardStatus(item.index, 'uploading')
|
this.updateItemCardStatus(item.index, 'uploading')
|
||||||
const result = await this.uploadItem(item)
|
const result = await this.uploadItem(item)
|
||||||
if (result) itemsUploaded++
|
|
||||||
else itemsFailed++
|
|
||||||
this.updateItemCardStatus(item.index, result ? 'success' : 'failed')
|
this.updateItemCardStatus(item.index, result ? 'success' : 'failed')
|
||||||
}
|
}
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export default class AudioTrack {
|
export default class AudioTrack {
|
||||||
constructor(track, userToken, routerBasePath) {
|
constructor(track, sessionId, routerBasePath) {
|
||||||
this.index = track.index || 0
|
this.index = track.index || 0
|
||||||
this.startOffset = track.startOffset || 0 // Total time of all previous tracks
|
this.startOffset = track.startOffset || 0 // Total time of all previous tracks
|
||||||
this.duration = track.duration || 0
|
this.duration = track.duration || 0
|
||||||
@@ -8,28 +8,29 @@ export default class AudioTrack {
|
|||||||
this.mimeType = track.mimeType
|
this.mimeType = track.mimeType
|
||||||
this.metadata = track.metadata || {}
|
this.metadata = track.metadata || {}
|
||||||
|
|
||||||
this.userToken = userToken
|
this.sessionId = sessionId
|
||||||
this.routerBasePath = routerBasePath || ''
|
this.routerBasePath = routerBasePath || ''
|
||||||
|
if (this.contentUrl?.startsWith('/hls')) {
|
||||||
|
this.sessionTrackUrl = this.contentUrl
|
||||||
|
} else {
|
||||||
|
this.sessionTrackUrl = `/public/session/${sessionId}/track/${this.index}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used for CastPlayer
|
* Used for CastPlayer
|
||||||
*/
|
*/
|
||||||
get fullContentUrl() {
|
get fullContentUrl() {
|
||||||
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
|
return `${process.env.serverUrl}${this.sessionTrackUrl}`
|
||||||
}
|
}
|
||||||
return `${window.location.origin}${this.routerBasePath}${this.contentUrl}?token=${this.userToken}`
|
return `${window.location.origin}${this.routerBasePath}${this.sessionTrackUrl}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used for LocalPlayer
|
* Used for LocalPlayer
|
||||||
*/
|
*/
|
||||||
get relativeContentUrl() {
|
get relativeContentUrl() {
|
||||||
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
|
return `${this.routerBasePath}${this.sessionTrackUrl}`
|
||||||
|
|
||||||
return `${this.routerBasePath}${this.contentUrl}?token=${this.userToken}`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,6 @@ export default class PlayerHandler {
|
|||||||
get isPlayingLocalItem() {
|
get isPlayingLocalItem() {
|
||||||
return this.libraryItem && this.player instanceof LocalAudioPlayer
|
return this.libraryItem && this.player instanceof LocalAudioPlayer
|
||||||
}
|
}
|
||||||
get userToken() {
|
|
||||||
return this.ctx.$store.getters['user/getToken']
|
|
||||||
}
|
|
||||||
get playerPlaying() {
|
get playerPlaying() {
|
||||||
return this.playerState === 'PLAYING'
|
return this.playerState === 'PLAYING'
|
||||||
}
|
}
|
||||||
@@ -226,7 +223,7 @@ export default class PlayerHandler {
|
|||||||
|
|
||||||
console.log('[PlayerHandler] Preparing Session', session)
|
console.log('[PlayerHandler] Preparing Session', session)
|
||||||
|
|
||||||
var audioTracks = session.audioTracks.map((at) => new AudioTrack(at, this.userToken, this.ctx.$config.routerBasePath))
|
var audioTracks = session.audioTracks.map((at) => new AudioTrack(at, session.id, this.ctx.$config.routerBasePath))
|
||||||
|
|
||||||
this.ctx.playerLoading = true
|
this.ctx.playerLoading = true
|
||||||
this.isHlsTranscode = true
|
this.isHlsTranscode = true
|
||||||
|
|||||||
+88
-3
@@ -1,4 +1,19 @@
|
|||||||
export default function ({ $axios, store, $config }) {
|
export default function ({ $axios, store, $root, app }) {
|
||||||
|
// Track if we're currently refreshing to prevent multiple refresh attempts
|
||||||
|
let isRefreshing = false
|
||||||
|
let failedQueue = []
|
||||||
|
|
||||||
|
const processQueue = (error, token = null) => {
|
||||||
|
failedQueue.forEach(({ resolve, reject }) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error)
|
||||||
|
} else {
|
||||||
|
resolve(token)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
failedQueue = []
|
||||||
|
}
|
||||||
|
|
||||||
$axios.onRequest((config) => {
|
$axios.onRequest((config) => {
|
||||||
if (!config.url) {
|
if (!config.url) {
|
||||||
console.error('Axios request invalid config', config)
|
console.error('Axios request invalid config', config)
|
||||||
@@ -7,7 +22,7 @@ export default function ({ $axios, store, $config }) {
|
|||||||
if (config.url.startsWith('http:') || config.url.startsWith('https:')) {
|
if (config.url.startsWith('http:') || config.url.startsWith('https:')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const bearerToken = store.state.user.user?.token || null
|
const bearerToken = store.getters['user/getToken']
|
||||||
if (bearerToken) {
|
if (bearerToken) {
|
||||||
config.headers.common['Authorization'] = `Bearer ${bearerToken}`
|
config.headers.common['Authorization'] = `Bearer ${bearerToken}`
|
||||||
}
|
}
|
||||||
@@ -17,9 +32,79 @@ export default function ({ $axios, store, $config }) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$axios.onError((error) => {
|
$axios.onError(async (error) => {
|
||||||
|
const originalRequest = error.config
|
||||||
const code = parseInt(error.response && error.response.status)
|
const code = parseInt(error.response && error.response.status)
|
||||||
const message = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
|
const message = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
|
||||||
|
|
||||||
console.error('Axios error', code, message)
|
console.error('Axios error', code, message)
|
||||||
|
|
||||||
|
// Handle 401 Unauthorized (token expired)
|
||||||
|
if (code === 401 && !originalRequest._retry) {
|
||||||
|
// Skip refresh for auth endpoints to prevent infinite loops
|
||||||
|
if (originalRequest.url === '/auth/refresh' || originalRequest.url === '/login') {
|
||||||
|
// Refresh failed or login failed, redirect to login
|
||||||
|
store.commit('user/setUser', null)
|
||||||
|
store.commit('user/setAccessToken', null)
|
||||||
|
app.router.push('/login')
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRefreshing) {
|
||||||
|
// If already refreshing, queue this request
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
failedQueue.push({ resolve, reject })
|
||||||
|
})
|
||||||
|
.then((token) => {
|
||||||
|
if (!originalRequest.headers) {
|
||||||
|
originalRequest.headers = {}
|
||||||
|
}
|
||||||
|
originalRequest.headers['Authorization'] = `Bearer ${token}`
|
||||||
|
return $axios(originalRequest)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
return Promise.reject(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
originalRequest._retry = true
|
||||||
|
isRefreshing = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Attempt to refresh the token
|
||||||
|
// Updates store if successful, otherwise clears store and throw error
|
||||||
|
const newAccessToken = await store.dispatch('user/refreshToken')
|
||||||
|
if (!newAccessToken) {
|
||||||
|
console.error('No new access token received')
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the original request with new token
|
||||||
|
if (!originalRequest.headers) {
|
||||||
|
originalRequest.headers = {}
|
||||||
|
}
|
||||||
|
originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`
|
||||||
|
|
||||||
|
// Process any queued requests
|
||||||
|
processQueue(null, newAccessToken)
|
||||||
|
|
||||||
|
// Retry the original request
|
||||||
|
return $axios(originalRequest)
|
||||||
|
} catch (refreshError) {
|
||||||
|
console.error('Token refresh failed:', refreshError)
|
||||||
|
|
||||||
|
// Process queued requests with error
|
||||||
|
processQueue(refreshError, null)
|
||||||
|
|
||||||
|
// Redirect to login
|
||||||
|
app.router.push('/login')
|
||||||
|
|
||||||
|
return Promise.reject(refreshError)
|
||||||
|
} finally {
|
||||||
|
isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const SupportedFileTypes = {
|
const SupportedFileTypes = {
|
||||||
image: ['png', 'jpg', 'jpeg', 'webp'],
|
image: ['png', 'jpg', 'jpeg', 'webp'],
|
||||||
audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma', 'mka', 'awb', 'caf', 'mpeg', 'mpg'],
|
audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'aif','wav', 'webm', 'webma', 'mka', 'awb', 'caf', 'mpeg', 'mpg'],
|
||||||
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
||||||
info: ['nfo'],
|
info: ['nfo'],
|
||||||
text: ['txt'],
|
text: ['txt'],
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { supplant } from './utils'
|
|||||||
const defaultCode = 'en-us'
|
const defaultCode = 'en-us'
|
||||||
|
|
||||||
const languageCodeMap = {
|
const languageCodeMap = {
|
||||||
|
ar: { label: 'عربي', dateFnsLocale: 'ar' },
|
||||||
bg: { label: 'Български', dateFnsLocale: 'bg' },
|
bg: { label: 'Български', dateFnsLocale: 'bg' },
|
||||||
bn: { label: 'বাংলা', dateFnsLocale: 'bn' },
|
bn: { label: 'বাংলা', dateFnsLocale: 'bn' },
|
||||||
ca: { label: 'Català', dateFnsLocale: 'ca' },
|
ca: { label: 'Català', dateFnsLocale: 'ca' },
|
||||||
|
|||||||
@@ -37,6 +37,48 @@ Vue.prototype.$elapsedPretty = (seconds, useFullNames = false, useMilliseconds =
|
|||||||
return `${hours} ${useFullNames ? `hour${hours === 1 ? '' : 's'}` : 'hr'} ${minutes} ${useFullNames ? `minute${minutes === 1 ? '' : 's'}` : 'min'}`
|
return `${hours} ${useFullNames ? `hour${hours === 1 ? '' : 's'}` : 'hr'} ${minutes} ${useFullNames ? `minute${minutes === 1 ? '' : 's'}` : 'min'}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Vue.prototype.$elapsedPrettyLocalized = (seconds, useFullNames = false, useMilliseconds = false) => {
|
||||||
|
if (isNaN(seconds) || seconds === null) return ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const df = new Intl.DurationFormat(Vue.prototype.$languageCodes.current, {
|
||||||
|
style: useFullNames ? 'long' : 'short'
|
||||||
|
})
|
||||||
|
|
||||||
|
const duration = {}
|
||||||
|
|
||||||
|
if (seconds < 60) {
|
||||||
|
if (useMilliseconds && seconds < 1) {
|
||||||
|
duration.milliseconds = Math.floor(seconds * 1000)
|
||||||
|
} else {
|
||||||
|
duration.seconds = Math.floor(seconds)
|
||||||
|
}
|
||||||
|
} else if (seconds < 3600) {
|
||||||
|
// 1 hour
|
||||||
|
duration.minutes = Math.floor(seconds / 60)
|
||||||
|
} else if (seconds < 86400) {
|
||||||
|
// 1 day
|
||||||
|
duration.hours = Math.floor(seconds / 3600)
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60)
|
||||||
|
if (minutes > 0) {
|
||||||
|
duration.minutes = minutes
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
duration.days = Math.floor(seconds / 86400)
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600)
|
||||||
|
if (hours > 0) {
|
||||||
|
duration.hours = hours
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return df.format(duration)
|
||||||
|
} catch (error) {
|
||||||
|
// Handle not supported
|
||||||
|
console.warn('Intl.DurationFormat not supported, not localizing duration')
|
||||||
|
return Vue.prototype.$elapsedPretty(seconds, useFullNames, useMilliseconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Vue.prototype.$secondsToTimestamp = (seconds, includeMs = false, alwaysIncludeHours = false) => {
|
Vue.prototype.$secondsToTimestamp = (seconds, includeMs = false, alwaysIncludeHours = false) => {
|
||||||
if (!seconds) {
|
if (!seconds) {
|
||||||
return alwaysIncludeHours ? '00:00:00' : '0:00'
|
return alwaysIncludeHours ? '00:00:00' : '0:00'
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export const actions = {
|
|||||||
commit('setNumUserPlaylists', numUserPlaylists)
|
commit('setNumUserPlaylists', numUserPlaylists)
|
||||||
commit('scanners/setCustomMetadataProviders', customMetadataProviders, { root: true })
|
commit('scanners/setCustomMetadataProviders', customMetadataProviders, { root: true })
|
||||||
|
|
||||||
commit('setCurrentLibrary', libraryId)
|
commit('setCurrentLibrary', { id: libraryId })
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -182,8 +182,8 @@ export const mutations = {
|
|||||||
setLibraryIssues(state, val) {
|
setLibraryIssues(state, val) {
|
||||||
state.issues = val
|
state.issues = val
|
||||||
},
|
},
|
||||||
setCurrentLibrary(state, val) {
|
setCurrentLibrary(state, { id }) {
|
||||||
state.currentLibraryId = val
|
state.currentLibraryId = id
|
||||||
},
|
},
|
||||||
set(state, libraries) {
|
set(state, libraries) {
|
||||||
state.libraries = libraries
|
state.libraries = libraries
|
||||||
|
|||||||
+36
-12
@@ -1,5 +1,6 @@
|
|||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
user: null,
|
user: null,
|
||||||
|
accessToken: null,
|
||||||
settings: {
|
settings: {
|
||||||
orderBy: 'media.metadata.title',
|
orderBy: 'media.metadata.title',
|
||||||
orderDesc: false,
|
orderDesc: false,
|
||||||
@@ -25,19 +26,19 @@ export const getters = {
|
|||||||
getIsRoot: (state) => state.user && state.user.type === 'root',
|
getIsRoot: (state) => state.user && state.user.type === 'root',
|
||||||
getIsAdminOrUp: (state) => state.user && (state.user.type === 'admin' || state.user.type === 'root'),
|
getIsAdminOrUp: (state) => state.user && (state.user.type === 'admin' || state.user.type === 'root'),
|
||||||
getToken: (state) => {
|
getToken: (state) => {
|
||||||
return state.user?.token || null
|
return state.accessToken || null
|
||||||
},
|
},
|
||||||
getUserMediaProgress:
|
getUserMediaProgress:
|
||||||
(state) =>
|
(state) =>
|
||||||
(libraryItemId, episodeId = null) => {
|
(libraryItemId, episodeId = null) => {
|
||||||
if (!state.user.mediaProgress) return null
|
if (!state.user?.mediaProgress) return null
|
||||||
return state.user.mediaProgress.find((li) => {
|
return state.user.mediaProgress.find((li) => {
|
||||||
if (episodeId && li.episodeId !== episodeId) return false
|
if (episodeId && li.episodeId !== episodeId) return false
|
||||||
return li.libraryItemId == libraryItemId
|
return li.libraryItemId == libraryItemId
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getUserBookmarksForItem: (state) => (libraryItemId) => {
|
getUserBookmarksForItem: (state) => (libraryItemId) => {
|
||||||
if (!state.user.bookmarks) return []
|
if (!state.user?.bookmarks) return []
|
||||||
return state.user.bookmarks.filter((bm) => bm.libraryItemId === libraryItemId)
|
return state.user.bookmarks.filter((bm) => bm.libraryItemId === libraryItemId)
|
||||||
},
|
},
|
||||||
getUserSetting: (state) => (key) => {
|
getUserSetting: (state) => (key) => {
|
||||||
@@ -58,6 +59,9 @@ export const getters = {
|
|||||||
getUserCanAccessAllLibraries: (state) => {
|
getUserCanAccessAllLibraries: (state) => {
|
||||||
return !!state.user?.permissions?.accessAllLibraries
|
return !!state.user?.permissions?.accessAllLibraries
|
||||||
},
|
},
|
||||||
|
getUserCanAccessExplicitContent: (state) => {
|
||||||
|
return !!state.user?.permissions?.accessExplicitContent
|
||||||
|
},
|
||||||
getLibrariesAccessible: (state, getters) => {
|
getLibrariesAccessible: (state, getters) => {
|
||||||
if (!state.user) return []
|
if (!state.user) return []
|
||||||
if (getters.getUserCanAccessAllLibraries) return []
|
if (getters.getUserCanAccessAllLibraries) return []
|
||||||
@@ -88,7 +92,7 @@ export const actions = {
|
|||||||
if (state.settings.orderBy == 'media.duration') {
|
if (state.settings.orderBy == 'media.duration') {
|
||||||
settingsUpdate.orderBy = 'media.numTracks'
|
settingsUpdate.orderBy = 'media.numTracks'
|
||||||
}
|
}
|
||||||
if (state.settings.orderBy == 'media.metadata.publishedYear') {
|
if (state.settings.orderBy == 'media.metadata.publishedYear' || state.settings.orderBy == 'progress') {
|
||||||
settingsUpdate.orderBy = 'media.metadata.title'
|
settingsUpdate.orderBy = 'media.metadata.title'
|
||||||
}
|
}
|
||||||
const invalidFilters = ['series', 'authors', 'narrators', 'publishers', 'publishedDecades', 'languages', 'progress', 'issues', 'ebooks', 'abridged']
|
const invalidFilters = ['series', 'authors', 'narrators', 'publishers', 'publishedDecades', 'languages', 'progress', 'issues', 'ebooks', 'abridged']
|
||||||
@@ -142,21 +146,41 @@ export const actions = {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load userSettings from local storage', error)
|
console.error('Failed to load userSettings from local storage', error)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
refreshToken({ state, commit }) {
|
||||||
|
return this.$axios
|
||||||
|
.$post('/auth/refresh')
|
||||||
|
.then(async (response) => {
|
||||||
|
const newAccessToken = response.user.accessToken
|
||||||
|
commit('setAccessToken', newAccessToken)
|
||||||
|
// Emit event used to re-authenticate socket in default.vue since $root is not available here
|
||||||
|
if (this.$eventBus) {
|
||||||
|
this.$eventBus.$emit('token_refreshed', newAccessToken)
|
||||||
|
}
|
||||||
|
return newAccessToken
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to refresh token', error)
|
||||||
|
commit('setUser', null)
|
||||||
|
commit('setAccessToken', null)
|
||||||
|
// Calling function handles redirect to login
|
||||||
|
throw error
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
setUser(state, user) {
|
setUser(state, user) {
|
||||||
state.user = user
|
state.user = user
|
||||||
if (user) {
|
|
||||||
if (user.token) localStorage.setItem('token', user.token)
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem('token')
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
setUserToken(state, token) {
|
setAccessToken(state, token) {
|
||||||
state.user.token = token
|
if (!token) {
|
||||||
localStorage.setItem('token', token)
|
localStorage.removeItem('token')
|
||||||
|
state.accessToken = null
|
||||||
|
} else {
|
||||||
|
state.accessToken = token
|
||||||
|
localStorage.setItem('token', token)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
updateMediaProgress(state, { id, data }) {
|
updateMediaProgress(state, { id, data }) {
|
||||||
if (!state.user) return
|
if (!state.user) return
|
||||||
|
|||||||
+965
-12
File diff suppressed because it is too large
Load Diff
+157
-13
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"ButtonAdd": "Дадаць",
|
"ButtonAdd": "Дадаць",
|
||||||
|
"ButtonAddApiKey": "Дадаць API-ключ",
|
||||||
"ButtonAddChapters": "Дадаць раздзелы",
|
"ButtonAddChapters": "Дадаць раздзелы",
|
||||||
"ButtonAddDevice": "Дадаць прыладу",
|
"ButtonAddDevice": "Дадаць прыладу",
|
||||||
"ButtonAddLibrary": "Дадаць бібліятэку",
|
"ButtonAddLibrary": "Дадаць бібліятэку",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"ButtonChooseAFolder": "Выбраць тэчку",
|
"ButtonChooseAFolder": "Выбраць тэчку",
|
||||||
"ButtonChooseFiles": "Выбраць файлы",
|
"ButtonChooseFiles": "Выбраць файлы",
|
||||||
"ButtonClearFilter": "Ачысціць фільтр",
|
"ButtonClearFilter": "Ачысціць фільтр",
|
||||||
|
"ButtonClose": "Закрыць",
|
||||||
"ButtonCloseFeed": "Закрыць стужку",
|
"ButtonCloseFeed": "Закрыць стужку",
|
||||||
"ButtonCloseSession": "Закрыць адкрыты сеанс",
|
"ButtonCloseSession": "Закрыць адкрыты сеанс",
|
||||||
"ButtonCollections": "Калекцыі",
|
"ButtonCollections": "Калекцыі",
|
||||||
@@ -69,7 +71,7 @@
|
|||||||
"ButtonQueueAddItem": "Дадаць у чаргу",
|
"ButtonQueueAddItem": "Дадаць у чаргу",
|
||||||
"ButtonQueueRemoveItem": "Выдаліць з чаргі",
|
"ButtonQueueRemoveItem": "Выдаліць з чаргі",
|
||||||
"ButtonQuickEmbed": "Хуткае ўбудаванне",
|
"ButtonQuickEmbed": "Хуткае ўбудаванне",
|
||||||
"ButtonQuickEmbedMetadata": "Хуткае ўбудаванне метаданых",
|
"ButtonQuickEmbedMetadata": "Хуткае ўбудаванне метададзеных",
|
||||||
"ButtonQuickMatch": "Хуткі пошук",
|
"ButtonQuickMatch": "Хуткі пошук",
|
||||||
"ButtonReScan": "Паўторнае сканаванне",
|
"ButtonReScan": "Паўторнае сканаванне",
|
||||||
"ButtonRead": "Чытаць",
|
"ButtonRead": "Чытаць",
|
||||||
@@ -98,8 +100,9 @@
|
|||||||
"ButtonSetChaptersFromTracks": "Усталяваць раздзелы з трэкаў",
|
"ButtonSetChaptersFromTracks": "Усталяваць раздзелы з трэкаў",
|
||||||
"ButtonShare": "Падзяліцца",
|
"ButtonShare": "Падзяліцца",
|
||||||
"ButtonShiftTimes": "Карэкцыя часу",
|
"ButtonShiftTimes": "Карэкцыя часу",
|
||||||
|
"ButtonShow": "Паказаць",
|
||||||
"ButtonStartM4BEncode": "Пачаць кадзіраванне ў M4B",
|
"ButtonStartM4BEncode": "Пачаць кадзіраванне ў M4B",
|
||||||
"ButtonStartMetadataEmbed": "Пачаць убудаванне метаданых",
|
"ButtonStartMetadataEmbed": "Пачаць убудаванне метададзеных",
|
||||||
"ButtonStats": "Статыстыка",
|
"ButtonStats": "Статыстыка",
|
||||||
"ButtonSubmit": "Адправіць",
|
"ButtonSubmit": "Адправіць",
|
||||||
"ButtonTest": "Тэст",
|
"ButtonTest": "Тэст",
|
||||||
@@ -107,7 +110,7 @@
|
|||||||
"ButtonUpload": "Загрузіць",
|
"ButtonUpload": "Загрузіць",
|
||||||
"ButtonUploadBackup": "Загрузіць рэзервовую копію",
|
"ButtonUploadBackup": "Загрузіць рэзервовую копію",
|
||||||
"ButtonUploadCover": "Загрузіць вокладку",
|
"ButtonUploadCover": "Загрузіць вокладку",
|
||||||
"ButtonUploadOPMLFile": "Загрузіць OPML файл",
|
"ButtonUploadOPMLFile": "Загрузіць файл OPML",
|
||||||
"ButtonUserDelete": "Выдаліць карыстальніка {0}",
|
"ButtonUserDelete": "Выдаліць карыстальніка {0}",
|
||||||
"ButtonUserEdit": "Рэдагаваць карыстальніка {0}",
|
"ButtonUserEdit": "Рэдагаваць карыстальніка {0}",
|
||||||
"ButtonViewAll": "Прагледзець усе",
|
"ButtonViewAll": "Прагледзець усе",
|
||||||
@@ -116,8 +119,9 @@
|
|||||||
"ErrorUploadFetchMetadataNoResults": "Не ўдалося атрымаць метададзеныя – паспрабуйце абнавіць назву і/або аўтара",
|
"ErrorUploadFetchMetadataNoResults": "Не ўдалося атрымаць метададзеныя – паспрабуйце абнавіць назву і/або аўтара",
|
||||||
"ErrorUploadLacksTitle": "Павінна быць назва",
|
"ErrorUploadLacksTitle": "Павінна быць назва",
|
||||||
"HeaderAccount": "Уліковы запіс",
|
"HeaderAccount": "Уліковы запіс",
|
||||||
"HeaderAddCustomMetadataProvider": "Дадаць карыстальніцкага пастаўшчыка метаданных",
|
"HeaderAddCustomMetadataProvider": "Дадаць карыстальніцкага пастаўшчыка метададзенных",
|
||||||
"HeaderAdvanced": "Дадаткова",
|
"HeaderAdvanced": "Дадаткова",
|
||||||
|
"HeaderApiKeys": "API-ключы",
|
||||||
"HeaderAppriseNotificationSettings": "Налады апавяшчэнняў Apprise",
|
"HeaderAppriseNotificationSettings": "Налады апавяшчэнняў Apprise",
|
||||||
"HeaderAudioTracks": "Аўдыядарожкі",
|
"HeaderAudioTracks": "Аўдыядарожкі",
|
||||||
"HeaderAudiobookTools": "Сродкі кіравання файламі аўдыякніг",
|
"HeaderAudiobookTools": "Сродкі кіравання файламі аўдыякніг",
|
||||||
@@ -157,9 +161,11 @@
|
|||||||
"HeaderManageGenres": "Кіраванне жанрамі",
|
"HeaderManageGenres": "Кіраванне жанрамі",
|
||||||
"HeaderManageTags": "Кіраванне тэгамі",
|
"HeaderManageTags": "Кіраванне тэгамі",
|
||||||
"HeaderMapDetails": "Падрабязнасці адлюстравання",
|
"HeaderMapDetails": "Падрабязнасці адлюстравання",
|
||||||
|
"HeaderMatch": "Супадзенне",
|
||||||
"HeaderMetadataOrderOfPrecedence": "Парадак прыярытэтнасці метададзеных",
|
"HeaderMetadataOrderOfPrecedence": "Парадак прыярытэтнасці метададзеных",
|
||||||
"HeaderMetadataToEmbed": "Метададзеныя для ўбудавання",
|
"HeaderMetadataToEmbed": "Метададзеныя для ўбудавання",
|
||||||
"HeaderNewAccount": "Новы ўліковы запіс",
|
"HeaderNewAccount": "Новы ўліковы запіс",
|
||||||
|
"HeaderNewApiKey": "Новы API-ключ",
|
||||||
"HeaderNewLibrary": "Новая бібліятэка",
|
"HeaderNewLibrary": "Новая бібліятэка",
|
||||||
"HeaderNotificationCreate": "Стварыць апавяшчэнне",
|
"HeaderNotificationCreate": "Стварыць апавяшчэнне",
|
||||||
"HeaderNotificationUpdate": "Абнавіць апавяшчэнне",
|
"HeaderNotificationUpdate": "Абнавіць апавяшчэнне",
|
||||||
@@ -175,9 +181,10 @@
|
|||||||
"HeaderPlaylist": "Спіс прайгравання",
|
"HeaderPlaylist": "Спіс прайгравання",
|
||||||
"HeaderPlaylistItems": "Элементы спіса прайгравання",
|
"HeaderPlaylistItems": "Элементы спіса прайгравання",
|
||||||
"HeaderPodcastsToAdd": "Падкасты для дадання",
|
"HeaderPodcastsToAdd": "Падкасты для дадання",
|
||||||
|
"HeaderPresets": "Прадустаноўкі",
|
||||||
"HeaderPreviewCover": "Прадпрагляд вокладкі",
|
"HeaderPreviewCover": "Прадпрагляд вокладкі",
|
||||||
"HeaderRSSFeedGeneral": "Падрабязнасці RSS",
|
"HeaderRSSFeedGeneral": "Падрабязнасці RSS",
|
||||||
"HeaderRSSFeedIsOpen": "RSS-стужка адкрыта",
|
"HeaderRSSFeedIsOpen": "RSS-стужка адкрытая",
|
||||||
"HeaderRSSFeeds": "RSS-стужкі",
|
"HeaderRSSFeeds": "RSS-стужкі",
|
||||||
"HeaderRemoveEpisode": "Выдаліць эпізод",
|
"HeaderRemoveEpisode": "Выдаліць эпізод",
|
||||||
"HeaderRemoveEpisodes": "Выдаліць {0} эпізодаў",
|
"HeaderRemoveEpisodes": "Выдаліць {0} эпізодаў",
|
||||||
@@ -203,6 +210,7 @@
|
|||||||
"HeaderTableOfContents": "Змест",
|
"HeaderTableOfContents": "Змест",
|
||||||
"HeaderTools": "Інструменты",
|
"HeaderTools": "Інструменты",
|
||||||
"HeaderUpdateAccount": "Абнавіць уліковы запіс",
|
"HeaderUpdateAccount": "Абнавіць уліковы запіс",
|
||||||
|
"HeaderUpdateApiKey": "Абнавіць API-ключ",
|
||||||
"HeaderUpdateAuthor": "Абнавіць аўтара",
|
"HeaderUpdateAuthor": "Абнавіць аўтара",
|
||||||
"HeaderUpdateDetails": "Абнавіць падрабязнасці",
|
"HeaderUpdateDetails": "Абнавіць падрабязнасці",
|
||||||
"HeaderUpdateLibrary": "Абнавіць бібліятэку",
|
"HeaderUpdateLibrary": "Абнавіць бібліятэку",
|
||||||
@@ -227,10 +235,15 @@
|
|||||||
"LabelAddedDate": "Дададзена {0}",
|
"LabelAddedDate": "Дададзена {0}",
|
||||||
"LabelAdminUsersOnly": "Толькі для адміністратараў",
|
"LabelAdminUsersOnly": "Толькі для адміністратараў",
|
||||||
"LabelAll": "Усе",
|
"LabelAll": "Усе",
|
||||||
|
"LabelAllEpisodesDownloaded": "Усе эпізоды спампаваныя",
|
||||||
"LabelAllUsers": "Усе карыстальнікі",
|
"LabelAllUsers": "Усе карыстальнікі",
|
||||||
"LabelAllUsersExcludingGuests": "Усе карыстальнікі, акрамя гасцей",
|
"LabelAllUsersExcludingGuests": "Усе карыстальнікі, акрамя гасцей",
|
||||||
"LabelAllUsersIncludingGuests": "Усе карыстальнікі, уключаючы гасцей",
|
"LabelAllUsersIncludingGuests": "Усе карыстальнікі, уключаючы гасцей",
|
||||||
"LabelAlreadyInYourLibrary": "Ужо ў вашай бібліятэцы",
|
"LabelAlreadyInYourLibrary": "Ужо ў вашай бібліятэцы",
|
||||||
|
"LabelApiKeyCreated": "API-ключ \"{0}\" паспяхова створаны.",
|
||||||
|
"LabelApiKeyCreatedDescription": "Пераканайцеся, што вы скапіявалі API-ключ зараз, бо паўторна яго ўбачыць не атрымаецца.",
|
||||||
|
"LabelApiKeyUser": "Дзейнічаць ад імя карыстальніка",
|
||||||
|
"LabelApiKeyUserDescription": "Гэты API-ключ будзе мець тыя ж правы, што і карыстальнік, ад імя якога ён дзейнічае. У журналах гэта будзе выглядаць так, быццам запыт робіць сам карыстальнік.",
|
||||||
"LabelApiToken": "Токен API",
|
"LabelApiToken": "Токен API",
|
||||||
"LabelAppend": "Дадаць",
|
"LabelAppend": "Дадаць",
|
||||||
"LabelAudioBitrate": "Бітрэйт аўдыё (напрыклад, 128к)",
|
"LabelAudioBitrate": "Бітрэйт аўдыё (напрыклад, 128к)",
|
||||||
@@ -242,39 +255,108 @@
|
|||||||
"LabelAuthors": "Аўтары",
|
"LabelAuthors": "Аўтары",
|
||||||
"LabelAutoDownloadEpisodes": "Аўтаматычнае спампаванне эпізодаў",
|
"LabelAutoDownloadEpisodes": "Аўтаматычнае спампаванне эпізодаў",
|
||||||
"LabelAutoFetchMetadata": "Аўтаматычнае атрыманне метададзеных",
|
"LabelAutoFetchMetadata": "Аўтаматычнае атрыманне метададзеных",
|
||||||
|
"LabelAutoFetchMetadataHelp": "Атрыманне звестак пра назву, аўтара і серыю для падыходнага фарматавання перад загрузкай. Далей можа быць неабходна дапоўніць метададзеныя.",
|
||||||
|
"LabelAutoLaunch": "Аўтазапуск",
|
||||||
|
"LabelAutoLaunchDescription": "Аўтаматычна перанакіроўваць да пастаўшчыка аўтэнтыфікацыі пры переходзе на старонку ўваходу (ручное пераключэнне праз шлях <code>/login?autoLaunch=0</code>)",
|
||||||
|
"LabelAutoRegister": "Аўтарэгістрацыя",
|
||||||
|
"LabelAutoRegisterDescription": "Аўтаматычна ствараць новых карыстальнікаў пасля ўваходу ў сістэму",
|
||||||
|
"LabelBackToUser": "Вярнуцца да карыстальніка",
|
||||||
"LabelBackupAudioFiles": "Рэзервовае капіраванне аўдыёфайлаў",
|
"LabelBackupAudioFiles": "Рэзервовае капіраванне аўдыёфайлаў",
|
||||||
"LabelBackupLocation": "Месцазнаходжанне рэзервовых копій",
|
"LabelBackupLocation": "Месцазнаходжанне рэзервовых копій",
|
||||||
|
"LabelBackupsEnableAutomaticBackups": "Аўтаматычнае рэзервовае капіраванне",
|
||||||
|
"LabelBackupsEnableAutomaticBackupsHelp": "Рэзервовыя копіі захаваныя ў /metadata/backups",
|
||||||
|
"LabelBackupsMaxBackupSize": "Максімальны памер рэзервовай копіі (у ГБ) (0 — неабмежавана)",
|
||||||
|
"LabelBackupsMaxBackupSizeHelp": "Для таго, каб пазбегнуць няправільных налад, рэзервовыя копіі не будуць створаны, калі іх памер будзе больш за дапушчальны.",
|
||||||
|
"LabelBackupsNumberToKeep": "Колькасць захаваных рэзервовых копій",
|
||||||
"LabelBackupsNumberToKeepHelp": "Адначасова будзе выдаляцца толькі 1 рэзервовая копія, таму, калі ў вас іх больш, вам варта выдаліць іх уручную.",
|
"LabelBackupsNumberToKeepHelp": "Адначасова будзе выдаляцца толькі 1 рэзервовая копія, таму, калі ў вас іх больш, вам варта выдаліць іх уручную.",
|
||||||
|
"LabelBitrate": "Бітрэйт",
|
||||||
|
"LabelBonus": "Бонус",
|
||||||
"LabelBooks": "Кнігі",
|
"LabelBooks": "Кнігі",
|
||||||
|
"LabelButtonText": "Тэкст кнопкі",
|
||||||
|
"LabelByAuthor": "ад {0}",
|
||||||
|
"LabelChangePassword": "Змяніць пароль",
|
||||||
|
"LabelChannels": "Каналы",
|
||||||
|
"LabelChapterCount": "{0} раздзелаў",
|
||||||
|
"LabelChapterTitle": "Назва раздзела",
|
||||||
"LabelChapters": "Раздзелы",
|
"LabelChapters": "Раздзелы",
|
||||||
|
"LabelChaptersFound": "раздзелаў знойдзена",
|
||||||
|
"LabelClickForMoreInfo": "Націсніце для больш падрабязнай інфармацыі",
|
||||||
|
"LabelClickToUseCurrentValue": "Націсніце, каб выкарыстоўваць бягучае значэнне",
|
||||||
"LabelClosePlayer": "Зачыніць прайгравальнік",
|
"LabelClosePlayer": "Зачыніць прайгравальнік",
|
||||||
|
"LabelCodec": "Кодэк",
|
||||||
"LabelCollapseSeries": "Згарнуць серыі",
|
"LabelCollapseSeries": "Згарнуць серыі",
|
||||||
|
"LabelCollapseSubSeries": "Згарнуць падсерыі",
|
||||||
|
"LabelCollection": "Калекцыя",
|
||||||
|
"LabelCollections": "Калекцыі",
|
||||||
"LabelComplete": "Завершана",
|
"LabelComplete": "Завершана",
|
||||||
|
"LabelConfirmPassword": "Пацвердзіце пароль",
|
||||||
"LabelContinueListening": "Працягваць слухаць",
|
"LabelContinueListening": "Працягваць слухаць",
|
||||||
"LabelContinueReading": "Працягнуць чытанне",
|
"LabelContinueReading": "Працягнуць чытанне",
|
||||||
"LabelContinueSeries": "Працягнуць серыі",
|
"LabelContinueSeries": "Працягнуць серыі",
|
||||||
|
"LabelCover": "Вокладка",
|
||||||
|
"LabelCoverImageURL": "URL выявы вокладкі",
|
||||||
|
"LabelCoverProvider": "Крыніца вокладак",
|
||||||
|
"LabelCreatedAt": "Дата стварэння",
|
||||||
|
"LabelCronExpression": "Запіс Cron",
|
||||||
|
"LabelCurrent": "Бягучы",
|
||||||
|
"LabelCurrently": "Бягучы:",
|
||||||
|
"LabelCustomCronExpression": "Уласны запіс Cron:",
|
||||||
"LabelDatetime": "Дата і час",
|
"LabelDatetime": "Дата і час",
|
||||||
|
"LabelDays": "Дзён",
|
||||||
|
"LabelDeleteFromFileSystemCheckbox": "Выдаліць з файлавай сістэмы (зніміце галачку, каб выдаліць толькі з базы даных)",
|
||||||
"LabelDescription": "Апісанне",
|
"LabelDescription": "Апісанне",
|
||||||
|
"LabelDeselectAll": "Скасаваць выбар усяго",
|
||||||
|
"LabelDevice": "Прылада",
|
||||||
|
"LabelDeviceInfo": "Інфармацыя пра прыладу",
|
||||||
|
"LabelDeviceIsAvailableTo": "Прылада даступная для...",
|
||||||
|
"LabelDirectory": "Каталог",
|
||||||
"LabelDiscFromFilename": "Дыск з імя файла",
|
"LabelDiscFromFilename": "Дыск з імя файла",
|
||||||
|
"LabelDiscFromMetadata": "Дыск па метададзеных",
|
||||||
"LabelDiscover": "Знайсці",
|
"LabelDiscover": "Знайсці",
|
||||||
"LabelDownload": "Спампаваць",
|
"LabelDownload": "Спампаваць",
|
||||||
"LabelDownloadNEpisodes": "Спампована {0} эпізодаў",
|
"LabelDownloadNEpisodes": "Спампована {0} эпізодаў",
|
||||||
"LabelDownloadable": "Спампоўваецца",
|
"LabelDownloadable": "Спампоўваецца",
|
||||||
"LabelDuration": "Працягласць",
|
"LabelDuration": "Працягласць",
|
||||||
|
"LabelDurationComparisonExactMatch": "(дакладнае супадзенне)",
|
||||||
|
"LabelDurationComparisonLonger": "(на {0} даўжэй)",
|
||||||
|
"LabelDurationComparisonShorter": "(на {0} карацей)",
|
||||||
|
"LabelDurationFound": "Знойдзеная працягласць:",
|
||||||
"LabelEbook": "Электронная кніга",
|
"LabelEbook": "Электронная кніга",
|
||||||
"LabelEbooks": "Электронныя кнігі",
|
"LabelEbooks": "Электронныя кнігі",
|
||||||
|
"LabelEdit": "Рэдагаваць",
|
||||||
|
"LabelEmail": "Электронная пошта",
|
||||||
|
"LabelEmailSettingsFromAddress": "Адрас адпраўніка",
|
||||||
|
"LabelEmailSettingsRejectUnauthorized": "Адхіляць неаўтарызаваныя сертыфікаты",
|
||||||
|
"LabelEmailSettingsRejectUnauthorizedHelp": "Адключэнне праверкі SSL-сертыфіката можа зрабіць ваша злучэнне ўразлівым перад пагрозамі бяспекі, такімі як атакі \"чалавек пасярэдзіне\". Адключайце гэтую опцыю толькі калі цалкам разумееце наступствы і ўпэўнены ў надзейнасці паштовага сервера.",
|
||||||
|
"LabelEmailSettingsSecure": "Бяспечныя",
|
||||||
|
"LabelEmailSettingsSecureHelp": "Калі ўключана, злучэнне будзе выкарыстоўваць TLS пры падключэнні да сервера. Калі выключана, TLS будзе выкарыстоўвацца толькі ў выпадку падтрымкі пашырэння STARTTLS на серверы. У большасці выпадкаў усталюйце значэнне true пры падключэнні да порта 465. Для партоў 587 або 25 не ўключайце яго. (інфармацыя з nodemailer.com/smtp/#authentication)",
|
||||||
|
"LabelEmailSettingsTestAddress": "Тэставы адрас",
|
||||||
|
"LabelEmbeddedCover": "Убудаваная вокладка",
|
||||||
"LabelEnable": "Уключыць",
|
"LabelEnable": "Уключыць",
|
||||||
"LabelEncodingBackupLocation": "Рэзервовая копія вашых арыгінальных аўдыёфайлаў будзе захавана ў:",
|
"LabelEncodingBackupLocation": "Рэзервовая копія вашых арыгінальных аўдыёфайлаў будзе захавана ў:",
|
||||||
"LabelEncodingChaptersNotEmbedded": "Раздзелы не ўбудаваны ў шматдарожкавыя аўдыякнігі.",
|
"LabelEncodingChaptersNotEmbedded": "Раздзелы не ўбудаваны ў шматдарожкавыя аўдыякнігі.",
|
||||||
|
"LabelEncodingClearItemCache": "Пераканайцеся, што перыядычна ачышчаеце кэш элементаў.",
|
||||||
"LabelEncodingFinishedM4B": "Гатовы файл M4B будзе змешчаны ў вашу тэчку з аўдыякнігамі па адрасе:",
|
"LabelEncodingFinishedM4B": "Гатовы файл M4B будзе змешчаны ў вашу тэчку з аўдыякнігамі па адрасе:",
|
||||||
"LabelEncodingInfoEmbedded": "Метаданыя будуць убудаваны ў аўдыядарожкі ўнутры вашай тэчкі з аўдыякнігамі.",
|
"LabelEncodingInfoEmbedded": "Метададзеныя будуць убудаваны ў аўдыядарожкі ўнутры вашай тэчкі з аўдыякнігамі.",
|
||||||
|
"LabelEncodingStartedNavigation": "Пасля запуску задачы вы можаце перайсці на іншую старонку.",
|
||||||
"LabelEncodingTimeWarning": "Кадаванне можа заняць да 30 хвілін.",
|
"LabelEncodingTimeWarning": "Кадаванне можа заняць да 30 хвілін.",
|
||||||
"LabelEnd": "Канец",
|
"LabelEnd": "Канец",
|
||||||
"LabelEndOfChapter": "Канец раздзела",
|
"LabelEndOfChapter": "Канец раздзела",
|
||||||
"LabelEpisode": "Эпізод",
|
"LabelEpisode": "Эпізод",
|
||||||
"LabelEpisodeNotLinkedToRssFeed": "Эпізод не звязаны з RSS-стужкай",
|
"LabelEpisodeNotLinkedToRssFeed": "Эпізод не звязаны з RSS-стужкай",
|
||||||
"LabelEpisodeUrlFromRssFeed": "URL эпізоду з RSS-стужкі",
|
"LabelEpisodeUrlFromRssFeed": "URL эпізоду з RSS-стужкі",
|
||||||
|
"LabelEpisodic": "Эпізадычны",
|
||||||
|
"LabelExample": "Прыклад",
|
||||||
|
"LabelExpandSeries": "Разгарнуць серыю",
|
||||||
|
"LabelExpandSubSeries": "Разгарнуць падсерыі",
|
||||||
|
"LabelExpired": "Пратэрмінаваны",
|
||||||
|
"LabelExpiresAt": "Тэрмін дзеяння заканчваецца ў",
|
||||||
|
"LabelExpiresInSeconds": "Тэрмін дзеяння заканчваецца праз (секунд)",
|
||||||
|
"LabelExpiresNever": "Ніколі",
|
||||||
|
"LabelExplicit": "Відверты",
|
||||||
|
"LabelExportOPML": "Экспарт OPML",
|
||||||
"LabelFeedURL": "URL стужкі",
|
"LabelFeedURL": "URL стужкі",
|
||||||
|
"LabelFetchingMetadata": "Атрыманне метададзеных",
|
||||||
"LabelFile": "Файл",
|
"LabelFile": "Файл",
|
||||||
"LabelFileBirthtime": "Час стварэння файла",
|
"LabelFileBirthtime": "Час стварэння файла",
|
||||||
"LabelFileModified": "Час змянення файла",
|
"LabelFileModified": "Час змянення файла",
|
||||||
@@ -289,6 +371,7 @@
|
|||||||
"LabelHasSupplementaryEbook": "Мае дадатковую электронную кнігу",
|
"LabelHasSupplementaryEbook": "Мае дадатковую электронную кнігу",
|
||||||
"LabelHideSubtitles": "Схаваць падзагалоўкі",
|
"LabelHideSubtitles": "Схаваць падзагалоўкі",
|
||||||
"LabelHost": "Хост",
|
"LabelHost": "Хост",
|
||||||
|
"LabelImageURLFromTheWeb": "URL выявы з інтэрнэту",
|
||||||
"LabelInProgress": "У працэсе",
|
"LabelInProgress": "У працэсе",
|
||||||
"LabelIncomplete": "Незавершана",
|
"LabelIncomplete": "Незавершана",
|
||||||
"LabelIntervalCustomDailyWeekly": "Карыстальніцкі штодзённы/штотыднёвы",
|
"LabelIntervalCustomDailyWeekly": "Карыстальніцкі штодзённы/штотыднёвы",
|
||||||
@@ -319,6 +402,7 @@
|
|||||||
"LabelLibraryFilterSublistEmpty": "Не {0}",
|
"LabelLibraryFilterSublistEmpty": "Не {0}",
|
||||||
"LabelLibraryItem": "Элемент бібліятэкі",
|
"LabelLibraryItem": "Элемент бібліятэкі",
|
||||||
"LabelLibraryName": "Імя бібліятэкі",
|
"LabelLibraryName": "Імя бібліятэкі",
|
||||||
|
"LabelLibrarySortByProgress": "Прагрэс абноўлены",
|
||||||
"LabelLimit": "Абмежаванне",
|
"LabelLimit": "Абмежаванне",
|
||||||
"LabelLineSpacing": "Міжрадковы інтэрвал",
|
"LabelLineSpacing": "Міжрадковы інтэрвал",
|
||||||
"LabelListenAgain": "Паслухаць зноў",
|
"LabelListenAgain": "Паслухаць зноў",
|
||||||
@@ -327,6 +411,8 @@
|
|||||||
"LabelMaxEpisodesToKeepHelp": "Значэнне 0 не ўстанаўлівае максімальнага абмежавання. Пасля аўтаматычнай спампоўкі новага эпізоду будзе выдалены самы стары эпізод, калі ў вас больш за X эпізодаў. Пры кожнай новай спампоўцы будзе выдаляцца толькі 1 эпізод.",
|
"LabelMaxEpisodesToKeepHelp": "Значэнне 0 не ўстанаўлівае максімальнага абмежавання. Пасля аўтаматычнай спампоўкі новага эпізоду будзе выдалены самы стары эпізод, калі ў вас больш за X эпізодаў. Пры кожнай новай спампоўцы будзе выдаляцца толькі 1 эпізод.",
|
||||||
"LabelMediaPlayer": "Медыяпрайгравальнік",
|
"LabelMediaPlayer": "Медыяпрайгравальнік",
|
||||||
"LabelMediaType": "Тып медыя",
|
"LabelMediaType": "Тып медыя",
|
||||||
|
"LabelMetadataOrderOfPrecedenceDescription": "Крыніцы метададзеных з вышэйшым прыярытэтам будуць замяняць крыніцы з ніжэйшым прыярытэтам",
|
||||||
|
"LabelMetadataProvider": "Пастаўшчык метададзеных",
|
||||||
"LabelMissing": "Адсутнічае",
|
"LabelMissing": "Адсутнічае",
|
||||||
"LabelMore": "Больш",
|
"LabelMore": "Больш",
|
||||||
"LabelMoreInfo": "Больш інфармацыі",
|
"LabelMoreInfo": "Больш інфармацыі",
|
||||||
@@ -335,6 +421,7 @@
|
|||||||
"LabelNarrators": "Чытальнікі",
|
"LabelNarrators": "Чытальнікі",
|
||||||
"LabelNewestAuthors": "Новыя аўтары",
|
"LabelNewestAuthors": "Новыя аўтары",
|
||||||
"LabelNewestEpisodes": "Новыя эпізоды",
|
"LabelNewestEpisodes": "Новыя эпізоды",
|
||||||
|
"LabelNoCustomMetadataProviders": "Няма карыстацкіх пастаўшчыкоў метададзеных",
|
||||||
"LabelNotFinished": "Не скончана",
|
"LabelNotFinished": "Не скончана",
|
||||||
"LabelNotStarted": "Не пачата",
|
"LabelNotStarted": "Не пачата",
|
||||||
"LabelNotificationsMaxFailedAttemptsHelp": "Апавяшчэнні адключаюцца пасля таго, як не ўдаецца іх адправіць гэтулькі разоў",
|
"LabelNotificationsMaxFailedAttemptsHelp": "Апавяшчэнні адключаюцца пасля таго, як не ўдаецца іх адправіць гэтулькі разоў",
|
||||||
@@ -353,7 +440,7 @@
|
|||||||
"LabelPublishedDate": "Апублікавана {0}",
|
"LabelPublishedDate": "Апублікавана {0}",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Карыстальніцкая электронная пошта ўладальніка",
|
"LabelRSSFeedCustomOwnerEmail": "Карыстальніцкая электронная пошта ўладальніка",
|
||||||
"LabelRSSFeedCustomOwnerName": "Карыстальніцкае імя ўладальніка",
|
"LabelRSSFeedCustomOwnerName": "Карыстальніцкае імя ўладальніка",
|
||||||
"LabelRSSFeedOpen": "RSS-стужка адкрытая",
|
"LabelRSSFeedOpen": "RSS-стужка адкрыта",
|
||||||
"LabelRSSFeedPreventIndexing": "Прадухіліць індэксацыю",
|
"LabelRSSFeedPreventIndexing": "Прадухіліць індэксацыю",
|
||||||
"LabelRSSFeedSlug": "Ідэнтыфікатар RSS-стужкі",
|
"LabelRSSFeedSlug": "Ідэнтыфікатар RSS-стужкі",
|
||||||
"LabelRSSFeedURL": "URL RSS-стужкі",
|
"LabelRSSFeedURL": "URL RSS-стужкі",
|
||||||
@@ -392,6 +479,7 @@
|
|||||||
"LabelSettingsAudiobooksOnly": "Толькі аўдыякнігі",
|
"LabelSettingsAudiobooksOnly": "Толькі аўдыякнігі",
|
||||||
"LabelSettingsAudiobooksOnlyHelp": "Уключэнне гэтай налады будзе ігнараваць файлы электронных кніг, калі толькі яны не знаходзяцца ў тэчцы з аўдыякнігамі. У такім выпадку яны будуць пазначаны як дадатковыя электронныя кнігі.",
|
"LabelSettingsAudiobooksOnlyHelp": "Уключэнне гэтай налады будзе ігнараваць файлы электронных кніг, калі толькі яны не знаходзяцца ў тэчцы з аўдыякнігамі. У такім выпадку яны будуць пазначаны як дадатковыя электронныя кнігі.",
|
||||||
"LabelSettingsBookshelfViewHelp": "Рэалістычны дызайн з драўлянымі паліцамі",
|
"LabelSettingsBookshelfViewHelp": "Рэалістычны дызайн з драўлянымі паліцамі",
|
||||||
|
"LabelSettingsEnableWatcherForLibrary": "Аўтаматычна правяраць бібліятэку на змены",
|
||||||
"LabelSettingsEnableWatcherHelp": "Адключае аўтаматычнае дадаванне/абнаўленне элементаў пры выяўленні змен у файлах. *Патрабуецца перазапуск сервера",
|
"LabelSettingsEnableWatcherHelp": "Адключае аўтаматычнае дадаванне/абнаўленне элементаў пры выяўленні змен у файлах. *Патрабуецца перазапуск сервера",
|
||||||
"LabelSettingsEpubsAllowScriptedContent": "Дазволіць скрыптавы кантэнт у EPUB",
|
"LabelSettingsEpubsAllowScriptedContent": "Дазволіць скрыптавы кантэнт у EPUB",
|
||||||
"LabelSettingsEpubsAllowScriptedContentHelp": "Дазволіць EPUB-файлам выконваць скрыпты. Рэкамендуецца пакінуць гэтую наладу выключанай, калі вы не давяраеце крыніцы EPUB-файлаў.",
|
"LabelSettingsEpubsAllowScriptedContentHelp": "Дазволіць EPUB-файлам выконваць скрыпты. Рэкамендуецца пакінуць гэтую наладу выключанай, калі вы не давяраеце крыніцы EPUB-файлаў.",
|
||||||
@@ -409,6 +497,11 @@
|
|||||||
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Палка \"Працягнуць серыю\" на галоўнай старонцы паказвае першую не пачатую кнігу ў серыях, дзе завершана хаця б адна кніга і няма кніг у працэсе чытання. Уключэнне гэтай налады дазволіць працягваць серыю з самай апошняй завершанай кнігі замест першай не пачатай.",
|
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Палка \"Працягнуць серыю\" на галоўнай старонцы паказвае першую не пачатую кнігу ў серыях, дзе завершана хаця б адна кніга і няма кніг у працэсе чытання. Уключэнне гэтай налады дазволіць працягваць серыю з самай апошняй завершанай кнігі замест першай не пачатай.",
|
||||||
"LabelSettingsParseSubtitles": "Разабраць падзагалоўкі",
|
"LabelSettingsParseSubtitles": "Разабраць падзагалоўкі",
|
||||||
"LabelSettingsParseSubtitlesHelp": "Выдзяляць падзагаловак з назваў тэчак аўдыякніг.<br>Падзагаловак павінен быць аддзелены знакам \" - \".<br>Напрыклад, \"Назва кнігі - Падзагаловак тут\" мае падзагаловак \"Падзагаловак тут\"",
|
"LabelSettingsParseSubtitlesHelp": "Выдзяляць падзагаловак з назваў тэчак аўдыякніг.<br>Падзагаловак павінен быць аддзелены знакам \" - \".<br>Напрыклад, \"Назва кнігі - Падзагаловак тут\" мае падзагаловак \"Падзагаловак тут\"",
|
||||||
|
"LabelSettingsPreferMatchedMetadata": "Аддаваць перавагу супадаючым метададзеным",
|
||||||
|
"LabelSettingsPreferMatchedMetadataHelp": "Супадаючыя дадзеныя будуць замяняць дэталі элемента пры выкарыстанні функцыі Хуткі пошук. Па змаўчанні Хуткі пошук запаўняе толькі адсутныя дэталі.",
|
||||||
|
"LabelSettingsStoreCoversWithItemHelp": "Па змаўчанні вокладкі захоўваюцца ў /metadata/items, уключэнне гэтай опцыі забяспечыць захоўванне вокладак у тэчцы элемента вашай бібліятэкі. Захоўвацца будзе толькі адзін файл з назвай \"cover\"",
|
||||||
|
"LabelSettingsStoreMetadataWithItem": "Захоўваць метададзеныя разам з элементам",
|
||||||
|
"LabelSettingsStoreMetadataWithItemHelp": "Па змаўчанні метададзеныя захоўваюцца ў /metadata/items. Уключэнне гэтай опцыі забяспечыць захоўванне файлаў метададзеных у тэчках элементаў вашай бібліятэкі",
|
||||||
"LabelSettingsTimeFormat": "Фармат часу",
|
"LabelSettingsTimeFormat": "Фармат часу",
|
||||||
"LabelShareDownloadableHelp": "Дазваляе карыстальнікам, якія маюць спасылку на доступ, спампаваць ZIP-файл элемента бібліятэкі.",
|
"LabelShareDownloadableHelp": "Дазваляе карыстальнікам, якія маюць спасылку на доступ, спампаваць ZIP-файл элемента бібліятэкі.",
|
||||||
"LabelShowAll": "Паказаць усё",
|
"LabelShowAll": "Паказаць усё",
|
||||||
@@ -438,7 +531,7 @@
|
|||||||
"LabelTags": "Меткі",
|
"LabelTags": "Меткі",
|
||||||
"LabelTagsAccessibleToUser": "Меткі, даступныя карыстальніку",
|
"LabelTagsAccessibleToUser": "Меткі, даступныя карыстальніку",
|
||||||
"LabelTagsNotAccessibleToUser": "Меткі, недаступныя карыстальніку",
|
"LabelTagsNotAccessibleToUser": "Меткі, недаступныя карыстальніку",
|
||||||
"LabelTasks": "Выконваюцца задачы",
|
"LabelTasks": "Запушчаныя задачы",
|
||||||
"LabelTextEditorBulletedList": "Маркіраваны спіс",
|
"LabelTextEditorBulletedList": "Маркіраваны спіс",
|
||||||
"LabelTextEditorLink": "Спасылка",
|
"LabelTextEditorLink": "Спасылка",
|
||||||
"LabelTextEditorNumberedList": "Нумараваны спіс",
|
"LabelTextEditorNumberedList": "Нумараваны спіс",
|
||||||
@@ -457,11 +550,14 @@
|
|||||||
"LabelTimeRemaining": "Засталося {0}",
|
"LabelTimeRemaining": "Засталося {0}",
|
||||||
"LabelTimeToShift": "Час зрушэння ў секундах",
|
"LabelTimeToShift": "Час зрушэння ў секундах",
|
||||||
"LabelTitle": "Назва",
|
"LabelTitle": "Назва",
|
||||||
"LabelToolsSplitM4bDescription": "Стварэнне MP3 з M4B, падзеленага па раздзелах, з убудаванымі метаданымі, вокладкай і раздзеламі.",
|
"LabelToolsEmbedMetadata": "Убудаваць метададзеныя",
|
||||||
|
"LabelToolsEmbedMetadataDescription": "Убудаваць метададзеныя ў аўдыёфайлы, уключаючы выяву вокладкі і раздзелы.",
|
||||||
|
"LabelToolsMakeM4bDescription": "Стварыць аўдыёкнігу ў фармаце .M4B з убудаванымі метададзенымі, выявай вокладкі і раздзеламі.",
|
||||||
|
"LabelToolsSplitM4bDescription": "Стварэнне MP3 з M4B, падзеленага па раздзелах, з убудаванымі метададзенымі, выявай вокладкі і раздзеламі.",
|
||||||
"LabelTotalDuration": "Агульная працягласць",
|
"LabelTotalDuration": "Агульная працягласць",
|
||||||
"LabelTotalTimeListened": "Агульны час праслухоўвання",
|
"LabelTotalTimeListened": "Агульны час праслухоўвання",
|
||||||
"LabelTrackFromFilename": "Дарожка з імя файла",
|
"LabelTrackFromFilename": "Дарожка з імя файла",
|
||||||
"LabelTrackFromMetadata": "Дарожка з метаданых",
|
"LabelTrackFromMetadata": "Дарожка з метададзеных",
|
||||||
"LabelTracks": "Дарожкі",
|
"LabelTracks": "Дарожкі",
|
||||||
"LabelTracksMultiTrack": "Шматдарожкавы",
|
"LabelTracksMultiTrack": "Шматдарожкавы",
|
||||||
"LabelTracksNone": "Няма дарожак",
|
"LabelTracksNone": "Няма дарожак",
|
||||||
@@ -510,19 +606,30 @@
|
|||||||
"MessageBackupsLocationPathEmpty": "Шлях да месцазнаходжання рэзервовых копій не можа быць пустым",
|
"MessageBackupsLocationPathEmpty": "Шлях да месцазнаходжання рэзервовых копій не можа быць пустым",
|
||||||
"MessageBatchEditPopulateMapDetailsAllHelp": "Запоўніце ўключаныя палі дадзенымі з усіх элементаў. Палі з некалькімі значэннямі будуць аб'яднаны",
|
"MessageBatchEditPopulateMapDetailsAllHelp": "Запоўніце ўключаныя палі дадзенымі з усіх элементаў. Палі з некалькімі значэннямі будуць аб'яднаны",
|
||||||
"MessageBatchEditPopulateMapDetailsItemHelp": "Запоўніце ўключаныя палі падрабязнасцей карты дадзенымі з гэтага элемента",
|
"MessageBatchEditPopulateMapDetailsItemHelp": "Запоўніце ўключаныя палі падрабязнасцей карты дадзенымі з гэтага элемента",
|
||||||
|
"MessageBatchQuickMatchDescription": "Хуткі пошук паспрабуе дадаць адсутныя вокладкі і метададзеныя для выбраных элементаў. Уключыце ніжэй выкладзеныя опцыі, каб дазволіць Хуткаму пошуку замяняць існуючыя вокладкі і/або метададзеныя.",
|
||||||
"MessageBookshelfNoRSSFeeds": "Няма адкрытых RSS-стужак",
|
"MessageBookshelfNoRSSFeeds": "Няма адкрытых RSS-стужак",
|
||||||
"MessageChapterErrorStartGteDuration": "Няправільны час пачатку: ён павінен быць меншым за працягласць аўдыякнігі",
|
"MessageChapterErrorStartGteDuration": "Няправільны час пачатку: ён павінен быць меншым за працягласць аўдыякнігі",
|
||||||
"MessageChapterErrorStartLtPrev": "Няправільны час пачатку: ён павінен быць большым або роўным часу пачатку папярэдняга раздзела",
|
"MessageChapterErrorStartLtPrev": "Няправільны час пачатку: ён павінен быць большым або роўным часу пачатку папярэдняга раздзела",
|
||||||
"MessageConfirmCloseFeed": "Вы ўпэўнены, што жадаеце закрыць гэтую стужку?",
|
"MessageConfirmCloseFeed": "Вы ўпэўнены, што жадаеце закрыць гэтую стужку?",
|
||||||
|
"MessageConfirmDeleteMetadataProvider": "Ці ўпэўненыя вы, што жадаеце выдаліць карыстацкага пастаўшчыка метададзеных \"{0}\"?",
|
||||||
|
"MessageConfirmEmbedMetadataInAudioFiles": "Ці ўпэўненыя вы, што жадаеце ўбудаваць метададзеныя ў {0} аўдыёфайлаў?",
|
||||||
|
"MessageConfirmPurgeCache": "Ачышчэнне кэша выдаліць увесь каталог па адрасе <code>/metadata/cache</code>. <br /><br /> Ці сапраўды вы жадаеце выдаліць каталог кэша?",
|
||||||
|
"MessageConfirmPurgeItemsCache": "Ачышчэнне кэша элементаў выдаліць увесь каталог па адрасе <code>/metadata/cache/items</code>. <br /> Вы ўпэўнены?",
|
||||||
|
"MessageConfirmQuickMatchEpisodes": "Хуткае супадзенне эпізодаў перазапіша дэталі, калі супадзенне будзе знойдзена. Будуць абноўлены толькі эпізоды, якія не супадаюць. Вы ўпэўнены?",
|
||||||
"MessageConfirmRemoveListeningSessions": "Вы ўпэўнены, што жадаеце выдаліць {0} сеансаў праслухоўвання?",
|
"MessageConfirmRemoveListeningSessions": "Вы ўпэўнены, што жадаеце выдаліць {0} сеансаў праслухоўвання?",
|
||||||
|
"MessageConfirmRemoveMetadataFiles": "Ці ўпэўненыя вы, што жадаеце выдаліць усе файлы метададзеных{0} у тэчках элементаў вашай бібліятэкі?",
|
||||||
"MessageConfirmRemovePlaylist": "Вы ўпэўненыя, што жадаеце выдаліць свой спіс прайгравання \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Вы ўпэўненыя, што жадаеце выдаліць свой спіс прайгравання \"{0}\"?",
|
||||||
"MessageConfirmSendEbookToDevice": "Вы ўпэўнены, што хочаце адправіць {0} электронную кнігу \"{1}\" на прыладу \"{2}\"?",
|
"MessageConfirmSendEbookToDevice": "Вы ўпэўнены, што хочаце адправіць {0} электронную кнігу \"{1}\" на прыладу \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "Спампоўка эпізоду",
|
"MessageDownloadingEpisode": "Спампоўка эпізоду",
|
||||||
|
"MessageEmbedQueue": "У чарзе на ўбудаванне метададзеных (у чарзе {0})",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} эпізод(аў) у чарзе для спампоўкі",
|
"MessageEpisodesQueuedForDownload": "{0} эпізод(аў) у чарзе для спампоўкі",
|
||||||
"MessageEreaderDevices": "Каб забяспечыць дастаўку электронных кніг, вам можа спатрэбіцца дадаць вышэйзгаданы адрас электроннай пошты як дазволенага адпраўніка для кожнай прылады, пералічанай ніжэй.",
|
"MessageEreaderDevices": "Каб забяспечыць дастаўку электронных кніг, вам можа спатрэбіцца дадаць вышэйзгаданы адрас электроннай пошты як дазволенага адпраўніка для кожнай прылады, пералічанай ніжэй.",
|
||||||
"MessageFeedURLWillBe": "URL стужкі будзе {0}",
|
"MessageFeedURLWillBe": "URL стужкі будзе {0}",
|
||||||
"MessageFetching": "Атрыманне...",
|
"MessageFetching": "Атрыманне...",
|
||||||
|
"MessageInvalidAsin": "Няправільны ASIN",
|
||||||
|
"MessageItemsUpdated": "{0} элементаў абноўлена",
|
||||||
"MessageLoading": "Загрузка...",
|
"MessageLoading": "Загрузка...",
|
||||||
|
"MessageLogsDescription": "Журналы захоўваюцца ў каталогу <code>/metadata/logs</code> у фармаце JSON. Журналы памылак захоўваюцца ў файле <code>/metadata/logs/crashlogs.txt</code>.",
|
||||||
"MessageMapChapterTitles": "Супаставіць назвы раздзелаў з вашымі існуючымі раздзеламі аўдыякнігі без змянення часовых метак",
|
"MessageMapChapterTitles": "Супаставіць назвы раздзелаў з вашымі існуючымі раздзеламі аўдыякнігі без змянення часовых метак",
|
||||||
"MessageMarkAsFinished": "Пазначыць як скончана",
|
"MessageMarkAsFinished": "Пазначыць як скончана",
|
||||||
"MessageNoBookmarks": "Няма закладак",
|
"MessageNoBookmarks": "Няма закладак",
|
||||||
@@ -536,26 +643,54 @@
|
|||||||
"MessageNoMediaProgress": "Няма прагрэсу медыя",
|
"MessageNoMediaProgress": "Няма прагрэсу медыя",
|
||||||
"MessageNoPodcastFeed": "Няправільны падкаст: Няма стужкі",
|
"MessageNoPodcastFeed": "Няправільны падкаст: Няма стужкі",
|
||||||
"MessageNoPodcastsFound": "Падкасты не знойдзены",
|
"MessageNoPodcastsFound": "Падкасты не знойдзены",
|
||||||
|
"MessageNoTasksRunning": "Няма запушчаных задач",
|
||||||
"MessageNoUpdatesWereNecessary": "Абнаўленні не патрабаваліся",
|
"MessageNoUpdatesWereNecessary": "Абнаўленні не патрабаваліся",
|
||||||
"MessageNoUserPlaylists": "У вас няма спісаў прайгравання",
|
"MessageNoUserPlaylists": "У вас няма спісаў прайгравання",
|
||||||
"MessageNoUserPlaylistsHelp": "Спісы прайгравання прыватныя. Толькі карыстальнік, які іх стварыў, можа іх бачыць.",
|
"MessageNoUserPlaylistsHelp": "Спісы прайгравання прыватныя. Толькі карыстальнік, які іх стварыў, можа іх бачыць.",
|
||||||
"MessageOpmlPreviewNote": "Заўвага: гэта папярэдні прагляд разабранага OPML-файла. Фактычная назва падкаста будзе ўзятая з RSS-стужкі.",
|
"MessageOpmlPreviewNote": "Заўвага: гэта папярэдні прагляд разабранага файла OPML. Фактычная назва падкаста будзе ўзятая з RSS-стужкі.",
|
||||||
"MessagePlaylistCreateFromCollection": "Стварыць спіс прайгравання з калекцыі",
|
"MessagePlaylistCreateFromCollection": "Стварыць спіс прайгравання з калекцыі",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "У падкаста няма URL RSS-стужкі для супадзення",
|
"MessagePodcastHasNoRSSFeedForMatching": "У падкаста няма URL RSS-стужкі для супадзення",
|
||||||
"MessagePodcastSearchField": "Увядзіце пошукавы запыт або URL RSS-стужкі",
|
"MessagePodcastSearchField": "Увядзіце пошукавы запыт або URL RSS-стужкі",
|
||||||
|
"MessageQuickMatchDescription": "Запоўніць пустыя дэталі элемента і вокладку першым вынікам супадзення з '{0}'. Не замяняе дэталі, калі опцыя «Аддаваць перавагу супадаючым метададзеным» на серверы не ўключана.",
|
||||||
"MessageReportBugsAndContribute": "Паведамляйце пра памылкі, прапануйце новыя функцыі і ўдзельнічайце на",
|
"MessageReportBugsAndContribute": "Паведамляйце пра памылкі, прапануйце новыя функцыі і ўдзельнічайце на",
|
||||||
|
"MessageRestoreBackupWarning": "Аднаўленне рэзервовай копіі перазапіша ўсю базу даных, размешчаную ў /config, а таксама выявы вокладкі ў /metadata/items і /metadata/authors. <br /><br /> Рэзервовыя копіі не змяняюць файлы ў вашых тэчках бібліятэкі. Калі вы ўключылі наладкі сервера для захоўвання воклак і метададзеных у тэчках бібліятэкі, гэтыя файлы не будуць захаваныя ў рэзервовых копіях і не зменяцца. <br /><br /> Усе кліенты, якія карыстаюцца вашым серверам, будуць аўтаматычна абноўлены.",
|
||||||
"MessageScheduleRunEveryWeekdayAtTime": "Выконваць кожныя {0} у {1}",
|
"MessageScheduleRunEveryWeekdayAtTime": "Выконваць кожныя {0} у {1}",
|
||||||
"MessageStartPlaybackAtTime": "Пачаць прайграванне для \"{0}\" з {1}?",
|
"MessageStartPlaybackAtTime": "Пачаць прайграванне для \"{0}\" з {1}?",
|
||||||
|
"MessageTaskAudioFileNotWritable": "Аўдыёфайл \"{0}\" недаступны для запісу",
|
||||||
"MessageTaskCanceledByUser": "Задача скасавана карыстальнікам",
|
"MessageTaskCanceledByUser": "Задача скасавана карыстальнікам",
|
||||||
"MessageTaskDownloadingEpisodeDescription": "Спампоўка эпізоду \"{0}\"",
|
"MessageTaskDownloadingEpisodeDescription": "Спампоўка эпізоду \"{0}\"",
|
||||||
|
"MessageTaskEmbeddingMetadata": "Убудаванне метададзеных",
|
||||||
|
"MessageTaskEmbeddingMetadataDescription": "Убудаванне метададзеных у аўдыёкнігу \"{0}\"",
|
||||||
|
"MessageTaskEncodingM4b": "Кадаванне M4B",
|
||||||
|
"MessageTaskEncodingM4bDescription": "Кадаванне аўдыякнігі \"{0}\" у адзін файл m4b",
|
||||||
|
"MessageTaskFailed": "Не ўдалося",
|
||||||
|
"MessageTaskFailedToBackupAudioFile": "Не ўдалося зрабіць рэзервовую копію аўдыёфайла \"{0}\"",
|
||||||
|
"MessageTaskFailedToCreateCacheDirectory": "Не ўдалося стварыць каталог кэша",
|
||||||
|
"MessageTaskFailedToEmbedMetadataInFile": "Не ўдалося ўбудаваць метададзеныя ў файл \"{0}\"",
|
||||||
|
"MessageTaskFailedToMergeAudioFiles": "Не ўдалося аб’яднаць аўдыёфайлы",
|
||||||
|
"MessageTaskFailedToMoveM4bFile": "Не ўдалося перамясціць файл m4b",
|
||||||
|
"MessageTaskFailedToWriteMetadataFile": "Не ўдалося захаваць файл метададзеных",
|
||||||
|
"MessageTaskMatchingBooksInLibrary": "Пошук супадзенняў кніг у бібліятэцы \"{0}\"",
|
||||||
|
"MessageTaskNoFilesToScan": "Няма файлаў для сканавання",
|
||||||
|
"MessageTaskOpmlImport": "Імпарт OPML",
|
||||||
"MessageTaskOpmlImportDescription": "Стварэнне падкастаў з {0} RSS-стужак",
|
"MessageTaskOpmlImportDescription": "Стварэнне падкастаў з {0} RSS-стужак",
|
||||||
"MessageTaskOpmlImportFeed": "Імпарт стужкі з OPML",
|
"MessageTaskOpmlImportFeed": "Імпарт стужкі OPML",
|
||||||
"MessageTaskOpmlImportFeedDescription": "Імпартаванне RSS-стужкі \"{0}\"",
|
"MessageTaskOpmlImportFeedDescription": "Імпартаванне RSS-стужкі \"{0}\"",
|
||||||
"MessageTaskOpmlImportFeedFailed": "Не ўдалося атрымаць стужку падкаста",
|
"MessageTaskOpmlImportFeedFailed": "Не ўдалося атрымаць стужку падкаста",
|
||||||
"MessageTaskOpmlImportFeedPodcastDescription": "Стварэнне падкаста \"{0}\"",
|
"MessageTaskOpmlImportFeedPodcastDescription": "Стварэнне падкаста \"{0}\"",
|
||||||
"MessageTaskOpmlImportFeedPodcastExists": "Падкаст ужо існуе па гэтым шляху",
|
"MessageTaskOpmlImportFeedPodcastExists": "Падкаст ужо існуе па гэтым шляху",
|
||||||
"MessageTaskOpmlImportFeedPodcastFailed": "Не ўдалося стварыць падкаст",
|
"MessageTaskOpmlImportFeedPodcastFailed": "Не ўдалося стварыць падкаст",
|
||||||
"MessageTaskOpmlParseNoneFound": "У OPML-файле не знойдзена стужак",
|
"MessageTaskOpmlImportFinished": "Дададзена {0} падкастаў",
|
||||||
|
"MessageTaskOpmlParseFailed": "Не ўдалося разабраць файл OPML",
|
||||||
|
"MessageTaskOpmlParseFastFail": "Неправільны файл OPML: тэг <opml> не знойдзены АБО тэг <outline> не знойдзены",
|
||||||
|
"MessageTaskOpmlParseNoneFound": "У файле OPML не знойдзена стужак",
|
||||||
|
"MessageTaskScanItemsAdded": "{0} дададзена",
|
||||||
|
"MessageTaskScanItemsMissing": "{0} адсутнічае",
|
||||||
|
"MessageTaskScanItemsUpdated": "{0} абноўлена",
|
||||||
|
"MessageTaskScanNoChangesNeeded": "Змены не патрабуюцца",
|
||||||
|
"MessageTaskScanningFileChanges": "Сканіраванне змяненняў у файле \"{0}\"",
|
||||||
|
"MessageTaskScanningLibrary": "Сканіраванне бібліятэкі \"{0}\"",
|
||||||
|
"MessageTaskTargetDirectoryNotWritable": "Мэтавы каталог недаступны для запісу",
|
||||||
"NoteChapterEditorTimes": "Заўвага: Час пачатку першага раздзела павінен заставацца 0:00, а час пачатку апошняга раздзела не можа перавышаць працягласць гэтай аўдыякнігі.",
|
"NoteChapterEditorTimes": "Заўвага: Час пачатку першага раздзела павінен заставацца 0:00, а час пачатку апошняга раздзела не можа перавышаць працягласць гэтай аўдыякнігі.",
|
||||||
"NoteRSSFeedPodcastAppsHttps": "Папярэджанне: большасць праграм для падкастаў патрабуюць, каб URL RSS-стужкі выкарыстоўваў HTTPS",
|
"NoteRSSFeedPodcastAppsHttps": "Папярэджанне: большасць праграм для падкастаў патрабуюць, каб URL RSS-стужкі выкарыстоўваў HTTPS",
|
||||||
"NoteRSSFeedPodcastAppsPubDate": "Папярэджанне: адзін ці больш вашых эпізодаў не маюць даты публікацыі. Некаторыя праграмы для падкастаў патрабуюць гэтага.",
|
"NoteRSSFeedPodcastAppsPubDate": "Папярэджанне: адзін ці больш вашых эпізодаў не маюць даты публікацыі. Некаторыя праграмы для падкастаў патрабуюць гэтага.",
|
||||||
@@ -567,6 +702,11 @@
|
|||||||
"StatsBooksListenedTo": "кнігі, якія былі праслуханы",
|
"StatsBooksListenedTo": "кнігі, якія былі праслуханы",
|
||||||
"StatsCollectionGrewTo": "Ваша калекцыя кніг павялічылася да…",
|
"StatsCollectionGrewTo": "Ваша калекцыя кніг павялічылася да…",
|
||||||
"ToastAccountUpdateSuccess": "Уліковы запіс абноўлены",
|
"ToastAccountUpdateSuccess": "Уліковы запіс абноўлены",
|
||||||
|
"ToastAuthorImageRemoveSuccess": "Выява аўтара выдалена",
|
||||||
|
"ToastAuthorUpdateSuccess": "Аўтар абноўлены",
|
||||||
|
"ToastAuthorUpdateSuccessNoImageFound": "Аўтар абноўлены (малюнак не знойдзены)",
|
||||||
|
"ToastBackupInvalidMaxKeep": "Няправільная колькасць рэзервовых копій для захоўвання",
|
||||||
|
"ToastBackupInvalidMaxSize": "Няправільны максімальны памер рэзервовай копіі",
|
||||||
"ToastBookmarkCreateFailed": "Не ўдалося стварыць закладку",
|
"ToastBookmarkCreateFailed": "Не ўдалося стварыць закладку",
|
||||||
"ToastDateTimeInvalidOrIncomplete": "Дата і час указаны некарэктна або не цалкам",
|
"ToastDateTimeInvalidOrIncomplete": "Дата і час указаны некарэктна або не цалкам",
|
||||||
"ToastDeviceTestEmailFailed": "Не ўдалося адправіць тэставае электроннае пісьмо",
|
"ToastDeviceTestEmailFailed": "Не ўдалося адправіць тэставае электроннае пісьмо",
|
||||||
@@ -574,6 +714,7 @@
|
|||||||
"ToastEncodeCancelSucces": "Кадаванне скасавана",
|
"ToastEncodeCancelSucces": "Кадаванне скасавана",
|
||||||
"ToastEpisodeDownloadQueueClearFailed": "Не ўдалося ачысціць чаргу",
|
"ToastEpisodeDownloadQueueClearFailed": "Не ўдалося ачысціць чаргу",
|
||||||
"ToastEpisodeDownloadQueueClearSuccess": "Чарга спампоўкі эпізодаў ачышчана",
|
"ToastEpisodeDownloadQueueClearSuccess": "Чарга спампоўкі эпізодаў ачышчана",
|
||||||
|
"ToastInvalidImageUrl": "Няправільны URL выявы",
|
||||||
"ToastInvalidMaxEpisodesToDownload": "Няправільная максімальная колькасць эпізодаў для спампоўкі",
|
"ToastInvalidMaxEpisodesToDownload": "Няправільная максімальная колькасць эпізодаў для спампоўкі",
|
||||||
"ToastItemMarkedAsFinishedFailed": "Не ўдалося пазначыць як Скончана",
|
"ToastItemMarkedAsFinishedFailed": "Не ўдалося пазначыць як Скончана",
|
||||||
"ToastItemMarkedAsFinishedSuccess": "Элемент пазначаны як Завершаны",
|
"ToastItemMarkedAsFinishedSuccess": "Элемент пазначаны як Завершаны",
|
||||||
@@ -602,6 +743,8 @@
|
|||||||
"ToastPlaylistCreateSuccess": "Спіс прайгравання створаны",
|
"ToastPlaylistCreateSuccess": "Спіс прайгравання створаны",
|
||||||
"ToastPlaylistRemoveSuccess": "Спіс прайгравання выдалены",
|
"ToastPlaylistRemoveSuccess": "Спіс прайгравання выдалены",
|
||||||
"ToastPlaylistUpdateSuccess": "Спіс прайгравання абноўлены",
|
"ToastPlaylistUpdateSuccess": "Спіс прайгравання абноўлены",
|
||||||
|
"ToastPodcastCreateFailed": "Не ўдалося стварыць падкаст",
|
||||||
|
"ToastPodcastCreateSuccess": "Падкаст паспяхова створаны",
|
||||||
"ToastPodcastGetFeedFailed": "Не ўдалося атрымаць стужку падкаста",
|
"ToastPodcastGetFeedFailed": "Не ўдалося атрымаць стужку падкаста",
|
||||||
"ToastPodcastNoEpisodesInFeed": "У RSS-стужцы не знойдзена эпізодаў",
|
"ToastPodcastNoEpisodesInFeed": "У RSS-стужцы не знойдзена эпізодаў",
|
||||||
"ToastPodcastNoRssFeed": "У падкаста няма RSS-стужкі",
|
"ToastPodcastNoRssFeed": "У падкаста няма RSS-стужкі",
|
||||||
@@ -610,6 +753,7 @@
|
|||||||
"ToastSendEbookToDeviceFailed": "Не ўдалося адправіць электронную кнігу на прыладу",
|
"ToastSendEbookToDeviceFailed": "Не ўдалося адправіць электронную кнігу на прыладу",
|
||||||
"ToastSendEbookToDeviceSuccess": "Электронная кніга адпраўлена на прыладу \"{0}\"",
|
"ToastSendEbookToDeviceSuccess": "Электронная кніга адпраўлена на прыладу \"{0}\"",
|
||||||
"ToastSleepTimerDone": "Таймер сну скончыўся... Хр-р-р",
|
"ToastSleepTimerDone": "Таймер сну скончыўся... Хр-р-р",
|
||||||
|
"ToastUploaderItemExistsInSubdirectoryError": "Элемент \"{0}\" выкарыстоўвае падкаталог шляху загрузкі.",
|
||||||
"ToastUserPasswordMustChange": "Новы пароль не можа супадаць са старым",
|
"ToastUserPasswordMustChange": "Новы пароль не можа супадаць са старым",
|
||||||
"ToastUserRootRequireName": "Неабходна ўвесці імя карыстальніка адміністратара"
|
"ToastUserRootRequireName": "Неабходна ўвесці імя карыстальніка адміністратара"
|
||||||
}
|
}
|
||||||
|
|||||||
+181
-8
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"ButtonAdd": "Създай",
|
"ButtonAdd": "Създай",
|
||||||
|
"ButtonAddApiKey": "Добави API ключ",
|
||||||
"ButtonAddChapters": "Добави Глави",
|
"ButtonAddChapters": "Добави Глави",
|
||||||
"ButtonAddDevice": "Добави Устройство",
|
"ButtonAddDevice": "Добави Устройство",
|
||||||
"ButtonAddLibrary": "Добави Библиотека",
|
"ButtonAddLibrary": "Добави Библиотека",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"ButtonChooseAFolder": "Избери Папка",
|
"ButtonChooseAFolder": "Избери Папка",
|
||||||
"ButtonChooseFiles": "Избери Файлове",
|
"ButtonChooseFiles": "Избери Файлове",
|
||||||
"ButtonClearFilter": "Изчисти филтър",
|
"ButtonClearFilter": "Изчисти филтър",
|
||||||
|
"ButtonClose": "Затвори",
|
||||||
"ButtonCloseFeed": "Затвори стената",
|
"ButtonCloseFeed": "Затвори стената",
|
||||||
"ButtonCloseSession": "Затвори отворената сесия",
|
"ButtonCloseSession": "Затвори отворената сесия",
|
||||||
"ButtonCollections": "Колекции",
|
"ButtonCollections": "Колекции",
|
||||||
@@ -119,11 +121,13 @@
|
|||||||
"HeaderAccount": "Профил",
|
"HeaderAccount": "Профил",
|
||||||
"HeaderAddCustomMetadataProvider": "Добави персонализиран доставчик на метаданни",
|
"HeaderAddCustomMetadataProvider": "Добави персонализиран доставчик на метаданни",
|
||||||
"HeaderAdvanced": "Разширени настройки",
|
"HeaderAdvanced": "Разширени настройки",
|
||||||
|
"HeaderApiKeys": "API ключове",
|
||||||
"HeaderAppriseNotificationSettings": "Apprise Notification Опции",
|
"HeaderAppriseNotificationSettings": "Apprise Notification Опции",
|
||||||
"HeaderAudioTracks": "Песни",
|
"HeaderAudioTracks": "Песни",
|
||||||
"HeaderAudiobookTools": "Инструмент за Менижиране на Аудиокниги",
|
"HeaderAudiobookTools": "Инструмент за Менижиране на Аудиокниги",
|
||||||
"HeaderAuthentication": "Аутентикация",
|
"HeaderAuthentication": "Аутентикация",
|
||||||
"HeaderBackups": "Архив",
|
"HeaderBackups": "Архив",
|
||||||
|
"HeaderBulkChapterModal": "Добави няколко глави",
|
||||||
"HeaderChangePassword": "Промяна на Парола",
|
"HeaderChangePassword": "Промяна на Парола",
|
||||||
"HeaderChapters": "Глави",
|
"HeaderChapters": "Глави",
|
||||||
"HeaderChooseAFolder": "Избети Папка",
|
"HeaderChooseAFolder": "Избети Папка",
|
||||||
@@ -162,6 +166,7 @@
|
|||||||
"HeaderMetadataOrderOfPrecedence": "Предимство на Метаданни",
|
"HeaderMetadataOrderOfPrecedence": "Предимство на Метаданни",
|
||||||
"HeaderMetadataToEmbed": "Метаданни за Вграждане",
|
"HeaderMetadataToEmbed": "Метаданни за Вграждане",
|
||||||
"HeaderNewAccount": "Нов Профил",
|
"HeaderNewAccount": "Нов Профил",
|
||||||
|
"HeaderNewApiKey": "Нов API ключ",
|
||||||
"HeaderNewLibrary": "Нова Библиотека",
|
"HeaderNewLibrary": "Нова Библиотека",
|
||||||
"HeaderNotificationCreate": "Създай нотификация",
|
"HeaderNotificationCreate": "Създай нотификация",
|
||||||
"HeaderNotificationUpdate": "Обнови нотификация",
|
"HeaderNotificationUpdate": "Обнови нотификация",
|
||||||
@@ -177,6 +182,7 @@
|
|||||||
"HeaderPlaylist": "Плейлист",
|
"HeaderPlaylist": "Плейлист",
|
||||||
"HeaderPlaylistItems": "Елементи от плейлист",
|
"HeaderPlaylistItems": "Елементи от плейлист",
|
||||||
"HeaderPodcastsToAdd": "Подкасти за Добавяне",
|
"HeaderPodcastsToAdd": "Подкасти за Добавяне",
|
||||||
|
"HeaderPresets": "Настройки по подразбиране",
|
||||||
"HeaderPreviewCover": "Преглед на Корица",
|
"HeaderPreviewCover": "Преглед на Корица",
|
||||||
"HeaderRSSFeedGeneral": "RSS подробности",
|
"HeaderRSSFeedGeneral": "RSS подробности",
|
||||||
"HeaderRSSFeedIsOpen": "RSS емисията е отворена",
|
"HeaderRSSFeedIsOpen": "RSS емисията е отворена",
|
||||||
@@ -194,6 +200,7 @@
|
|||||||
"HeaderSettingsExperimental": "Експериментални Функции",
|
"HeaderSettingsExperimental": "Експериментални Функции",
|
||||||
"HeaderSettingsGeneral": "Общи",
|
"HeaderSettingsGeneral": "Общи",
|
||||||
"HeaderSettingsScanner": "Скенер",
|
"HeaderSettingsScanner": "Скенер",
|
||||||
|
"HeaderSettingsSecurity": "Сигурност",
|
||||||
"HeaderSettingsWebClient": "Уеб клиент",
|
"HeaderSettingsWebClient": "Уеб клиент",
|
||||||
"HeaderSleepTimer": "Таймер за заспиване",
|
"HeaderSleepTimer": "Таймер за заспиване",
|
||||||
"HeaderStatsLargestItems": "Най-Големите Елементи",
|
"HeaderStatsLargestItems": "Най-Големите Елементи",
|
||||||
@@ -205,6 +212,7 @@
|
|||||||
"HeaderTableOfContents": "Съдържание",
|
"HeaderTableOfContents": "Съдържание",
|
||||||
"HeaderTools": "Инструменти",
|
"HeaderTools": "Инструменти",
|
||||||
"HeaderUpdateAccount": "Обнови Профил",
|
"HeaderUpdateAccount": "Обнови Профил",
|
||||||
|
"HeaderUpdateApiKey": "Обнови API ключ",
|
||||||
"HeaderUpdateAuthor": "Обнови Автор",
|
"HeaderUpdateAuthor": "Обнови Автор",
|
||||||
"HeaderUpdateDetails": "Обнови Детайли",
|
"HeaderUpdateDetails": "Обнови Детайли",
|
||||||
"HeaderUpdateLibrary": "Обнови Библиотека",
|
"HeaderUpdateLibrary": "Обнови Библиотека",
|
||||||
@@ -219,6 +227,7 @@
|
|||||||
"LabelAccountTypeAdmin": "Администратор",
|
"LabelAccountTypeAdmin": "Администратор",
|
||||||
"LabelAccountTypeGuest": "Гост",
|
"LabelAccountTypeGuest": "Гост",
|
||||||
"LabelAccountTypeUser": "Потребител",
|
"LabelAccountTypeUser": "Потребител",
|
||||||
|
"LabelActivities": "Дейности",
|
||||||
"LabelActivity": "Дейност",
|
"LabelActivity": "Дейност",
|
||||||
"LabelAddToCollection": "Добави в Колекция",
|
"LabelAddToCollection": "Добави в Колекция",
|
||||||
"LabelAddToCollectionBatch": "Добави {0} Книги в Колекция",
|
"LabelAddToCollectionBatch": "Добави {0} Книги в Колекция",
|
||||||
@@ -228,10 +237,15 @@
|
|||||||
"LabelAddedDate": "Добавено",
|
"LabelAddedDate": "Добавено",
|
||||||
"LabelAdminUsersOnly": "Само за Администратори",
|
"LabelAdminUsersOnly": "Само за Администратори",
|
||||||
"LabelAll": "Всичко",
|
"LabelAll": "Всичко",
|
||||||
|
"LabelAllEpisodesDownloaded": "Всички епизоди са изтеглени",
|
||||||
"LabelAllUsers": "Всички Потребители",
|
"LabelAllUsers": "Всички Потребители",
|
||||||
"LabelAllUsersExcludingGuests": "Всички потребители без гости",
|
"LabelAllUsersExcludingGuests": "Всички потребители без гости",
|
||||||
"LabelAllUsersIncludingGuests": "Всички потребители включително гости",
|
"LabelAllUsersIncludingGuests": "Всички потребители включително гости",
|
||||||
"LabelAlreadyInYourLibrary": "Вече е в твоята библиотека",
|
"LabelAlreadyInYourLibrary": "Вече е в твоята библиотека",
|
||||||
|
"LabelApiKeyCreated": "API ключ \"{0}\" успешно създатен.",
|
||||||
|
"LabelApiKeyCreatedDescription": "Погрижете се да копирате API ключът сега, защото повече няма да можете да го виждате онново.",
|
||||||
|
"LabelApiKeyUser": "Действай от името на потребителя",
|
||||||
|
"LabelApiKeyUserDescription": "Този API ключ ще има същите права като на потребителя за чието име действа. В логовете ще изглежда все едно потребителя прави заявката.",
|
||||||
"LabelApiToken": "АПИ Токен",
|
"LabelApiToken": "АПИ Токен",
|
||||||
"LabelAppend": "Добави",
|
"LabelAppend": "Добави",
|
||||||
"LabelAudioBitrate": "Аудио битрейт (напр. 128k)",
|
"LabelAudioBitrate": "Аудио битрейт (напр. 128k)",
|
||||||
@@ -251,9 +265,9 @@
|
|||||||
"LabelBackToUser": "Обратно към Потребител",
|
"LabelBackToUser": "Обратно към Потребител",
|
||||||
"LabelBackupAudioFiles": "Създай резервно копие на аудио файлове",
|
"LabelBackupAudioFiles": "Създай резервно копие на аудио файлове",
|
||||||
"LabelBackupLocation": "Местоположение на Архив",
|
"LabelBackupLocation": "Местоположение на Архив",
|
||||||
"LabelBackupsEnableAutomaticBackups": "Включи автоматично архивиране",
|
"LabelBackupsEnableAutomaticBackups": "Автоматично архивиране",
|
||||||
"LabelBackupsEnableAutomaticBackupsHelp": "Архиви запазени в /metadata/backups",
|
"LabelBackupsEnableAutomaticBackupsHelp": "Архиви запазени в /metadata/backups",
|
||||||
"LabelBackupsMaxBackupSize": "Максимален размер на архива (в GB)",
|
"LabelBackupsMaxBackupSize": "Максимален размер на архива (в GB) (0 за неограничен)",
|
||||||
"LabelBackupsMaxBackupSizeHelp": "За защита срещу грешки в конфигурацията, архивите ще се провалят ако надхвърлят конфигурирания размер.",
|
"LabelBackupsMaxBackupSizeHelp": "За защита срещу грешки в конфигурацията, архивите ще се провалят ако надхвърлят конфигурирания размер.",
|
||||||
"LabelBackupsNumberToKeep": "Брой архиви за запазване",
|
"LabelBackupsNumberToKeep": "Брой архиви за запазване",
|
||||||
"LabelBackupsNumberToKeepHelp": "Само 1 архив ще бъде премахнат на веднъж, така че ако вече имате повече архиви от това трябва да ги премахнете ръчно.",
|
"LabelBackupsNumberToKeepHelp": "Само 1 архив ще бъде премахнат на веднъж, така че ако вече имате повече архиви от това трябва да ги премахнете ръчно.",
|
||||||
@@ -270,7 +284,7 @@
|
|||||||
"LabelChaptersFound": "намерени глави",
|
"LabelChaptersFound": "намерени глави",
|
||||||
"LabelClickForMoreInfo": "Кликни за повече информация",
|
"LabelClickForMoreInfo": "Кликни за повече информация",
|
||||||
"LabelClickToUseCurrentValue": "Натисни да ползваш сегашната стойност",
|
"LabelClickToUseCurrentValue": "Натисни да ползваш сегашната стойност",
|
||||||
"LabelClosePlayer": "Затвори",
|
"LabelClosePlayer": "Затвори плейъра",
|
||||||
"LabelCodec": "Кодек",
|
"LabelCodec": "Кодек",
|
||||||
"LabelCollapseSeries": "Скрий сериите",
|
"LabelCollapseSeries": "Скрий сериите",
|
||||||
"LabelCollapseSubSeries": "Свий подсерии",
|
"LabelCollapseSubSeries": "Свий подсерии",
|
||||||
@@ -281,8 +295,10 @@
|
|||||||
"LabelContinueListening": "Продължи слушане",
|
"LabelContinueListening": "Продължи слушане",
|
||||||
"LabelContinueReading": "Продължи четене",
|
"LabelContinueReading": "Продължи четене",
|
||||||
"LabelContinueSeries": "Продължи серии",
|
"LabelContinueSeries": "Продължи серии",
|
||||||
|
"LabelCorsAllowed": "Разрешени CORS Origins",
|
||||||
"LabelCover": "Корица",
|
"LabelCover": "Корица",
|
||||||
"LabelCoverImageURL": "URL на Корица",
|
"LabelCoverImageURL": "URL на Корица",
|
||||||
|
"LabelCoverProvider": "Източник за обложки",
|
||||||
"LabelCreatedAt": "Създадено на",
|
"LabelCreatedAt": "Създадено на",
|
||||||
"LabelCronExpression": "Cron израз",
|
"LabelCronExpression": "Cron израз",
|
||||||
"LabelCurrent": "Текущо",
|
"LabelCurrent": "Текущо",
|
||||||
@@ -293,6 +309,7 @@
|
|||||||
"LabelDeleteFromFileSystemCheckbox": "Изтрий от файловата система (отмени за да бъдат премахни само от базата данни)",
|
"LabelDeleteFromFileSystemCheckbox": "Изтрий от файловата система (отмени за да бъдат премахни само от базата данни)",
|
||||||
"LabelDescription": "Описание",
|
"LabelDescription": "Описание",
|
||||||
"LabelDeselectAll": "Премахни всички",
|
"LabelDeselectAll": "Премахни всички",
|
||||||
|
"LabelDetectedPattern": "Намерен образец:",
|
||||||
"LabelDevice": "Устройство",
|
"LabelDevice": "Устройство",
|
||||||
"LabelDeviceInfo": "Информация за Устройство",
|
"LabelDeviceInfo": "Информация за Устройство",
|
||||||
"LabelDeviceIsAvailableTo": "Устройството е достъпно за ...",
|
"LabelDeviceIsAvailableTo": "Устройството е достъпно за ...",
|
||||||
@@ -325,15 +342,28 @@
|
|||||||
"LabelEncodingClearItemCache": "Уверете се, че периодично изчиствате кеша на елементите.",
|
"LabelEncodingClearItemCache": "Уверете се, че периодично изчиствате кеша на елементите.",
|
||||||
"LabelEncodingFinishedM4B": "Завършеният M4B файл ще бъде поставен в папката на вашите аудиокниги на:",
|
"LabelEncodingFinishedM4B": "Завършеният M4B файл ще бъде поставен в папката на вашите аудиокниги на:",
|
||||||
"LabelEncodingInfoEmbedded": "Метаданните ще бъдат вградени в аудио траковете в папката на вашите аудиокниги.",
|
"LabelEncodingInfoEmbedded": "Метаданните ще бъдат вградени в аудио траковете в папката на вашите аудиокниги.",
|
||||||
|
"LabelEncodingStartedNavigation": "Когато задачата е стартирана, можете да смените тази страница.",
|
||||||
|
"LabelEncodingTimeWarning": "Кодирането може да отнеме до 30 минути.",
|
||||||
|
"LabelEncodingWarningAdvancedSettings": "Внимание: Не променяйте тези настройки, ако не сте запознати с ffmpeg настройките за кодиране.",
|
||||||
|
"LabelEncodingWatcherDisabled": "Ако сте изключили наблюдението на папки, ще е нужно да сканирате повторно аудио книгата.",
|
||||||
"LabelEnd": "Край",
|
"LabelEnd": "Край",
|
||||||
"LabelEndOfChapter": "Край на глава",
|
"LabelEndOfChapter": "Край на глава",
|
||||||
"LabelEpisode": "Епизод",
|
"LabelEpisode": "Епизод",
|
||||||
|
"LabelEpisodeNotLinkedToRssFeed": "Епизодът не е свързан с RSS канал",
|
||||||
|
"LabelEpisodeNumber": "Епизод #{0}",
|
||||||
"LabelEpisodeTitle": "Заглавие на Епизод",
|
"LabelEpisodeTitle": "Заглавие на Епизод",
|
||||||
"LabelEpisodeType": "Тип на Епизод",
|
"LabelEpisodeType": "Тип на Епизод",
|
||||||
|
"LabelEpisodeUrlFromRssFeed": "URL адрес на епизод от RSS канал",
|
||||||
|
"LabelEpisodes": "Епизоди",
|
||||||
|
"LabelEpisodic": "Епизодичен",
|
||||||
"LabelExample": "Пример",
|
"LabelExample": "Пример",
|
||||||
"LabelExpandSeries": "Покажи сериите",
|
"LabelExpandSeries": "Покажи сериите",
|
||||||
"LabelExpandSubSeries": "Покажи съб сериите",
|
"LabelExpandSubSeries": "Покажи съб сериите",
|
||||||
"LabelExplicit": "С нецензурно съдържание",
|
"LabelExpired": "Изтекъл",
|
||||||
|
"LabelExpiresAt": "Изтича на",
|
||||||
|
"LabelExpiresInSeconds": "Изтича след (секунди)",
|
||||||
|
"LabelExpiresNever": "Никога",
|
||||||
|
"LabelExplicit": "Експлицитно",
|
||||||
"LabelExplicitChecked": "С нецензурно съдържание (проверено)",
|
"LabelExplicitChecked": "С нецензурно съдържание (проверено)",
|
||||||
"LabelExplicitUnchecked": "Без нецензурно съдържание (непроверено)",
|
"LabelExplicitUnchecked": "Без нецензурно съдържание (непроверено)",
|
||||||
"LabelExportOPML": "Експортирай OPML",
|
"LabelExportOPML": "Експортирай OPML",
|
||||||
@@ -341,7 +371,9 @@
|
|||||||
"LabelFetchingMetadata": "Взимане на Метаданни",
|
"LabelFetchingMetadata": "Взимане на Метаданни",
|
||||||
"LabelFile": "Файл",
|
"LabelFile": "Файл",
|
||||||
"LabelFileBirthtime": "Дата на създаване на файла",
|
"LabelFileBirthtime": "Дата на създаване на файла",
|
||||||
|
"LabelFileBornDate": "Роден {0}",
|
||||||
"LabelFileModified": "Дата на модификация на файла",
|
"LabelFileModified": "Дата на модификация на файла",
|
||||||
|
"LabelFileModifiedDate": "Променен {0}",
|
||||||
"LabelFilename": "Име на файла",
|
"LabelFilename": "Име на файла",
|
||||||
"LabelFilterByUser": "Филтриране по Потребител",
|
"LabelFilterByUser": "Филтриране по Потребител",
|
||||||
"LabelFindEpisodes": "Намери Епизоди",
|
"LabelFindEpisodes": "Намери Епизоди",
|
||||||
@@ -355,14 +387,17 @@
|
|||||||
"LabelFontScale": "Мащаб на шрифта",
|
"LabelFontScale": "Мащаб на шрифта",
|
||||||
"LabelFontStrikethrough": "Зачертан",
|
"LabelFontStrikethrough": "Зачертан",
|
||||||
"LabelFormat": "Формат",
|
"LabelFormat": "Формат",
|
||||||
|
"LabelFull": "Пълен",
|
||||||
"LabelGenre": "Жанр",
|
"LabelGenre": "Жанр",
|
||||||
"LabelGenres": "Жанрове",
|
"LabelGenres": "Жанрове",
|
||||||
"LabelHardDeleteFile": "Пълно Изтриване на Файл",
|
"LabelHardDeleteFile": "Пълно Изтриване на Файл",
|
||||||
"LabelHasEbook": "Има е-книга",
|
"LabelHasEbook": "Има е-книга",
|
||||||
"LabelHasSupplementaryEbook": "Има допълнителна е-книга",
|
"LabelHasSupplementaryEbook": "Има допълнителна е-книга",
|
||||||
|
"LabelHideSubtitles": "Скрий субтитри",
|
||||||
"LabelHighestPriority": "Най-висок Приоритет",
|
"LabelHighestPriority": "Най-висок Приоритет",
|
||||||
"LabelHost": "Хост",
|
"LabelHost": "Хост",
|
||||||
"LabelHour": "Час",
|
"LabelHour": "Час",
|
||||||
|
"LabelHours": "Часа",
|
||||||
"LabelIcon": "Икона",
|
"LabelIcon": "Икона",
|
||||||
"LabelImageURLFromTheWeb": "URL на Изображение от Интернет",
|
"LabelImageURLFromTheWeb": "URL на Изображение от Интернет",
|
||||||
"LabelInProgress": "В процес на изпълнение",
|
"LabelInProgress": "В процес на изпълнение",
|
||||||
@@ -377,13 +412,17 @@
|
|||||||
"LabelIntervalEvery6Hours": "Всеки 6 часа",
|
"LabelIntervalEvery6Hours": "Всеки 6 часа",
|
||||||
"LabelIntervalEveryDay": "Всеки ден",
|
"LabelIntervalEveryDay": "Всеки ден",
|
||||||
"LabelIntervalEveryHour": "Всеки час",
|
"LabelIntervalEveryHour": "Всеки час",
|
||||||
|
"LabelIntervalEveryMinute": "Всяка минута",
|
||||||
"LabelInvert": "Обърни",
|
"LabelInvert": "Обърни",
|
||||||
"LabelItem": "Елемент",
|
"LabelItem": "Елемент",
|
||||||
|
"LabelJumpBackwardAmount": "Количество за прескачане назад",
|
||||||
|
"LabelJumpForwardAmount": "Количество за прескачане напред",
|
||||||
"LabelLanguage": "Език",
|
"LabelLanguage": "Език",
|
||||||
"LabelLanguageDefaultServer": "Език по подразбиране на сървъра",
|
"LabelLanguageDefaultServer": "Език по подразбиране на сървъра",
|
||||||
"LabelLanguages": "Езици",
|
"LabelLanguages": "Езици",
|
||||||
"LabelLastBookAdded": "Последно Добавена Книга",
|
"LabelLastBookAdded": "Последно Добавена Книга",
|
||||||
"LabelLastBookUpdated": "Последно Обновена Книга",
|
"LabelLastBookUpdated": "Последно Обновена Книга",
|
||||||
|
"LabelLastProgressDate": "Последен прогрес: {0}",
|
||||||
"LabelLastSeen": "Последно Видян",
|
"LabelLastSeen": "Последно Видян",
|
||||||
"LabelLastTime": "Последно Време",
|
"LabelLastTime": "Последно Време",
|
||||||
"LabelLastUpdate": "Последно Обновяване",
|
"LabelLastUpdate": "Последно Обновяване",
|
||||||
@@ -393,8 +432,10 @@
|
|||||||
"LabelLess": "По-малко",
|
"LabelLess": "По-малко",
|
||||||
"LabelLibrariesAccessibleToUser": "Библиотеки Достъпни за Потребителя",
|
"LabelLibrariesAccessibleToUser": "Библиотеки Достъпни за Потребителя",
|
||||||
"LabelLibrary": "Библиотека",
|
"LabelLibrary": "Библиотека",
|
||||||
|
"LabelLibraryFilterSublistEmpty": "Не {0}",
|
||||||
"LabelLibraryItem": "Елемент на Библиотека",
|
"LabelLibraryItem": "Елемент на Библиотека",
|
||||||
"LabelLibraryName": "Име на Библиотека",
|
"LabelLibraryName": "Име на Библиотека",
|
||||||
|
"LabelLibrarySortByProgress": "Прогресът е обновен",
|
||||||
"LabelLimit": "Лимит",
|
"LabelLimit": "Лимит",
|
||||||
"LabelLineSpacing": "Междуредие",
|
"LabelLineSpacing": "Междуредие",
|
||||||
"LabelListenAgain": "Слушай отново",
|
"LabelListenAgain": "Слушай отново",
|
||||||
@@ -403,8 +444,13 @@
|
|||||||
"LabelLogLevelWarn": "Предупреждение",
|
"LabelLogLevelWarn": "Предупреждение",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Търси нови епизоди след дата",
|
"LabelLookForNewEpisodesAfterDate": "Търси нови епизоди след дата",
|
||||||
"LabelLowestPriority": "Най-нисък Приоритет",
|
"LabelLowestPriority": "Най-нисък Приоритет",
|
||||||
|
"LabelMatchConfidence": "Увереност",
|
||||||
"LabelMatchExistingUsersBy": "Съпостави съществуващи потребители по",
|
"LabelMatchExistingUsersBy": "Съпостави съществуващи потребители по",
|
||||||
"LabelMatchExistingUsersByDescription": "Използва се за свързване на съществуващи потребители. След свързване потребителите ще бъдат съпоставени по уникален идентификатор от вашия доставчик на SSO",
|
"LabelMatchExistingUsersByDescription": "Използва се за свързване на съществуващи потребители. След свързване потребителите ще бъдат съпоставени по уникален идентификатор от вашия доставчик на SSO",
|
||||||
|
"LabelMaxEpisodesToDownload": "Максимален брой епизоди за сваляне. Използвай 0 за неограничен.",
|
||||||
|
"LabelMaxEpisodesToDownloadPerCheck": "Максимален брой нови епизоди за сваляне за проверка",
|
||||||
|
"LabelMaxEpisodesToKeep": "Максимален брой епизоди за запазване",
|
||||||
|
"LabelMaxEpisodesToKeepHelp": "Стойност 0 указва без максимален лимит. След като нов епизод е автоматично свален, най-старият епизод ще бъде изтрит, ако имате повече от X епизода. Само по един епизод ще бъде изтриван за всеки нов свален такъв.",
|
||||||
"LabelMediaPlayer": "Медия Плейър",
|
"LabelMediaPlayer": "Медия Плейър",
|
||||||
"LabelMediaType": "Тип медия",
|
"LabelMediaType": "Тип медия",
|
||||||
"LabelMetaTag": "Мета Таг",
|
"LabelMetaTag": "Мета Таг",
|
||||||
@@ -412,6 +458,7 @@
|
|||||||
"LabelMetadataOrderOfPrecedenceDescription": "По-високите източници на метаданни ще заменят по-ниските",
|
"LabelMetadataOrderOfPrecedenceDescription": "По-високите източници на метаданни ще заменят по-ниските",
|
||||||
"LabelMetadataProvider": "Доставчик на Метаданни",
|
"LabelMetadataProvider": "Доставчик на Метаданни",
|
||||||
"LabelMinute": "Минута",
|
"LabelMinute": "Минута",
|
||||||
|
"LabelMinutes": "Минути",
|
||||||
"LabelMissing": "Липсващо",
|
"LabelMissing": "Липсващо",
|
||||||
"LabelMissingEbook": "Няма електронна книга",
|
"LabelMissingEbook": "Няма електронна книга",
|
||||||
"LabelMissingSupplementaryEbook": "Няма допълнителна електронна книга",
|
"LabelMissingSupplementaryEbook": "Няма допълнителна електронна книга",
|
||||||
@@ -427,7 +474,9 @@
|
|||||||
"LabelNewestAuthors": "Най-новите автори",
|
"LabelNewestAuthors": "Най-новите автори",
|
||||||
"LabelNewestEpisodes": "Най-новите епизоди",
|
"LabelNewestEpisodes": "Най-новите епизоди",
|
||||||
"LabelNextBackupDate": "Следваща Дата на Архивиране",
|
"LabelNextBackupDate": "Следваща Дата на Архивиране",
|
||||||
|
"LabelNextChapters": "Следващите глави ще бъдат:",
|
||||||
"LabelNextScheduledRun": "Следващо Планирано Изпълнение",
|
"LabelNextScheduledRun": "Следващо Планирано Изпълнение",
|
||||||
|
"LabelNoApiKeys": "Няма API ключове",
|
||||||
"LabelNoCustomMetadataProviders": "Няма потребителски доставчици на метаданни",
|
"LabelNoCustomMetadataProviders": "Няма потребителски доставчици на метаданни",
|
||||||
"LabelNoEpisodesSelected": "Няма избрани епизоди",
|
"LabelNoEpisodesSelected": "Няма избрани епизоди",
|
||||||
"LabelNotFinished": "Не е приключено",
|
"LabelNotFinished": "Не е приключено",
|
||||||
@@ -443,17 +492,21 @@
|
|||||||
"LabelNotificationsMaxQueueSize": "Максимален размер на опашката за известия",
|
"LabelNotificationsMaxQueueSize": "Максимален размер на опашката за известия",
|
||||||
"LabelNotificationsMaxQueueSizeHelp": "Събитията са ограничени до изстрелване на 1 на секунда. Събитията ще бъдат игнорирани ако опашката е на максимален размер. Това предотвратява спамирането на известия.",
|
"LabelNotificationsMaxQueueSizeHelp": "Събитията са ограничени до изстрелване на 1 на секунда. Събитията ще бъдат игнорирани ако опашката е на максимален размер. Това предотвратява спамирането на известия.",
|
||||||
"LabelNumberOfBooks": "Брой на Книги",
|
"LabelNumberOfBooks": "Брой на Книги",
|
||||||
|
"LabelNumberOfChapters": "Брой глави:",
|
||||||
"LabelNumberOfEpisodes": "Брой епизоди",
|
"LabelNumberOfEpisodes": "Брой епизоди",
|
||||||
"LabelOpenIDAdvancedPermsClaimDescription": "Име на OpenID твърдението, което съдържа разширени права за достъп до потребителски действия в приложението, които ще се прилагат за роли, различни от администраторските (<b>ако е конфигурирано</b>). Ако твърдението липсва в отговора, достъпът до ABS ще бъде отказан. Ако липсва една опция, тя ще се третира като <code>false</code>. Уверете се, че твърдението на доставчика на идентичност съответства на очакваната структура:",
|
"LabelOpenIDAdvancedPermsClaimDescription": "Име на OpenID твърдението, което съдържа разширени права за достъп до потребителски действия в приложението, които ще се прилагат за роли, различни от администраторските (<b>ако е конфигурирано</b>). Ако твърдението липсва в отговора, достъпът до ABS ще бъде отказан. Ако липсва една опция, тя ще се третира като <code>false</code>. Уверете се, че твърдението на доставчика на идентичност съответства на очакваната структура:",
|
||||||
"LabelOpenIDClaims": "Оставете следните опции празни, за да деактивирате разширеното присвояване на групи, като автоматично ще бъде присвоена групата 'Потребител'.",
|
"LabelOpenIDClaims": "Оставете следните опции празни, за да деактивирате разширеното присвояване на групи, като автоматично ще бъде присвоена групата 'Потребител'.",
|
||||||
"LabelOpenIDGroupClaimDescription": "Име на OpenID твърдението, което съдържа списък с групите на потребителя. Обикновено се нарича <code>groups</code>. <b>Ако е конфигурирано</b>, приложението автоматично ще присвоява роли въз основа на членството на потребителя в групи, при условие че тези групи са наименувани без чувствителност към регистъра като 'admin', 'user' или 'guest' в твърдението. Твърдението трябва да съдържа списък и ако потребителят принадлежи към множество групи, приложението ще присвои ролята, съответстваща на най-високото ниво на достъп. Ако няма съвпадение с група, достъпът ще бъде отказан.",
|
"LabelOpenIDGroupClaimDescription": "Име на OpenID твърдението, което съдържа списък с групите на потребителя. Обикновено се нарича <code>groups</code>. <b>Ако е конфигурирано</b>, приложението автоматично ще присвоява роли въз основа на членството на потребителя в групи, при условие че тези групи са наименувани без чувствителност към регистъра като 'admin', 'user' или 'guest' в твърдението. Твърдението трябва да съдържа списък и ако потребителят принадлежи към множество групи, приложението ще присвои ролята, съответстваща на най-високото ниво на достъп. Ако няма съвпадение с група, достъпът ще бъде отказан.",
|
||||||
"LabelOpenRSSFeed": "Отвори RSS Feed",
|
"LabelOpenRSSFeed": "Отвори RSS Feed",
|
||||||
"LabelOverwrite": "Презапиши",
|
"LabelOverwrite": "Презапиши",
|
||||||
|
"LabelPaginationPageXOfY": "Страница {0} от {1}",
|
||||||
"LabelPassword": "Парола",
|
"LabelPassword": "Парола",
|
||||||
"LabelPath": "Път",
|
"LabelPath": "Път",
|
||||||
|
"LabelPermanent": "Постоянен",
|
||||||
"LabelPermissionsAccessAllLibraries": "Може да достъпи до всички библиотеки",
|
"LabelPermissionsAccessAllLibraries": "Може да достъпи до всички библиотеки",
|
||||||
"LabelPermissionsAccessAllTags": "Може да достъпи всички тагове",
|
"LabelPermissionsAccessAllTags": "Може да достъпи всички тагове",
|
||||||
"LabelPermissionsAccessExplicitContent": "Може да достъпи експлицитно съдържание",
|
"LabelPermissionsAccessExplicitContent": "Може да достъпи експлицитно съдържание",
|
||||||
|
"LabelPermissionsCreateEreader": "Може да създава електронен четец",
|
||||||
"LabelPermissionsDelete": "Може да трие",
|
"LabelPermissionsDelete": "Може да трие",
|
||||||
"LabelPermissionsDownload": "Може да сваля",
|
"LabelPermissionsDownload": "Може да сваля",
|
||||||
"LabelPermissionsUpdate": "Може да обновява",
|
"LabelPermissionsUpdate": "Може да обновява",
|
||||||
@@ -461,6 +514,8 @@
|
|||||||
"LabelPersonalYearReview": "Преглед на годината Ви ({0})",
|
"LabelPersonalYearReview": "Преглед на годината Ви ({0})",
|
||||||
"LabelPhotoPathURL": "Път/URL на Снимка",
|
"LabelPhotoPathURL": "Път/URL на Снимка",
|
||||||
"LabelPlayMethod": "Метод на Пускане",
|
"LabelPlayMethod": "Метод на Пускане",
|
||||||
|
"LabelPlaybackRateIncrementDecrement": "Размер на увеличаване/намаляне при скоростта на възпроизвеждане",
|
||||||
|
"LabelPlayerChapterNumberMarker": "{0} от {1}",
|
||||||
"LabelPlaylists": "Плейлисти",
|
"LabelPlaylists": "Плейлисти",
|
||||||
"LabelPodcast": "Подкаст",
|
"LabelPodcast": "Подкаст",
|
||||||
"LabelPodcastSearchRegion": "Регион за Търсене на Подкасти",
|
"LabelPodcastSearchRegion": "Регион за Търсене на Подкасти",
|
||||||
@@ -472,18 +527,22 @@
|
|||||||
"LabelPrimaryEbook": "Основна Електронна Книга",
|
"LabelPrimaryEbook": "Основна Електронна Книга",
|
||||||
"LabelProgress": "Прогрес",
|
"LabelProgress": "Прогрес",
|
||||||
"LabelProvider": "Доставчик",
|
"LabelProvider": "Доставчик",
|
||||||
|
"LabelProviderAuthorizationValue": "Стойност на Authorization Header",
|
||||||
"LabelPubDate": "Дата на публикуване",
|
"LabelPubDate": "Дата на публикуване",
|
||||||
"LabelPublishYear": "Година на публикуване",
|
"LabelPublishYear": "Година на публикуване",
|
||||||
"LabelPublishedDate": "Публикувани {0}",
|
"LabelPublishedDate": "Публикувани {0}",
|
||||||
|
"LabelPublishedDecade": "Десетилетие на публикуване",
|
||||||
|
"LabelPublishedDecades": "Десетилетия на публикуване",
|
||||||
"LabelPublisher": "Издател",
|
"LabelPublisher": "Издател",
|
||||||
"LabelPublishers": "Издателство",
|
"LabelPublishers": "Издателство",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Персонализиран имейл на собственика",
|
"LabelRSSFeedCustomOwnerEmail": "Персонализиран имейл на собственика",
|
||||||
"LabelRSSFeedCustomOwnerName": "Персонализирано име на собственика",
|
"LabelRSSFeedCustomOwnerName": "Персонализирано име на собственика",
|
||||||
"LabelRSSFeedOpen": "RSS Feed Оптворен",
|
"LabelRSSFeedOpen": "RSS Feed е отворен",
|
||||||
"LabelRSSFeedPreventIndexing": "Предотвратете индексиране",
|
"LabelRSSFeedPreventIndexing": "Предотвратете индексиране",
|
||||||
"LabelRSSFeedSlug": "идентификатор на RSS емисия",
|
"LabelRSSFeedSlug": "идентификатор на RSS емисия",
|
||||||
"LabelRSSFeedURL": "URL на RSS емисия",
|
"LabelRSSFeedURL": "URL на RSS емисия",
|
||||||
"LabelRandomly": "Случайно",
|
"LabelRandomly": "Случайно",
|
||||||
|
"LabelReAddSeriesToContinueListening": "Добави отново в \"Продължете да слушате\"",
|
||||||
"LabelRead": "Прочети",
|
"LabelRead": "Прочети",
|
||||||
"LabelReadAgain": "Прочети отново",
|
"LabelReadAgain": "Прочети отново",
|
||||||
"LabelReadEbookWithoutProgress": "Прочети електронната книга без записване прогрес",
|
"LabelReadEbookWithoutProgress": "Прочети електронната книга без записване прогрес",
|
||||||
@@ -493,29 +552,41 @@
|
|||||||
"LabelRedo": "Повтори",
|
"LabelRedo": "Повтори",
|
||||||
"LabelRegion": "Регион",
|
"LabelRegion": "Регион",
|
||||||
"LabelReleaseDate": "Дата на Издаване",
|
"LabelReleaseDate": "Дата на Издаване",
|
||||||
|
"LabelRemoveAllMetadataAbs": "Премахни всички metadata.abs файлове",
|
||||||
|
"LabelRemoveAllMetadataJson": "Премахни всички metadata.json файлове",
|
||||||
|
"LabelRemoveAudibleBranding": "Премахни въведението и заключението на Audible от главите",
|
||||||
"LabelRemoveCover": "Премахни Корица",
|
"LabelRemoveCover": "Премахни Корица",
|
||||||
|
"LabelRemoveMetadataFile": "Премахни файловете с метаданни от папката на библиотеката",
|
||||||
|
"LabelRemoveMetadataFileHelp": "Премахни всички metadata.json и metadata.abs файлове от вашата {0} папка.",
|
||||||
"LabelRowsPerPage": "Редове на Страница",
|
"LabelRowsPerPage": "Редове на Страница",
|
||||||
"LabelSearchTerm": "Търси Термин",
|
"LabelSearchTerm": "Търси Термин",
|
||||||
"LabelSearchTitle": "Търси Заглавие",
|
"LabelSearchTitle": "Търси Заглавие",
|
||||||
"LabelSearchTitleOrASIN": "Търси Заглавие или ASIN",
|
"LabelSearchTitleOrASIN": "Търси Заглавие или ASIN",
|
||||||
"LabelSeason": "Сезон",
|
"LabelSeason": "Сезон",
|
||||||
|
"LabelSeasonNumber": "Сезон #{0}",
|
||||||
"LabelSelectAll": "Избери всичко",
|
"LabelSelectAll": "Избери всичко",
|
||||||
"LabelSelectAllEpisodes": "Избери всички епизоди",
|
"LabelSelectAllEpisodes": "Избери всички епизоди",
|
||||||
"LabelSelectEpisodesShowing": "Избери {0} епизоди показани",
|
"LabelSelectEpisodesShowing": "Избери {0} епизоди показани",
|
||||||
|
"LabelSelectUser": "Избери потребител",
|
||||||
"LabelSelectUsers": "Избери Потребители",
|
"LabelSelectUsers": "Избери Потребители",
|
||||||
"LabelSendEbookToDevice": "Изпрати електронна книга до ...",
|
"LabelSendEbookToDevice": "Изпрати електронна книга до ...",
|
||||||
"LabelSequence": "Последователност",
|
"LabelSequence": "Последователност",
|
||||||
|
"LabelSerial": "Сериал",
|
||||||
"LabelSeries": "От сериите",
|
"LabelSeries": "От сериите",
|
||||||
"LabelSeriesName": "Име на Серия",
|
"LabelSeriesName": "Име на Серия",
|
||||||
"LabelSeriesProgress": "Прогрес на Серия",
|
"LabelSeriesProgress": "Прогрес на Серия",
|
||||||
|
"LabelServerLogLevel": "Ниво на сървърен журнал",
|
||||||
"LabelServerYearReview": "Преглед на годината на сървъра ({0})",
|
"LabelServerYearReview": "Преглед на годината на сървъра ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Направи главен",
|
"LabelSetEbookAsPrimary": "Направи главен",
|
||||||
"LabelSetEbookAsSupplementary": "Направи второстепенен",
|
"LabelSetEbookAsSupplementary": "Направи второстепенен",
|
||||||
|
"LabelSettingsAllowIframe": "Разреши вграждане в iframe",
|
||||||
"LabelSettingsAudiobooksOnly": "Само аудиокниги",
|
"LabelSettingsAudiobooksOnly": "Само аудиокниги",
|
||||||
"LabelSettingsAudiobooksOnlyHelp": "Активирането на тази настройка ще игнорира файловете на електронни книги, освен ако не са в папка с аудиокниги, в което случай ще бъдат зададени като допълнителни електронни книги",
|
"LabelSettingsAudiobooksOnlyHelp": "Активирането на тази настройка ще игнорира файловете на електронни книги, освен ако не са в папка с аудиокниги, в което случай ще бъдат зададени като допълнителни електронни книги",
|
||||||
"LabelSettingsBookshelfViewHelp": "Скеуморфен дизайн с дървени рафтове",
|
"LabelSettingsBookshelfViewHelp": "Скеуморфен дизайн с дървени рафтове",
|
||||||
"LabelSettingsChromecastSupport": "Chromecast поддръжка",
|
"LabelSettingsChromecastSupport": "Chromecast поддръжка",
|
||||||
"LabelSettingsDateFormat": "Формат на Дата",
|
"LabelSettingsDateFormat": "Формат на Дата",
|
||||||
|
"LabelSettingsEnableWatcher": "Автоматично сканиране на библиотеките за промени",
|
||||||
|
"LabelSettingsEnableWatcherForLibrary": "Автоматично сканиране на библиотеката за промени",
|
||||||
"LabelSettingsEnableWatcherHelp": "Включва автоматичното добавяне/обновяване на елементи, когато се открият промени във файловете. *Изисква рестарт на сървъра",
|
"LabelSettingsEnableWatcherHelp": "Включва автоматичното добавяне/обновяване на елементи, когато се открият промени във файловете. *Изисква рестарт на сървъра",
|
||||||
"LabelSettingsEpubsAllowScriptedContent": "Позволи скриптово съдържание в epub-и",
|
"LabelSettingsEpubsAllowScriptedContent": "Позволи скриптово съдържание в epub-и",
|
||||||
"LabelSettingsEpubsAllowScriptedContentHelp": "Позволи epub файловете да изпълняват скриптове. Препоръчително е да бъде изключено освен ако не се доверявате на източника на epub файловете.",
|
"LabelSettingsEpubsAllowScriptedContentHelp": "Позволи epub файловете да изпълняват скриптове. Препоръчително е да бъде изключено освен ако не се доверявате на източника на epub файловете.",
|
||||||
@@ -527,10 +598,13 @@
|
|||||||
"LabelSettingsHideSingleBookSeriesHelp": "Сериите с една книга ще бъдат скрити от страницата на серията и рафтовете на началната страница.",
|
"LabelSettingsHideSingleBookSeriesHelp": "Сериите с една книга ще бъдат скрити от страницата на серията и рафтовете на началната страница.",
|
||||||
"LabelSettingsHomePageBookshelfView": "Начална страница изглед на рафт",
|
"LabelSettingsHomePageBookshelfView": "Начална страница изглед на рафт",
|
||||||
"LabelSettingsLibraryBookshelfView": "Библиотека изглед на рафт",
|
"LabelSettingsLibraryBookshelfView": "Библиотека изглед на рафт",
|
||||||
|
"LabelSettingsLibraryMarkAsFinishedPercentComplete": "Процент завършеност е по-голям от",
|
||||||
|
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Оставащо време е по-малко от (секунди)",
|
||||||
|
"LabelSettingsLibraryMarkAsFinishedWhen": "Отбелязване на мултимедиен елемент като завършен когато",
|
||||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропусни предишни книги в Продължи Поредица",
|
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропусни предишни книги в Продължи Поредица",
|
||||||
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Рафтът на началната страница 'Продължи поредицата' показва първата книга, която не е започната в поредици, в които има поне една завършена книга и няма книги в процес на четене. Активирането на тази настройка ще продължи поредицата от най-далечната завършена книга вместо от първата незапочната книга.",
|
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Рафтът на началната страница 'Продължи поредицата' показва първата книга, която не е започната в поредици, в които има поне една завършена книга и няма книги в процес на четене. Активирането на тази настройка ще продължи поредицата от най-далечната завършена книга вместо от първата незапочната книга.",
|
||||||
"LabelSettingsParseSubtitles": "Извлечи подзаглавия",
|
"LabelSettingsParseSubtitles": "Извлечи подзаглавия",
|
||||||
"LabelSettingsParseSubtitlesHelp": "Извлича подзаглавия от имената на папките на аудиокнигите.<br>Подзаглавията трябва да бъдат разделени с \" - \"<br>например \"Заглавие на Книга - Тук е Подзаглавито\" има подзаглавие \"Тук е Подзаглавито\"",
|
"LabelSettingsParseSubtitlesHelp": "Извлича подзаглавия от имената на папките на аудио книгите.<br>Подзаглавията трябва да бъдат разделени с \" - \"<br>например \"Заглавие на Книга - Тук е подзаглавието\" има подзаглавие \"Тук е подзаглавието\"",
|
||||||
"LabelSettingsPreferMatchedMetadata": "Предпочети съвпадащи метаданни",
|
"LabelSettingsPreferMatchedMetadata": "Предпочети съвпадащи метаданни",
|
||||||
"LabelSettingsPreferMatchedMetadataHelp": "Съвпадащите данни ще заменят детайлите на елемента при използване на Бързо Съпоставяне. По подразбиране Бързото Съпоставяне ще попълни само липсващите детайли.",
|
"LabelSettingsPreferMatchedMetadataHelp": "Съвпадащите данни ще заменят детайлите на елемента при използване на Бързо Съпоставяне. По подразбиране Бързото Съпоставяне ще попълни само липсващите детайли.",
|
||||||
"LabelSettingsSkipMatchingBooksWithASIN": "Пропусни съвпадащи книги, които вече имат ASIN",
|
"LabelSettingsSkipMatchingBooksWithASIN": "Пропусни съвпадащи книги, които вече имат ASIN",
|
||||||
@@ -544,11 +618,19 @@
|
|||||||
"LabelSettingsStoreMetadataWithItem": "Запази метаданните с елемента",
|
"LabelSettingsStoreMetadataWithItem": "Запази метаданните с елемента",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "По подразбиране метаданните се съхраняват в /metadata/items, като активирате тази настройка метаданните ще се съхраняват в папката на елемента на вашата библиотека",
|
"LabelSettingsStoreMetadataWithItemHelp": "По подразбиране метаданните се съхраняват в /metadata/items, като активирате тази настройка метаданните ще се съхраняват в папката на елемента на вашата библиотека",
|
||||||
"LabelSettingsTimeFormat": "Формат на Време",
|
"LabelSettingsTimeFormat": "Формат на Време",
|
||||||
|
"LabelShare": "Сподели",
|
||||||
|
"LabelShareDownloadableHelp": "Разреши на потребителите през връзка за споделяне да свалят zip файл с мултимедийния елемент.",
|
||||||
|
"LabelShareOpen": "Общодостъпно",
|
||||||
|
"LabelShareURL": "URL за споделяне",
|
||||||
"LabelShowAll": "Покажи всички",
|
"LabelShowAll": "Покажи всички",
|
||||||
"LabelShowSeconds": "Покажи секунди",
|
"LabelShowSeconds": "Покажи секунди",
|
||||||
|
"LabelShowSubtitles": "Показвай подзаглавия",
|
||||||
"LabelSize": "Размер",
|
"LabelSize": "Размер",
|
||||||
"LabelSleepTimer": "Таймер за изключване",
|
"LabelSleepTimer": "Таймер за изключване",
|
||||||
"LabelSlug": "Слъг",
|
"LabelSlug": "Слъг",
|
||||||
|
"LabelSortAscending": "Възходящ",
|
||||||
|
"LabelSortDescending": "Низходящ",
|
||||||
|
"LabelSortPubDate": "Подреди по дата на публикуване",
|
||||||
"LabelStart": "Старт",
|
"LabelStart": "Старт",
|
||||||
"LabelStartTime": "Начално Време",
|
"LabelStartTime": "Начално Време",
|
||||||
"LabelStarted": "Стартирано",
|
"LabelStarted": "Стартирано",
|
||||||
@@ -582,7 +664,13 @@
|
|||||||
"LabelTheme": "Тема",
|
"LabelTheme": "Тема",
|
||||||
"LabelThemeDark": "Тъмна",
|
"LabelThemeDark": "Тъмна",
|
||||||
"LabelThemeLight": "Светла",
|
"LabelThemeLight": "Светла",
|
||||||
|
"LabelThemeSepia": "Сепия",
|
||||||
"LabelTimeBase": "Времева Основа",
|
"LabelTimeBase": "Времева Основа",
|
||||||
|
"LabelTimeDurationXHours": "{0} часа",
|
||||||
|
"LabelTimeDurationXMinutes": "{0} минути",
|
||||||
|
"LabelTimeDurationXSeconds": "{0} секунди",
|
||||||
|
"LabelTimeInMinutes": "Време в минути",
|
||||||
|
"LabelTimeLeft": "остава {0}",
|
||||||
"LabelTimeListened": "Време Слушано",
|
"LabelTimeListened": "Време Слушано",
|
||||||
"LabelTimeListenedToday": "Време Слушано Днес",
|
"LabelTimeListenedToday": "Време Слушано Днес",
|
||||||
"LabelTimeRemaining": "{0} оставащи",
|
"LabelTimeRemaining": "{0} оставащи",
|
||||||
@@ -590,6 +678,7 @@
|
|||||||
"LabelTitle": "Заглавие",
|
"LabelTitle": "Заглавие",
|
||||||
"LabelToolsEmbedMetadata": "Вграждане на Метаданни",
|
"LabelToolsEmbedMetadata": "Вграждане на Метаданни",
|
||||||
"LabelToolsEmbedMetadataDescription": "Вграждане на метаданни в аудио файлове, включително корица и глави.",
|
"LabelToolsEmbedMetadataDescription": "Вграждане на метаданни в аудио файлове, включително корица и глави.",
|
||||||
|
"LabelToolsM4bEncoder": "M4B кодировчик",
|
||||||
"LabelToolsMakeM4b": "Направи M4B Аудиокнига Файл",
|
"LabelToolsMakeM4b": "Направи M4B Аудиокнига Файл",
|
||||||
"LabelToolsMakeM4bDescription": "Генериране на .M4B аудиокнига файл с вградени метаданни, корица и глави.",
|
"LabelToolsMakeM4bDescription": "Генериране на .M4B аудиокнига файл с вградени метаданни, корица и глави.",
|
||||||
"LabelToolsSplitM4b": "Раздели M4B на MP3-ки",
|
"LabelToolsSplitM4b": "Раздели M4B на MP3-ки",
|
||||||
@@ -602,29 +691,39 @@
|
|||||||
"LabelTracksMultiTrack": "Многоканален",
|
"LabelTracksMultiTrack": "Многоканален",
|
||||||
"LabelTracksNone": "Няма канали",
|
"LabelTracksNone": "Няма канали",
|
||||||
"LabelTracksSingleTrack": "Единичен канал",
|
"LabelTracksSingleTrack": "Единичен канал",
|
||||||
|
"LabelTrailer": "Трейлър",
|
||||||
"LabelType": "Тип",
|
"LabelType": "Тип",
|
||||||
"LabelUnabridged": "Несъкратен",
|
"LabelUnabridged": "Несъкратен",
|
||||||
"LabelUndo": "Отмени",
|
"LabelUndo": "Отмени",
|
||||||
"LabelUnknown": "Неизвестен",
|
"LabelUnknown": "Неизвестен",
|
||||||
|
"LabelUnknownPublishDate": "Неизвестна дата на публикуване",
|
||||||
"LabelUpdateCover": "Обнови Корица",
|
"LabelUpdateCover": "Обнови Корица",
|
||||||
"LabelUpdateCoverHelp": "Позволи презаписване на съществуващите корици за избраните книги, когато се намери съвпадение",
|
"LabelUpdateCoverHelp": "Позволи презаписване на съществуващите корици за избраните книги, когато се намери съвпадение",
|
||||||
"LabelUpdateDetails": "Обнови Детайли",
|
"LabelUpdateDetails": "Обнови Детайли",
|
||||||
"LabelUpdateDetailsHelp": "Позволи презаписване на съществуващите детайли за избраните книги, когато се намери съвпадение",
|
"LabelUpdateDetailsHelp": "Позволи презаписване на съществуващите детайли за избраните книги, когато се намери съвпадение",
|
||||||
"LabelUpdatedAt": "Обновено на",
|
"LabelUpdatedAt": "Обновено на",
|
||||||
"LabelUploaderDragAndDrop": "Плъзни и Пусни Файлове или Папки",
|
"LabelUploaderDragAndDrop": "Плъзни и Пусни Файлове или Папки",
|
||||||
|
"LabelUploaderDragAndDropFilesOnly": "Извлачване на файлове",
|
||||||
"LabelUploaderDropFiles": "Пусни Файлове",
|
"LabelUploaderDropFiles": "Пусни Файлове",
|
||||||
"LabelUploaderItemFetchMetadataHelp": "Автоматично вземи заглавие, автор и серия",
|
"LabelUploaderItemFetchMetadataHelp": "Автоматично вземи заглавие, автор и серия",
|
||||||
|
"LabelUseAdvancedOptions": "Използвай разширени опции",
|
||||||
"LabelUseChapterTrack": "Използвай канал за глава",
|
"LabelUseChapterTrack": "Използвай канал за глава",
|
||||||
"LabelUseFullTrack": "Използвай пълен канал",
|
"LabelUseFullTrack": "Използвай пълен канал",
|
||||||
|
"LabelUseZeroForUnlimited": "Използвай 0 за неограничен",
|
||||||
"LabelUser": "Потребител",
|
"LabelUser": "Потребител",
|
||||||
"LabelUsername": "Потребителско име",
|
"LabelUsername": "Потребителско име",
|
||||||
"LabelValue": "Стойност",
|
"LabelValue": "Стойност",
|
||||||
"LabelVersion": "Версия",
|
"LabelVersion": "Версия",
|
||||||
"LabelViewBookmarks": "Виж Отметки",
|
"LabelViewBookmarks": "Виж Отметки",
|
||||||
"LabelViewChapters": "Виж Глави",
|
"LabelViewChapters": "Виж Глави",
|
||||||
|
"LabelViewPlayerSettings": "Виж настройки на плеъра",
|
||||||
"LabelViewQueue": "Виж Опашка",
|
"LabelViewQueue": "Виж Опашка",
|
||||||
"LabelVolume": "Сила на Звука",
|
"LabelVolume": "Сила на Звука",
|
||||||
|
"LabelWebRedirectURLsDescription": "Разрешете тези URL-и във вашият OAuth доставчик, за да позволите пренасочването обратно към уеб приложението след вход:",
|
||||||
|
"LabelWebRedirectURLsSubfolder": "Подпапка за URL адреси за пренасочване",
|
||||||
"LabelWeekdaysToRun": "Делници за изпълнение",
|
"LabelWeekdaysToRun": "Делници за изпълнение",
|
||||||
|
"LabelXBooks": "{0} книги",
|
||||||
|
"LabelXItems": "{0} елемента",
|
||||||
"LabelYearReviewHide": "Скрий ревю на годината ти",
|
"LabelYearReviewHide": "Скрий ревю на годината ти",
|
||||||
"LabelYearReviewShow": "Виж ревю на годината ти",
|
"LabelYearReviewShow": "Виж ревю на годината ти",
|
||||||
"LabelYourAudiobookDuration": "Продължителност на вашата аудиокнига",
|
"LabelYourAudiobookDuration": "Продължителност на вашата аудиокнига",
|
||||||
@@ -633,31 +732,51 @@
|
|||||||
"LabelYourProgress": "Твоят прогрес",
|
"LabelYourProgress": "Твоят прогрес",
|
||||||
"MessageAddToPlayerQueue": "Добави към опашката на плейъра",
|
"MessageAddToPlayerQueue": "Добави към опашката на плейъра",
|
||||||
"MessageAppriseDescription": "За да ползвате тази функция трябва да имате активна инстанция на <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> или на друго АПИ което да обработва тези заявки. <br />The Apprise API Url-а трябва дае пълния URL път за изпращане на известията, например, ако вашето АПИ ве подава от <code>http://192.168.1.1:8337</code> трябва да сложитев <code>http://192.168.1.1:8337/notify</code>.",
|
"MessageAppriseDescription": "За да ползвате тази функция трябва да имате активна инстанция на <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> или на друго АПИ което да обработва тези заявки. <br />The Apprise API Url-а трябва дае пълния URL път за изпращане на известията, например, ако вашето АПИ ве подава от <code>http://192.168.1.1:8337</code> трябва да сложитев <code>http://192.168.1.1:8337/notify</code>.",
|
||||||
|
"MessageAsinCheck": "Уверете се, че използвате ASIN от правилния Audible регион, а не от Amazon.",
|
||||||
|
"MessageAuthenticationLegacyTokenWarning": "Остарелите API токени ще бъдат премахнати в бъдеще. Вместо това използвайте <a href=\"/config/api-keys\">API ключове</a>.",
|
||||||
|
"MessageAuthenticationOIDCChangesRestart": "Рестартирайте сървърът след записването на настройките, за да активирате OIDC промените.",
|
||||||
|
"MessageAuthenticationSecurityMessage": "За осигуряването на по-добра сигурност, автентикацията беше подобрена. Всеки потребител ще трябва да се автентикира наново.",
|
||||||
"MessageBackupsDescription": "Резервните копия включват потребители, напредък на потребителите, подробности за елементите в библиотеката, настройки на сървъра и изображения, съхранени в <code>/metadata/items</code> и <code>/metadata/authors</code>. Резервните копия <strong>не</strong> включват никакви файлове, съхранени в папките на вашата библиотека.",
|
"MessageBackupsDescription": "Резервните копия включват потребители, напредък на потребителите, подробности за елементите в библиотеката, настройки на сървъра и изображения, съхранени в <code>/metadata/items</code> и <code>/metadata/authors</code>. Резервните копия <strong>не</strong> включват никакви файлове, съхранени в папките на вашата библиотека.",
|
||||||
|
"MessageBackupsLocationEditNote": "Забележка: Актуализирането на местоположението за архивиране няма да премести или промени съществуващите архиви",
|
||||||
|
"MessageBackupsLocationNoEditNote": "Забележка: Местоположението за архивиране се задава с помощта на променлива на средата и не може бъде променена от тук.",
|
||||||
|
"MessageBackupsLocationPathEmpty": "Пътят към местоположението за архивиране не може да бъде празен",
|
||||||
|
"MessageBatchEditPopulateMapDetailsAllHelp": "Популирайте активираните полета с данни от всички елементи. Полетата със няколко стоайности ще бъдат обединени",
|
||||||
|
"MessageBatchEditPopulateMapDetailsItemHelp": "Попълнете активираните полета с информация за картата с данни от този елемент",
|
||||||
"MessageBatchQuickMatchDescription": "Бързото Съпоставяне ще опита да добави липсващи корици и метаданни за избраните елементи. Активирайте опциите по-долу, за да позволите на Бързото съпоставяне да презапише съществуващите корици и/или метаданни.",
|
"MessageBatchQuickMatchDescription": "Бързото Съпоставяне ще опита да добави липсващи корици и метаданни за избраните елементи. Активирайте опциите по-долу, за да позволите на Бързото съпоставяне да презапише съществуващите корици и/или метаданни.",
|
||||||
"MessageBookshelfNoCollections": "Все още нямате създадени колекции",
|
"MessageBookshelfNoCollections": "Все още нямате създадени колекции",
|
||||||
|
"MessageBookshelfNoCollectionsHelp": "Колекциите са публични. Всички потребители с достъп до библиотеката ще могат да ги виждат.",
|
||||||
"MessageBookshelfNoRSSFeeds": "Няма отворени RSS feed-ове",
|
"MessageBookshelfNoRSSFeeds": "Няма отворени RSS feed-ове",
|
||||||
"MessageBookshelfNoResultsForFilter": "Няма резултат за филтер \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Няма резултат за филтер \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoResultsForQuery": "Няма резултати от заявката",
|
"MessageBookshelfNoResultsForQuery": "Няма резултати от заявката",
|
||||||
"MessageBookshelfNoSeries": "Нямаш сеЗЙ",
|
"MessageBookshelfNoSeries": "Нямаш сеЗЙ",
|
||||||
|
"MessageBulkChapterPattern": "Колко глави искате да добавите, използвайки тази схема за номериране?",
|
||||||
"MessageChapterEndIsAfter": "Краят на главата е след края на вашата аудиокнига",
|
"MessageChapterEndIsAfter": "Краят на главата е след края на вашата аудиокнига",
|
||||||
"MessageChapterErrorFirstNotZero": "Първата глава трябва да започва от 0",
|
"MessageChapterErrorFirstNotZero": "Първата глава трябва да започва от 0",
|
||||||
"MessageChapterErrorStartGteDuration": "Началото на главата трябва да бъде по-малко от продължителността на аудиокнигата",
|
"MessageChapterErrorStartGteDuration": "Началото на главата трябва да бъде по-малко от продължителността на аудиокнигата",
|
||||||
"MessageChapterErrorStartLtPrev": "Началото на главата трябва да бъде по-голямо или равно на края на предишната глава",
|
"MessageChapterErrorStartLtPrev": "Началото на главата трябва да бъде по-голямо или равно на края на предишната глава",
|
||||||
"MessageChapterStartIsAfter": "Началото на главата е след края на вашата аудиокнига",
|
"MessageChapterStartIsAfter": "Началото на главата е след края на вашата аудиокнига",
|
||||||
|
"MessageChaptersNotFound": "Главите не са намерени",
|
||||||
"MessageCheckingCron": "Проверяване на cron...",
|
"MessageCheckingCron": "Проверяване на cron...",
|
||||||
"MessageConfirmCloseFeed": "Сигурни ли сте, че искате да затворите този feed?",
|
"MessageConfirmCloseFeed": "Сигурни ли сте, че искате да затворите този feed?",
|
||||||
|
"MessageConfirmDeleteApiKey": "Сигурни ли сте, че искате да изтриете API ключ \"{0}\"?",
|
||||||
"MessageConfirmDeleteBackup": "Сигурни ли сте, че искате да изтриете този архив {0}?",
|
"MessageConfirmDeleteBackup": "Сигурни ли сте, че искате да изтриете този архив {0}?",
|
||||||
|
"MessageConfirmDeleteDevice": "Сигурни ли сте, че искате да изтриете е-четец \"{0}\"?",
|
||||||
"MessageConfirmDeleteFile": "Това ще изтрие файла от файловата Ви система. Сигурни ли сте?",
|
"MessageConfirmDeleteFile": "Това ще изтрие файла от файловата Ви система. Сигурни ли сте?",
|
||||||
"MessageConfirmDeleteLibrary": "Сигурни ли сте, че искате да изтриете за винаги библиотека \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Сигурни ли сте, че искате да изтриете за винаги библиотека \"{0}\"?",
|
||||||
"MessageConfirmDeleteLibraryItem": "Това ще изтрие елемента от базата данни и файловата Ви система. Сигурни ли сте?",
|
"MessageConfirmDeleteLibraryItem": "Това ще изтрие елемента от базата данни и файловата Ви система. Сигурни ли сте?",
|
||||||
"MessageConfirmDeleteLibraryItems": "Това ще изтрие {0} елемента от базата данни и файловата Ви система. Сигурни ли сте?",
|
"MessageConfirmDeleteLibraryItems": "Това ще изтрие {0} елемента от базата данни и файловата Ви система. Сигурни ли сте?",
|
||||||
|
"MessageConfirmDeleteMetadataProvider": "Сигурни ли сте, че искате да изтриете доставчика нa метаданни \"{0}\"?",
|
||||||
|
"MessageConfirmDeleteNotification": "Сигурни ли сте, че искате да изтриете това уведомление?",
|
||||||
"MessageConfirmDeleteSession": "Сигурни ли сте, че искате да изтриете тази сесия?",
|
"MessageConfirmDeleteSession": "Сигурни ли сте, че искате да изтриете тази сесия?",
|
||||||
|
"MessageConfirmEmbedMetadataInAudioFiles": "Сигурнли ли сте, че искате да вградите метаданните в {0} аудио файла?",
|
||||||
"MessageConfirmForceReScan": "Сигурни ли сте, че искате да принудите повторно сканиране?",
|
"MessageConfirmForceReScan": "Сигурни ли сте, че искате да принудите повторно сканиране?",
|
||||||
"MessageConfirmMarkAllEpisodesFinished": "Сигурни ли сте, че искате да маркирате всички епизоди като завършени?",
|
"MessageConfirmMarkAllEpisodesFinished": "Сигурни ли сте, че искате да маркирате всички епизоди като завършени?",
|
||||||
"MessageConfirmMarkAllEpisodesNotFinished": "Сигурни ли сте, че искате да маркирате всички епизоди като незавършени?",
|
"MessageConfirmMarkAllEpisodesNotFinished": "Сигурни ли сте, че искате да маркирате всички епизоди като незавършени?",
|
||||||
|
"MessageConfirmMarkItemFinished": "Сигурни ли сте, че искате да маркирате \"{0}\" като приключено?",
|
||||||
|
"MessageConfirmMarkItemNotFinished": "Сигурни ли сте, че искате да маркирате \"{0}\" като неприключено?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като завършени?",
|
"MessageConfirmMarkSeriesFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като завършени?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като незавършени?",
|
"MessageConfirmMarkSeriesNotFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като незавършени?",
|
||||||
|
"MessageConfirmNotificationTestTrigger": "Пуснете това уведомление с тестови данни?",
|
||||||
"MessageConfirmPurgeCache": "Изчистването на кеша ще изтрие цялата директория в <code>/metadata/cache</code>. <br /><br />Сигурни ли сте, че искате да премахнете директорията на кеша?",
|
"MessageConfirmPurgeCache": "Изчистването на кеша ще изтрие цялата директория в <code>/metadata/cache</code>. <br /><br />Сигурни ли сте, че искате да премахнете директорията на кеша?",
|
||||||
"MessageConfirmPurgeItemsCache": "Изчистването на кеша на елементите ще изтрие цялата директория в <code>/metadata/cache/items</code>. <br />Сигурни ли сте?",
|
"MessageConfirmPurgeItemsCache": "Изчистването на кеша на елементите ще изтрие цялата директория в <code>/metadata/cache/items</code>. <br />Сигурни ли сте?",
|
||||||
"MessageConfirmQuickEmbed": "Внимание! Бързото вграждане няма да архивира вашите аудио файлове. Уверете се, че имате резервно копие на вашите аудио файлове. <br><br>Искате ли да продължите?",
|
"MessageConfirmQuickEmbed": "Внимание! Бързото вграждане няма да архивира вашите аудио файлове. Уверете се, че имате резервно копие на вашите аудио файлове. <br><br>Искате ли да продължите?",
|
||||||
@@ -666,6 +785,7 @@
|
|||||||
"MessageConfirmRemoveAuthor": "Сигурни ли сте, че искате да премахнете автор \"{0}\"?",
|
"MessageConfirmRemoveAuthor": "Сигурни ли сте, че искате да премахнете автор \"{0}\"?",
|
||||||
"MessageConfirmRemoveCollection": "Сигурни ли сте, че искате да премахнете колекция \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Сигурни ли сте, че искате да премахнете колекция \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Сигурни ли сте, че искате да премахнете епизод \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Сигурни ли сте, че искате да премахнете епизод \"{0}\"?",
|
||||||
|
"MessageConfirmRemoveEpisodeNote": "Забележка: Това няма да доведе до изтриване на аудио файла, освен ако не активирате опцията \"Твърдо изтриване на файла\"",
|
||||||
"MessageConfirmRemoveEpisodes": "Сигурни ли сте, че искате да премахнете {0} епизода?",
|
"MessageConfirmRemoveEpisodes": "Сигурни ли сте, че искате да премахнете {0} епизода?",
|
||||||
"MessageConfirmRemoveListeningSessions": "Сигурни ли сте, че искате да премахнете {0} слушателски сесии?",
|
"MessageConfirmRemoveListeningSessions": "Сигурни ли сте, че искате да премахнете {0} слушателски сесии?",
|
||||||
"MessageConfirmRemoveNarrator": "Сигурни ли сте, че искате да премахнете разказвач \"{0}\"?",
|
"MessageConfirmRemoveNarrator": "Сигурни ли сте, че искате да премахнете разказвач \"{0}\"?",
|
||||||
@@ -676,19 +796,27 @@
|
|||||||
"MessageConfirmRenameTag": "Сигурни ли сте, че искате да преименувате таг \"{0}\" на \"{1}\" за всички елементи?",
|
"MessageConfirmRenameTag": "Сигурни ли сте, че искате да преименувате таг \"{0}\" на \"{1}\" за всички елементи?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Забележка: Този таг вече съществува и ще бъде слято.",
|
"MessageConfirmRenameTagMergeNote": "Забележка: Този таг вече съществува и ще бъде слято.",
|
||||||
"MessageConfirmRenameTagWarning": "Внимание! Вече съществува подобен таг с различно писане \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Внимание! Вече съществува подобен таг с различно писане \"{0}\".",
|
||||||
|
"MessageConfirmResetProgress": "Сигурни ли сте, че искате да нулирате прогреса си?",
|
||||||
"MessageConfirmSendEbookToDevice": "Сигурни ли сте, че искате да изпратите {0} електронна книга \"{1}\" до устройство \"{2}\"?",
|
"MessageConfirmSendEbookToDevice": "Сигурни ли сте, че искате да изпратите {0} електронна книга \"{1}\" до устройство \"{2}\"?",
|
||||||
|
"MessageConfirmUnlinkOpenId": "Сигурни ли сте, че искате да отвържете този потребител от OpenID?",
|
||||||
|
"MessageDaysListenedInTheLastYear": "{0} дни слушане през последната година",
|
||||||
"MessageDownloadingEpisode": "Сваля епизод",
|
"MessageDownloadingEpisode": "Сваля епизод",
|
||||||
"MessageDragFilesIntoTrackOrder": "Плъзнете файлове в правилния ред на каналите",
|
"MessageDragFilesIntoTrackOrder": "Плъзнете файлове в правилния ред на каналите",
|
||||||
|
"MessageEmbedFailed": "Вграждането беше неуспешно!",
|
||||||
"MessageEmbedFinished": "Вграждането завърши!",
|
"MessageEmbedFinished": "Вграждането завърши!",
|
||||||
|
"MessageEmbedQueue": "Поставено в опашката за вграждане на метаданни ({0} в опашката)",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} Епизод(и) са сложени за сваляне",
|
"MessageEpisodesQueuedForDownload": "{0} Епизод(и) са сложени за сваляне",
|
||||||
"MessageEreaderDevices": "За да осигурите доставката на е-книги, може да се наложи да добавите горепосочения имейл адрес като валиден подател за всяко устройство, изброено по-долу.",
|
"MessageEreaderDevices": "За да осигурите доставката на е-книги, може да се наложи да добавите горепосочения имейл адрес като валиден подател за всяко устройство, изброено по-долу.",
|
||||||
"MessageFeedURLWillBe": "Адресът на емисията ще бъде {0}",
|
"MessageFeedURLWillBe": "Адресът на емисията ще бъде {0}",
|
||||||
"MessageFetching": "Извличане...",
|
"MessageFetching": "Извличане...",
|
||||||
"MessageForceReScanDescription": "ще сканира всички файлове отново като прясно сканиране. Аудио файлове ID3 тагове, OPF файлове и текстови файлове ще бъдат сканирани като нови.",
|
"MessageForceReScanDescription": "ще сканира всички файлове отново като прясно сканиране. Аудио файлове ID3 тагове, OPF файлове и текстови файлове ще бъдат сканирани като нови.",
|
||||||
|
"MessageHeatmapListeningTimeTooltip": "<strong>{0} слушане</strong> на {1}",
|
||||||
|
"MessageHeatmapNoListeningSessions": "Няма сесии за слушане на {0}",
|
||||||
"MessageImportantNotice": "Важно Съобщение!",
|
"MessageImportantNotice": "Важно Съобщение!",
|
||||||
"MessageInsertChapterBelow": "Вмъкни глава под",
|
"MessageInsertChapterBelow": "Вмъкни глава под",
|
||||||
"MessageItemsSelected": "{0} избрани",
|
"MessageInvalidAsin": "Невалиден ASIN",
|
||||||
"MessageItemsUpdated": "{0} елемента обновени",
|
"MessageItemsSelected": "{0} избрани елемента",
|
||||||
|
"MessageItemsUpdated": "{0} обновени елемента",
|
||||||
"MessageJoinUsOn": "Присъединете се към нас",
|
"MessageJoinUsOn": "Присъединете се към нас",
|
||||||
"MessageLoading": "Зарежда...",
|
"MessageLoading": "Зарежда...",
|
||||||
"MessageLoadingFolders": "Зареждане на Папки...",
|
"MessageLoadingFolders": "Зареждане на Папки...",
|
||||||
@@ -709,6 +837,7 @@
|
|||||||
"MessageNoCollections": "Няма колекции",
|
"MessageNoCollections": "Няма колекции",
|
||||||
"MessageNoCoversFound": "Не са намерени корици",
|
"MessageNoCoversFound": "Не са намерени корици",
|
||||||
"MessageNoDescription": "Няма описание",
|
"MessageNoDescription": "Няма описание",
|
||||||
|
"MessageNoDevices": "Няма устройства",
|
||||||
"MessageNoDownloadsInProgress": "Няма изтегляния в прогрес",
|
"MessageNoDownloadsInProgress": "Няма изтегляния в прогрес",
|
||||||
"MessageNoDownloadsQueued": "Няма изтегляния в опашка",
|
"MessageNoDownloadsQueued": "Няма изтегляния в опашка",
|
||||||
"MessageNoEpisodeMatchesFound": "Няма намерени съвпадения за епизоди",
|
"MessageNoEpisodeMatchesFound": "Няма намерени съвпадения за епизоди",
|
||||||
@@ -722,6 +851,7 @@
|
|||||||
"MessageNoLogs": "Няма логове",
|
"MessageNoLogs": "Няма логове",
|
||||||
"MessageNoMediaProgress": "Няма прогрес на медията",
|
"MessageNoMediaProgress": "Няма прогрес на медията",
|
||||||
"MessageNoNotifications": "Няма известия",
|
"MessageNoNotifications": "Няма известия",
|
||||||
|
"MessageNoPodcastFeed": "Невалиден подкаст: Няма канал",
|
||||||
"MessageNoPodcastsFound": "Няма намерени подкасти",
|
"MessageNoPodcastsFound": "Няма намерени подкасти",
|
||||||
"MessageNoResults": "Няма резултати",
|
"MessageNoResults": "Няма резултати",
|
||||||
"MessageNoSearchResultsFor": "Няма резултати за \"{0}\"",
|
"MessageNoSearchResultsFor": "Няма резултати за \"{0}\"",
|
||||||
@@ -730,13 +860,17 @@
|
|||||||
"MessageNoTasksRunning": "Няма вършещи се задачи",
|
"MessageNoTasksRunning": "Няма вършещи се задачи",
|
||||||
"MessageNoUpdatesWereNecessary": "Няма нужда от обновяване",
|
"MessageNoUpdatesWereNecessary": "Няма нужда от обновяване",
|
||||||
"MessageNoUserPlaylists": "Нямате създадени плейлисти",
|
"MessageNoUserPlaylists": "Нямате създадени плейлисти",
|
||||||
|
"MessageNoUserPlaylistsHelp": "Плейлистите за частни. Само създалият ги потребител ще може да ги вижда.",
|
||||||
"MessageNotYetImplemented": "Още не е изпълнено",
|
"MessageNotYetImplemented": "Още не е изпълнено",
|
||||||
"MessageOr": "или",
|
"MessageOr": "или",
|
||||||
"MessagePauseChapter": "Пауза на глава",
|
"MessagePauseChapter": "Пауза на глава",
|
||||||
"MessagePlayChapter": "Пусни налчалото на глава",
|
"MessagePlayChapter": "Пусни налчалото на глава",
|
||||||
"MessagePlaylistCreateFromCollection": "Създай плейлист от колекция",
|
"MessagePlaylistCreateFromCollection": "Създай плейлист от колекция",
|
||||||
|
"MessagePleaseWait": "Моля изчакайте...",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Подкастът няма URL адрес на RSS feed за използване за съпоставяне",
|
"MessagePodcastHasNoRSSFeedForMatching": "Подкастът няма URL адрес на RSS feed за използване за съпоставяне",
|
||||||
"MessagePodcastSearchField": "Въведи какво да търся или RSS емисия адрес",
|
"MessagePodcastSearchField": "Въведи какво да търся или RSS емисия адрес",
|
||||||
|
"MessageQuickEmbedInProgress": "Бързото вграждане е в процес на изпълнение",
|
||||||
|
"MessageQuickEmbedQueue": "Поставено в опашката за бързо вграждане ({0} в опашката)",
|
||||||
"MessageQuickMatchDescription": "Попълни празните детайли и корици с първия резултат от '{0}'. Не презаписва детайлите, освен ако не е активирана настройката 'Предпочети съвпадащи метаданни' на сървъра.",
|
"MessageQuickMatchDescription": "Попълни празните детайли и корици с първия резултат от '{0}'. Не презаписва детайлите, освен ако не е активирана настройката 'Предпочети съвпадащи метаданни' на сървъра.",
|
||||||
"MessageRemoveChapter": "Премахни глава",
|
"MessageRemoveChapter": "Премахни глава",
|
||||||
"MessageRemoveEpisodes": "Премахни {0} епизод(и)",
|
"MessageRemoveEpisodes": "Премахни {0} епизод(и)",
|
||||||
@@ -746,11 +880,43 @@
|
|||||||
"MessageResetChaptersConfirm": "Сигурни ли сте, че искате да нулирате главите и да отмените промените, които сте направили?",
|
"MessageResetChaptersConfirm": "Сигурни ли сте, че искате да нулирате главите и да отмените промените, които сте направили?",
|
||||||
"MessageRestoreBackupConfirm": "Сигурни ли сте, че искате да възстановите архива създаден на",
|
"MessageRestoreBackupConfirm": "Сигурни ли сте, че искате да възстановите архива създаден на",
|
||||||
"MessageRestoreBackupWarning": "Възстановяването на архив ще презапише цялата база данни, намираща се в /config и кориците в /metadata/items & /metadata/authors.<br /><br />Архивите не променят файловете в папките на вашата библиотека. Ако сте активирали настройките на сървъра за съхранение на корици и метаданни в папките на вашата библиотека, те няма да бъдат архивирани или презаписани.<br /><br />Всички клиенти, използващи вашия сървър, ще бъдат автоматично обновени.",
|
"MessageRestoreBackupWarning": "Възстановяването на архив ще презапише цялата база данни, намираща се в /config и кориците в /metadata/items & /metadata/authors.<br /><br />Архивите не променят файловете в папките на вашата библиотека. Ако сте активирали настройките на сървъра за съхранение на корици и метаданни в папките на вашата библиотека, те няма да бъдат архивирани или презаписани.<br /><br />Всички клиенти, използващи вашия сървър, ще бъдат автоматично обновени.",
|
||||||
|
"MessageScheduleRunEveryWeekdayAtTime": "Изпълни всеки {0} в {1}",
|
||||||
"MessageSearchResultsFor": "Резултати от търсенето за",
|
"MessageSearchResultsFor": "Резултати от търсенето за",
|
||||||
"MessageSelected": "{0} избрани",
|
"MessageSelected": "{0} избрани",
|
||||||
"MessageServerCouldNotBeReached": "Сървърът не може да бъде достигнат",
|
"MessageServerCouldNotBeReached": "Сървърът не може да бъде достигнат",
|
||||||
"MessageSetChaptersFromTracksDescription": "Задайте глави, като използвате всеки аудио файл като глава и заглавие на главата като име на аудио файла",
|
"MessageSetChaptersFromTracksDescription": "Задайте глави, като използвате всеки аудио файл като глава и заглавие на главата като име на аудио файла",
|
||||||
|
"MessageShareExpiresIn": "Изтича след {0}",
|
||||||
"MessageStartPlaybackAtTime": "Започни възпроизвеждане на \"{0}\" в {1}?",
|
"MessageStartPlaybackAtTime": "Започни възпроизвеждане на \"{0}\" в {1}?",
|
||||||
|
"MessageTaskDownloadingEpisodeDescription": "Изтегляне на епизод \"{0}\"",
|
||||||
|
"MessageTaskEmbeddingMetadata": "Вграждане на метаданни",
|
||||||
|
"MessageTaskEmbeddingMetadataDescription": "Вграждане на метаданни в аудиокнига \"{0}\"",
|
||||||
|
"MessageTaskEncodingM4bDescription": "Кодиране на аудиокнига \"{0}\" в единичен m4b файл",
|
||||||
|
"MessageTaskFailed": "Неуспешно",
|
||||||
|
"MessageTaskFailedToBackupAudioFile": "Неуспешно създаване на разервно копие на аудио файл \"{0}\"",
|
||||||
|
"MessageTaskFailedToCreateCacheDirectory": "Неуспешно създаване на директория за кеширане",
|
||||||
|
"MessageTaskFailedToEmbedMetadataInFile": "Неуспешно вграждане на метаданни във файл \"{0}\"",
|
||||||
|
"MessageTaskFailedToMergeAudioFiles": "Неуспешно сливане на аудио файловете",
|
||||||
|
"MessageTaskFailedToMoveM4bFile": "Неуспешно преместване на m4b файл",
|
||||||
|
"MessageTaskFailedToWriteMetadataFile": "Неуспешно записване на файла за метаданни",
|
||||||
|
"MessageTaskMatchingBooksInLibrary": "Съответстващи книги в библиотека \"{0}\"",
|
||||||
|
"MessageTaskNoFilesToScan": "Няма файлове за сканиране",
|
||||||
|
"MessageTaskOpmlImport": "OPML импортиране",
|
||||||
|
"MessageTaskOpmlImportDescription": "Създаване на подкасти от {0} RSS хранилки",
|
||||||
|
"MessageTaskOpmlImportFeedDescription": "Импортиране на RSS хранилка \"{0}\"",
|
||||||
|
"MessageTaskOpmlImportFeedPodcastDescription": "Създаване на подкаст \"{0}\"",
|
||||||
|
"MessageTaskOpmlImportFeedPodcastExists": "На този път вече съществува подкаст",
|
||||||
|
"MessageTaskOpmlImportFeedPodcastFailed": "Неуспешно създаване на подкаст",
|
||||||
|
"MessageTaskOpmlImportFinished": "Добавени {0} подкаста",
|
||||||
|
"MessageTaskOpmlParseFailed": "Неуспешно анализиране на OPML файла",
|
||||||
|
"MessageTaskOpmlParseFastFail": "Невалиден OPML файл, не беше намерен нито <opml> таг нито <outline> таг",
|
||||||
|
"MessageTaskOpmlParseNoneFound": "Няма намерени канали във OPML файла",
|
||||||
|
"MessageTaskScanItemsAdded": "{0} добавени",
|
||||||
|
"MessageTaskScanItemsMissing": "{0} липсващи",
|
||||||
|
"MessageTaskScanItemsUpdated": "{0} обновени",
|
||||||
|
"MessageTaskScanNoChangesNeeded": "Не са нужни промени",
|
||||||
|
"MessageTaskScanningFileChanges": "Проверка за промени във файловете в \"{0}\"",
|
||||||
|
"MessageTaskScanningLibrary": "Сканиране на \"{0}\" библиотека",
|
||||||
|
"MessageTaskTargetDirectoryNotWritable": "Целевата директория не е достъпна за запис",
|
||||||
"MessageThinking": "Мисля...",
|
"MessageThinking": "Мисля...",
|
||||||
"MessageUploaderItemFailed": "Неуспешно качване",
|
"MessageUploaderItemFailed": "Неуспешно качване",
|
||||||
"MessageUploaderItemSuccess": "Успешно качване!",
|
"MessageUploaderItemSuccess": "Успешно качване!",
|
||||||
@@ -768,11 +934,18 @@
|
|||||||
"NoteUploaderFoldersWithMediaFiles": "Папките с медийни файлове ще бъдат обработени като отделни елементи на библиотеката.",
|
"NoteUploaderFoldersWithMediaFiles": "Папките с медийни файлове ще бъдат обработени като отделни елементи на библиотеката.",
|
||||||
"NoteUploaderOnlyAudioFiles": "Ако качвате само аудио файлове, то всеки аудио файл ще бъде обработен като отделна аудиокнига.",
|
"NoteUploaderOnlyAudioFiles": "Ако качвате само аудио файлове, то всеки аудио файл ще бъде обработен като отделна аудиокнига.",
|
||||||
"NoteUploaderUnsupportedFiles": "Неподдържаните файлове се игнорират. При избор или пускане на папка, други файлове, които не са в папка на елемент, се игнорират.",
|
"NoteUploaderUnsupportedFiles": "Неподдържаните файлове се игнорират. При избор или пускане на папка, други файлове, които не са в папка на елемент, се игнорират.",
|
||||||
|
"NotificationOnBackupCompletedDescription": "Изпълнява се при завършване на създаване на резервно копие",
|
||||||
|
"NotificationOnBackupFailedDescription": "Изпълнява се при неуспешено създаване на резервно копие",
|
||||||
|
"NotificationOnEpisodeDownloadedDescription": "Изпълнява се при автоматично изтегляне на подкаст епизод",
|
||||||
|
"NotificationOnRSSFeedDisabledDescription": "Изпълнява се, когато автоматичното изтегляне на епизодите е деактивирано, поради твърде много неуспешни опити",
|
||||||
"PlaceholderNewCollection": "Ново име на колекцията",
|
"PlaceholderNewCollection": "Ново име на колекцията",
|
||||||
"PlaceholderNewFolderPath": "Нов път на папката",
|
"PlaceholderNewFolderPath": "Нов път на папката",
|
||||||
"PlaceholderNewPlaylist": "Ново име на плейлиста",
|
"PlaceholderNewPlaylist": "Ново име на плейлиста",
|
||||||
"PlaceholderSearch": "Търсене...",
|
"PlaceholderSearch": "Търсене...",
|
||||||
"PlaceholderSearchEpisode": "Търсене на Епизоди...",
|
"PlaceholderSearchEpisode": "Търсене на Епизоди...",
|
||||||
|
"StatsAuthorsAdded": "добаврени автори",
|
||||||
|
"StatsBooksAdded": "добавени книги",
|
||||||
|
"StatsBooksFinished": "завършени книги",
|
||||||
"ToastAccountUpdateSuccess": "Успешно обновяване на акаунта",
|
"ToastAccountUpdateSuccess": "Успешно обновяване на акаунта",
|
||||||
"ToastAuthorImageRemoveSuccess": "Авторската снимка е премахната",
|
"ToastAuthorImageRemoveSuccess": "Авторската снимка е премахната",
|
||||||
"ToastAuthorUpdateMerged": "Обновяване на автора сливано",
|
"ToastAuthorUpdateMerged": "Обновяване на автора сливано",
|
||||||
|
|||||||
+177
-147
@@ -1,33 +1,35 @@
|
|||||||
{
|
{
|
||||||
"ButtonAdd": "Afegeix",
|
"ButtonAdd": "Afegeix",
|
||||||
"ButtonAddChapters": "Afegeix",
|
"ButtonAddChapters": "Afegeix capítols",
|
||||||
"ButtonAddDevice": "Afegeix Dispositiu",
|
"ButtonAddDevice": "Afegeix un aparell",
|
||||||
"ButtonAddLibrary": "Crea Biblioteca",
|
"ButtonAddLibrary": "Afegeix una biblioteca",
|
||||||
"ButtonAddPodcasts": "Afegeix pòdcasts",
|
"ButtonAddPodcasts": "Afegeix pòdcasts",
|
||||||
"ButtonAddUser": "Crea Usuari",
|
"ButtonAddUser": "Afegeix un usuari",
|
||||||
"ButtonAddYourFirstLibrary": "Crea la teva Primera Biblioteca",
|
"ButtonAddYourFirstLibrary": "Afegiu la vostra primera biblioteca",
|
||||||
"ButtonApply": "Aplica",
|
"ButtonApply": "Aplica",
|
||||||
"ButtonApplyChapters": "Aplica Capítols",
|
"ButtonApplyChapters": "Aplica capítols",
|
||||||
"ButtonAuthors": "Autors",
|
"ButtonAuthors": "Autors",
|
||||||
"ButtonBack": "Enrere",
|
"ButtonBack": "Enrere",
|
||||||
"ButtonBrowseForFolder": "Cerca Carpeta",
|
"ButtonBatchEditPopulateFromExisting": "Omplir des d'existent",
|
||||||
|
"ButtonBatchEditPopulateMapDetails": "Omple els detalls del mapa",
|
||||||
|
"ButtonBrowseForFolder": "Cerca una carpeta",
|
||||||
"ButtonCancel": "Cancel·la",
|
"ButtonCancel": "Cancel·la",
|
||||||
"ButtonCancelEncode": "Cancel·la Codificador",
|
"ButtonCancelEncode": "Cancel·la la codificació",
|
||||||
"ButtonChangeRootPassword": "Canvia Contrasenya Root",
|
"ButtonChangeRootPassword": "Canvia Contrasenya Root",
|
||||||
"ButtonCheckAndDownloadNewEpisodes": "Verifica i Descarrega Nous Episodis",
|
"ButtonCheckAndDownloadNewEpisodes": "Verifica i Descarrega Nous Episodis",
|
||||||
"ButtonChooseAFolder": "Tria una Carpeta",
|
"ButtonChooseAFolder": "Trieu una carpeta",
|
||||||
"ButtonChooseFiles": "Tria un Fitxer",
|
"ButtonChooseFiles": "Trieu fitxers",
|
||||||
"ButtonClearFilter": "Elimina Filtres",
|
"ButtonClearFilter": "Neteja el filtre",
|
||||||
"ButtonCloseFeed": "Tanca Font",
|
"ButtonCloseFeed": "Tanca el canal",
|
||||||
"ButtonCloseSession": "Tanca la sessió oberta",
|
"ButtonCloseSession": "Tanca la sessió oberta",
|
||||||
"ButtonCollections": "Col·leccions",
|
"ButtonCollections": "Col·leccions",
|
||||||
"ButtonConfigureScanner": "Configura Escàner",
|
"ButtonConfigureScanner": "Configura Escàner",
|
||||||
"ButtonCreate": "Crea",
|
"ButtonCreate": "Crea",
|
||||||
"ButtonCreateBackup": "Crea Còpia de Seguretat",
|
"ButtonCreateBackup": "Crea Còpia de Seguretat",
|
||||||
"ButtonDelete": "Elimina",
|
"ButtonDelete": "Suprimeix",
|
||||||
"ButtonDownloadQueue": "Cua",
|
"ButtonDownloadQueue": "Cua",
|
||||||
"ButtonEdit": "Edita",
|
"ButtonEdit": "Edita",
|
||||||
"ButtonEditChapters": "Edita Capítol",
|
"ButtonEditChapters": "Edita capítols",
|
||||||
"ButtonEditPodcast": "Edita el pòdcast",
|
"ButtonEditPodcast": "Edita el pòdcast",
|
||||||
"ButtonEnable": "Habilita",
|
"ButtonEnable": "Habilita",
|
||||||
"ButtonFireAndFail": "Executat i fallat",
|
"ButtonFireAndFail": "Executat i fallat",
|
||||||
@@ -175,6 +177,7 @@
|
|||||||
"HeaderPlaylist": "Llista de Reproducció",
|
"HeaderPlaylist": "Llista de Reproducció",
|
||||||
"HeaderPlaylistItems": "Elements de la Llista de Reproducció",
|
"HeaderPlaylistItems": "Elements de la Llista de Reproducció",
|
||||||
"HeaderPodcastsToAdd": "Pòdcasts a afegir",
|
"HeaderPodcastsToAdd": "Pòdcasts a afegir",
|
||||||
|
"HeaderPresets": "Valors predefinits",
|
||||||
"HeaderPreviewCover": "Previsualització de la Portada",
|
"HeaderPreviewCover": "Previsualització de la Portada",
|
||||||
"HeaderRSSFeedGeneral": "Detalls RSS",
|
"HeaderRSSFeedGeneral": "Detalls RSS",
|
||||||
"HeaderRSSFeedIsOpen": "La Font RSS està oberta",
|
"HeaderRSSFeedIsOpen": "La Font RSS està oberta",
|
||||||
@@ -190,7 +193,7 @@
|
|||||||
"HeaderSettings": "Paràmetres",
|
"HeaderSettings": "Paràmetres",
|
||||||
"HeaderSettingsDisplay": "Interfície",
|
"HeaderSettingsDisplay": "Interfície",
|
||||||
"HeaderSettingsExperimental": "Funcionalitats experimentals",
|
"HeaderSettingsExperimental": "Funcionalitats experimentals",
|
||||||
"HeaderSettingsGeneral": "General",
|
"HeaderSettingsGeneral": "Generals",
|
||||||
"HeaderSettingsScanner": "Escàner",
|
"HeaderSettingsScanner": "Escàner",
|
||||||
"HeaderSettingsWebClient": "Client web",
|
"HeaderSettingsWebClient": "Client web",
|
||||||
"HeaderSleepTimer": "Temporitzador de son",
|
"HeaderSleepTimer": "Temporitzador de son",
|
||||||
@@ -219,10 +222,10 @@
|
|||||||
"LabelAccountTypeUser": "Usuari",
|
"LabelAccountTypeUser": "Usuari",
|
||||||
"LabelActivities": "Activitats",
|
"LabelActivities": "Activitats",
|
||||||
"LabelActivity": "Activitat",
|
"LabelActivity": "Activitat",
|
||||||
"LabelAddToCollection": "Afegit a la Col·lecció",
|
"LabelAddToCollection": "Afegeix a la col·lecció",
|
||||||
"LabelAddToCollectionBatch": "S'han Afegit {0} Llibres a la Col·lecció",
|
"LabelAddToCollectionBatch": "Afegeix {0} llibres a la col·lecció",
|
||||||
"LabelAddToPlaylist": "Afegit a la llista de reproducció",
|
"LabelAddToPlaylist": "Afegeix a la llista de reproducció",
|
||||||
"LabelAddToPlaylistBatch": "S'han Afegit {0} Elements a la Llista de Reproducció",
|
"LabelAddToPlaylistBatch": "Afegeix {0} elements a la llista de reproducció",
|
||||||
"LabelAddedAt": "Afegit",
|
"LabelAddedAt": "Afegit",
|
||||||
"LabelAddedDate": "{0} Afegit",
|
"LabelAddedDate": "{0} Afegit",
|
||||||
"LabelAdminUsersOnly": "Només usuaris administradors",
|
"LabelAdminUsersOnly": "Només usuaris administradors",
|
||||||
@@ -231,7 +234,7 @@
|
|||||||
"LabelAllUsers": "Tots els usuaris",
|
"LabelAllUsers": "Tots els usuaris",
|
||||||
"LabelAllUsersExcludingGuests": "Tots els usuaris excepte convidats",
|
"LabelAllUsersExcludingGuests": "Tots els usuaris excepte convidats",
|
||||||
"LabelAllUsersIncludingGuests": "Tots els usuaris i convidats",
|
"LabelAllUsersIncludingGuests": "Tots els usuaris i convidats",
|
||||||
"LabelAlreadyInYourLibrary": "Ja existeix a la Biblioteca",
|
"LabelAlreadyInYourLibrary": "Ja existeix a la biblioteca",
|
||||||
"LabelApiToken": "Testimoni de l'API",
|
"LabelApiToken": "Testimoni de l'API",
|
||||||
"LabelAppend": "Adjuntar",
|
"LabelAppend": "Adjuntar",
|
||||||
"LabelAudioBitrate": "Taxa de bits d'àudio (per exemple, 128k)",
|
"LabelAudioBitrate": "Taxa de bits d'àudio (per exemple, 128k)",
|
||||||
@@ -288,14 +291,14 @@
|
|||||||
"LabelCronExpression": "Expressió de Cron",
|
"LabelCronExpression": "Expressió de Cron",
|
||||||
"LabelCurrent": "Actual",
|
"LabelCurrent": "Actual",
|
||||||
"LabelCurrently": "En aquest moment:",
|
"LabelCurrently": "En aquest moment:",
|
||||||
"LabelCustomCronExpression": "Expressió de Cron Personalitzada:",
|
"LabelCustomCronExpression": "Expressió del Cron personalitzada:",
|
||||||
"LabelDatetime": "Hora i Data",
|
"LabelDatetime": "Data i hora",
|
||||||
"LabelDays": "Dies",
|
"LabelDays": "Dies",
|
||||||
"LabelDeleteFromFileSystemCheckbox": "Suprimeix del sistema de fitxers (desmarqueu per a eliminar de la base de dades només)",
|
"LabelDeleteFromFileSystemCheckbox": "Suprimeix del sistema de fitxers (desmarqueu per a eliminar de la base de dades només)",
|
||||||
"LabelDescription": "Descripció",
|
"LabelDescription": "Descripció",
|
||||||
"LabelDeselectAll": "Desseleccionar Tots",
|
"LabelDeselectAll": "Desseleccionar Tots",
|
||||||
"LabelDevice": "Dispositiu",
|
"LabelDevice": "Dispositiu",
|
||||||
"LabelDeviceInfo": "Informació del Dispositiu",
|
"LabelDeviceInfo": "Informació de l'aparell",
|
||||||
"LabelDeviceIsAvailableTo": "El dispositiu està disponible per a...",
|
"LabelDeviceIsAvailableTo": "El dispositiu està disponible per a...",
|
||||||
"LabelDirectory": "Directori",
|
"LabelDirectory": "Directori",
|
||||||
"LabelDiscFromFilename": "Disc a partir del nom de fitxer",
|
"LabelDiscFromFilename": "Disc a partir del nom de fitxer",
|
||||||
@@ -333,11 +336,11 @@
|
|||||||
"LabelEnd": "Fi",
|
"LabelEnd": "Fi",
|
||||||
"LabelEndOfChapter": "Fi del capítol",
|
"LabelEndOfChapter": "Fi del capítol",
|
||||||
"LabelEpisode": "Episodi",
|
"LabelEpisode": "Episodi",
|
||||||
"LabelEpisodeNotLinkedToRssFeed": "Episodi no enllaçat al feed RSS",
|
"LabelEpisodeNotLinkedToRssFeed": "Episodi no enllaçat al canal RSS",
|
||||||
"LabelEpisodeNumber": "Episodi #{0}",
|
"LabelEpisodeNumber": "Episodi #{0}",
|
||||||
"LabelEpisodeTitle": "Títol de l'Episodi",
|
"LabelEpisodeTitle": "Títol de l'Episodi",
|
||||||
"LabelEpisodeType": "Tipus d'Episodi",
|
"LabelEpisodeType": "Tipus d'Episodi",
|
||||||
"LabelEpisodeUrlFromRssFeed": "URL de l'episodi del feed RSS",
|
"LabelEpisodeUrlFromRssFeed": "URL de l'episodi del canal RSS",
|
||||||
"LabelEpisodes": "Episodis",
|
"LabelEpisodes": "Episodis",
|
||||||
"LabelEpisodic": "Episodis",
|
"LabelEpisodic": "Episodis",
|
||||||
"LabelExample": "Exemple",
|
"LabelExample": "Exemple",
|
||||||
@@ -350,7 +353,7 @@
|
|||||||
"LabelFeedURL": "Font de URL",
|
"LabelFeedURL": "Font de URL",
|
||||||
"LabelFetchingMetadata": "Obtenció de metadades",
|
"LabelFetchingMetadata": "Obtenció de metadades",
|
||||||
"LabelFile": "Fitxer",
|
"LabelFile": "Fitxer",
|
||||||
"LabelFileBirthtime": "Arxiu creat a",
|
"LabelFileBirthtime": "Fitxer creat a",
|
||||||
"LabelFileBornDate": "Creat {0}",
|
"LabelFileBornDate": "Creat {0}",
|
||||||
"LabelFileModified": "Fitxer modificat",
|
"LabelFileModified": "Fitxer modificat",
|
||||||
"LabelFileModifiedDate": "Modificat {0}",
|
"LabelFileModifiedDate": "Modificat {0}",
|
||||||
@@ -437,7 +440,7 @@
|
|||||||
"LabelMinute": "Minut",
|
"LabelMinute": "Minut",
|
||||||
"LabelMinutes": "Minuts",
|
"LabelMinutes": "Minuts",
|
||||||
"LabelMissing": "Absent",
|
"LabelMissing": "Absent",
|
||||||
"LabelMissingEbook": "No té ebook",
|
"LabelMissingEbook": "No té llibre electrònic",
|
||||||
"LabelMissingSupplementaryEbook": "No té ebook complementari",
|
"LabelMissingSupplementaryEbook": "No té ebook complementari",
|
||||||
"LabelMobileRedirectURIs": "URI de redirecció mòbil permeses",
|
"LabelMobileRedirectURIs": "URI de redirecció mòbil permeses",
|
||||||
"LabelMobileRedirectURIsDescription": "Aquesta és una llista blanca d'URI de redirecció vàlides per a aplicacions mòbils. El predeterminat és <code> audiobookshelf</code>, que pots eliminar o complementar amb URI addicionals per a la integració d'aplicacions de tercers. Usant un asterisc (<code> *</code>) com a única entrada que permet qualsevol URI.",
|
"LabelMobileRedirectURIsDescription": "Aquesta és una llista blanca d'URI de redirecció vàlides per a aplicacions mòbils. El predeterminat és <code> audiobookshelf</code>, que pots eliminar o complementar amb URI addicionals per a la integració d'aplicacions de tercers. Usant un asterisc (<code> *</code>) com a única entrada que permet qualsevol URI.",
|
||||||
@@ -471,6 +474,7 @@
|
|||||||
"LabelOpenIDAdvancedPermsClaimDescription": "Nom de la notificació de OpenID que conté permisos avançats per accions d'usuari dins l'aplicació que s'aplicaran a rols que no siguin d'administrador (<b>si estan configurats</b>). Si el reclam no apareix en la resposta, es denegarà l'accés a ABS. Si manca una sola opció, es tractarà com a <code>falsa</code>. Assegura't que la notificació del proveïdor d'identitats coincideixi amb l'estructura esperada:",
|
"LabelOpenIDAdvancedPermsClaimDescription": "Nom de la notificació de OpenID que conté permisos avançats per accions d'usuari dins l'aplicació que s'aplicaran a rols que no siguin d'administrador (<b>si estan configurats</b>). Si el reclam no apareix en la resposta, es denegarà l'accés a ABS. Si manca una sola opció, es tractarà com a <code>falsa</code>. Assegura't que la notificació del proveïdor d'identitats coincideixi amb l'estructura esperada:",
|
||||||
"LabelOpenIDClaims": "Deixa les següents opcions buides per desactivar l'assignació avançada de grups i permisos, el que assignaria automàticament al grup 'Usuari'.",
|
"LabelOpenIDClaims": "Deixa les següents opcions buides per desactivar l'assignació avançada de grups i permisos, el que assignaria automàticament al grup 'Usuari'.",
|
||||||
"LabelOpenIDGroupClaimDescription": "Nom de la declaració OpenID que conté una llista de grups de l'usuari. Comunament coneguts com <code>grups</code>. <b>Si es configura</b>, l'aplicació assignarà automàticament rols basats en la pertinença a grups de l'usuari, sempre que aquests grups es denominen 'admin', 'user' o 'guest' en la notificació. La sol·licitud ha de contenir una llista, i si un usuari pertany a diversos grups, l'aplicació assignarà el rol corresponent al major nivell d'accés. Si cap grup coincideix, es denegarà l'accés.",
|
"LabelOpenIDGroupClaimDescription": "Nom de la declaració OpenID que conté una llista de grups de l'usuari. Comunament coneguts com <code>grups</code>. <b>Si es configura</b>, l'aplicació assignarà automàticament rols basats en la pertinença a grups de l'usuari, sempre que aquests grups es denominen 'admin', 'user' o 'guest' en la notificació. La sol·licitud ha de contenir una llista, i si un usuari pertany a diversos grups, l'aplicació assignarà el rol corresponent al major nivell d'accés. Si cap grup coincideix, es denegarà l'accés.",
|
||||||
|
"LabelOpenRSSFeed": "Obre el canal RSS",
|
||||||
"LabelOverwrite": "Sobreescriure",
|
"LabelOverwrite": "Sobreescriure",
|
||||||
"LabelPaginationPageXOfY": "Pàgina {0} de {1}",
|
"LabelPaginationPageXOfY": "Pàgina {0} de {1}",
|
||||||
"LabelPassword": "Contrasenya",
|
"LabelPassword": "Contrasenya",
|
||||||
@@ -494,25 +498,25 @@
|
|||||||
"LabelPodcastType": "Tipus de pòdcast",
|
"LabelPodcastType": "Tipus de pòdcast",
|
||||||
"LabelPodcasts": "Pòdcasts",
|
"LabelPodcasts": "Pòdcasts",
|
||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Prefixos per Ignorar (no distingeix entre majúscules i minúscules.)",
|
"LabelPrefixesToIgnore": "Prefixos a ignorar (no distingeix entre majúscules i minúscules)",
|
||||||
"LabelPreventIndexing": "Evita que la teva font sigui indexada pels directoris de podcasts d'iTunes i Google",
|
"LabelPreventIndexing": "Evita que el vostre canal l'indexin els directoris de pòdcasts de l'iTunes i Google",
|
||||||
"LabelPrimaryEbook": "Ebook Principal",
|
"LabelPrimaryEbook": "Llibre electrònic principal",
|
||||||
"LabelProgress": "Progrés",
|
"LabelProgress": "Progrés",
|
||||||
"LabelProvider": "Proveïdor",
|
"LabelProvider": "Proveïdor",
|
||||||
"LabelProviderAuthorizationValue": "Valor de l'encapçalament d'autorització",
|
"LabelProviderAuthorizationValue": "Valor de l'encapçalament d'autorització",
|
||||||
"LabelPubDate": "Data de Publicació",
|
"LabelPubDate": "Data de publicació",
|
||||||
"LabelPublishYear": "Any de Publicació",
|
"LabelPublishYear": "Any de publicació",
|
||||||
"LabelPublishedDate": "Publicat {0}",
|
"LabelPublishedDate": "Publicat {0}",
|
||||||
"LabelPublishedDecade": "Dècada de Publicació",
|
"LabelPublishedDecade": "Dècada de publicació",
|
||||||
"LabelPublishedDecades": "Dècades Publicades",
|
"LabelPublishedDecades": "Dècades Publicades",
|
||||||
"LabelPublisher": "Editor",
|
"LabelPublisher": "Editor",
|
||||||
"LabelPublishers": "Editors",
|
"LabelPublishers": "Editors",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Correu Electrònic Personalitzat del Propietari",
|
"LabelRSSFeedCustomOwnerEmail": "Correu Electrònic Personalitzat del Propietari",
|
||||||
"LabelRSSFeedCustomOwnerName": "Nom Personalitzat del Propietari",
|
"LabelRSSFeedCustomOwnerName": "Nom Personalitzat del Propietari",
|
||||||
"LabelRSSFeedOpen": "Font RSS Oberta",
|
"LabelRSSFeedOpen": "Font RSS Oberta",
|
||||||
"LabelRSSFeedPreventIndexing": "Evitar l'indexació",
|
"LabelRSSFeedPreventIndexing": "Evita la indexació",
|
||||||
"LabelRSSFeedSlug": "Font RSS Slug",
|
"LabelRSSFeedSlug": "URL semàntic del canal RSS",
|
||||||
"LabelRSSFeedURL": "URL de la Font RSS",
|
"LabelRSSFeedURL": "URL del canal RSS",
|
||||||
"LabelRandomly": "A l'atzar",
|
"LabelRandomly": "A l'atzar",
|
||||||
"LabelReAddSeriesToContinueListening": "Reafegir la sèrie per continuar escoltant-la",
|
"LabelReAddSeriesToContinueListening": "Reafegir la sèrie per continuar escoltant-la",
|
||||||
"LabelRead": "Llegit",
|
"LabelRead": "Llegit",
|
||||||
@@ -521,52 +525,61 @@
|
|||||||
"LabelRecentSeries": "Sèries recents",
|
"LabelRecentSeries": "Sèries recents",
|
||||||
"LabelRecentlyAdded": "Addicions recents",
|
"LabelRecentlyAdded": "Addicions recents",
|
||||||
"LabelRecommended": "Recomanats",
|
"LabelRecommended": "Recomanats",
|
||||||
"LabelRedo": "Refer",
|
"LabelRedo": "Refés",
|
||||||
"LabelRegion": "Regió",
|
"LabelRegion": "Regió",
|
||||||
"LabelReleaseDate": "Data d'Estrena",
|
"LabelReleaseDate": "Data d'estrena",
|
||||||
"LabelRemoveAllMetadataAbs": "Eliminar tots els fitxers metadata.abs",
|
"LabelRemoveAllMetadataAbs": "Elimina tots els fitxers metadata.abs",
|
||||||
"LabelRemoveAllMetadataJson": "Eliminar tots els fitxers metadata.json",
|
"LabelRemoveAllMetadataJson": "Elimina tots els fitxers metadata.json",
|
||||||
"LabelRemoveCover": "Eliminar Coberta",
|
"LabelRemoveAudibleBranding": "Elimina la introducció i el tancament de l'Audible dels capítols",
|
||||||
|
"LabelRemoveCover": "Elimina la coberta",
|
||||||
"LabelRemoveMetadataFile": "Eliminar fitxers de metadades en carpetes d'elements de biblioteca",
|
"LabelRemoveMetadataFile": "Eliminar fitxers de metadades en carpetes d'elements de biblioteca",
|
||||||
"LabelRemoveMetadataFileHelp": "Elimina tots els fitxers metadata.json i metadata.abs de les teves carpetes {0}.",
|
"LabelRemoveMetadataFileHelp": "Elimina tots els fitxers metadata.json i metadata.abs de les vostres carpetes {0}.",
|
||||||
"LabelRowsPerPage": "Files per Pàgina",
|
"LabelRowsPerPage": "Files per pàgina",
|
||||||
"LabelSearchTerm": "Cercar Terme",
|
"LabelSearchTerm": "Cerca terme",
|
||||||
"LabelSearchTitle": "Cercar Títol",
|
"LabelSearchTitle": "Cerca títol",
|
||||||
"LabelSearchTitleOrASIN": "Cercar Títol o ASIN",
|
"LabelSearchTitleOrASIN": "Cerca títol o ASIN",
|
||||||
"LabelSeason": "Temporada",
|
"LabelSeason": "Temporada",
|
||||||
"LabelSeasonNumber": "Temporada #{0}",
|
"LabelSeasonNumber": "{0}a temporada",
|
||||||
"LabelSelectAll": "Seleccionar tot",
|
"LabelSelectAll": "Selecciona-ho tot",
|
||||||
"LabelSelectAllEpisodes": "Seleccionar tots els episodis",
|
"LabelSelectAllEpisodes": "Selecciona tots els episodis",
|
||||||
"LabelSelectEpisodesShowing": "Seleccionar els {0} episodis visibles",
|
"LabelSelectEpisodesShowing": "Seleccionar els {0} episodis visibles",
|
||||||
"LabelSelectUsers": "Seleccionar usuaris",
|
"LabelSelectUsers": "Seleccionar usuaris",
|
||||||
"LabelSendEbookToDevice": "Enviar Ebook a...",
|
"LabelSendEbookToDevice": "Enviar Ebook a...",
|
||||||
"LabelSequence": "Seqüència",
|
"LabelSequence": "Seqüència",
|
||||||
"LabelSerial": "En sèrie",
|
"LabelSerial": "En sèrie",
|
||||||
"LabelSeries": "Sèries",
|
"LabelSeries": "Sèrie",
|
||||||
"LabelSeriesName": "Nom de la Sèrie",
|
"LabelSeriesName": "Nom de la sèrie",
|
||||||
"LabelSeriesProgress": "Progrés de la Sèrie",
|
"LabelSeriesProgress": "Progrés de la sèrie",
|
||||||
"LabelServerLogLevel": "Nivell de registre del servidor",
|
"LabelServerLogLevel": "Nivell de registre del servidor",
|
||||||
"LabelServerYearReview": "Resum de l'any del servidor ({0})",
|
"LabelServerYearReview": "Resum de l'any del servidor ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Establir com a principal",
|
"LabelSetEbookAsPrimary": "Establir com a principal",
|
||||||
"LabelSetEbookAsSupplementary": "Establir com a suplementari",
|
"LabelSetEbookAsSupplementary": "Establir com a suplementari",
|
||||||
"LabelSettingsAudiobooksOnly": "Només Audiollibres",
|
"LabelSettingsAudiobooksOnly": "Només audiollibres",
|
||||||
"LabelSettingsAudiobooksOnlyHelp": "Activant aquesta opció s'ignoraran els fitxers d'ebook, excepte si estan dins d'una carpeta d'audiollibre, en aquest cas es marcaran com ebooks suplementaris",
|
"LabelSettingsAudiobooksOnlyHelp": "En activar aquesta opció s'ignoraran els fitxers de llibre electrònic, excepte si estan dins d'una carpeta d'audiollibre; en aquest cas es marcaran com a llibres suplementaris",
|
||||||
"LabelSettingsBookshelfViewHelp": "Disseny esqueomorf amb prestatgeries de fusta",
|
"LabelSettingsBookshelfViewHelp": "Disseny esqueomorf amb prestatgeries de fusta",
|
||||||
"LabelSettingsChromecastSupport": "Compatibilitat amb Chromecast",
|
"LabelSettingsChromecastSupport": "Compatibilitat amb Chromecast",
|
||||||
"LabelSettingsDateFormat": "Format de Data",
|
"LabelSettingsDateFormat": "Format de data",
|
||||||
"LabelSettingsEnableWatcherHelp": "Permet afegir/actualitzar elements automàticament quan es detectin canvis en els fitxers. *Requereix reiniciar el servidor",
|
"LabelSettingsEnableWatcherHelp": "Permet afegir/actualitzar elements automàticament quan es detectin canvis en els fitxers. *Requereix reiniciar el servidor",
|
||||||
"LabelSettingsEpubsAllowScriptedContent": "Permetre scripts en epubs",
|
"LabelSettingsEpubsAllowScriptedContent": "Permetre scripts en epubs",
|
||||||
"LabelSettingsEpubsAllowScriptedContentHelp": "Permetre que els fitxers epub executin scripts. Es recomana mantenir aquesta opció desactivada tret que confiïs en l'origen dels fitxers epub.",
|
"LabelSettingsEpubsAllowScriptedContentHelp": "Permetre que els fitxers epub executin scripts. Es recomana mantenir aquesta opció desactivada tret que confiïs en l'origen dels fitxers epub.",
|
||||||
"LabelSettingsExperimentalFeatures": "Funcions Experimentals",
|
"LabelSettingsExperimentalFeatures": "Funcions Experimentals",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Funcions en desenvolupament que es beneficiarien dels teus comentaris i experiències de prova. Feu clic aquí per obrir una conversa a Github.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funcions en desenvolupament que es beneficiarien dels teus comentaris i experiències de prova. Feu clic aquí per obrir una conversa a Github.",
|
||||||
"LabelSettingsFindCovers": "Troba cobertes",
|
"LabelSettingsFindCovers": "Troba cobertes",
|
||||||
|
"LabelSettingsHideSingleBookSeries": "Amaga les sèries amb un sol llibre",
|
||||||
|
"LabelSettingsParseSubtitles": "Analitza els subtítols",
|
||||||
"LabelSettingsSortingIgnorePrefixes": "Ignora els prefixos en ordenar",
|
"LabelSettingsSortingIgnorePrefixes": "Ignora els prefixos en ordenar",
|
||||||
|
"LabelSettingsTimeFormat": "Format d'hora",
|
||||||
|
"LabelShare": "Comparteix",
|
||||||
|
"LabelShareDownloadableHelp": "Permet els usuaris amb l'enllaç de compartició de baixar un fitxer ZIP amb l'element de la biblioteca.",
|
||||||
|
"LabelShareURL": "URL de compartició",
|
||||||
"LabelShowAll": "Mostra-ho tot",
|
"LabelShowAll": "Mostra-ho tot",
|
||||||
"LabelShowSeconds": "Mostra segons",
|
"LabelShowSeconds": "Mostra segons",
|
||||||
"LabelShowSubtitles": "Mostra subtítols",
|
"LabelShowSubtitles": "Mostra subtítols",
|
||||||
"LabelSize": "Mida",
|
"LabelSize": "Mida",
|
||||||
"LabelSleepTimer": "Temporitzador de repòs",
|
"LabelSleepTimer": "Temporitzador de repòs",
|
||||||
"LabelSlug": "Slug",
|
"LabelSlug": "Slug",
|
||||||
|
"LabelSortAscending": "Ascendent",
|
||||||
|
"LabelSortDescending": "Descendent",
|
||||||
"LabelStart": "Inicia",
|
"LabelStart": "Inicia",
|
||||||
"LabelStartTime": "Hora d'inici",
|
"LabelStartTime": "Hora d'inici",
|
||||||
"LabelStarted": "Iniciat",
|
"LabelStarted": "Iniciat",
|
||||||
@@ -654,89 +667,98 @@
|
|||||||
"LabelViewPlayerSettings": "Mostra els ajustaments del reproductor",
|
"LabelViewPlayerSettings": "Mostra els ajustaments del reproductor",
|
||||||
"LabelViewQueue": "Mostra cua del reproductor",
|
"LabelViewQueue": "Mostra cua del reproductor",
|
||||||
"LabelVolume": "Volum",
|
"LabelVolume": "Volum",
|
||||||
"LabelWebRedirectURLsDescription": "Autoritza aquestes URL al teu proveïdor OAuth per permetre redirecció a l'aplicació web després d'iniciar sessió:",
|
"LabelWebRedirectURLsDescription": "Autoritzeu aquests URL al vostre proveïdor OAuth per a permetre redirigir a l’aplicació web després d'iniciar sessió:",
|
||||||
"LabelWebRedirectURLsSubfolder": "Subcarpeta per a URL de redirecció",
|
"LabelWebRedirectURLsSubfolder": "Subcarpeta per a URL de redirecció",
|
||||||
"LabelWeekdaysToRun": "Executar en dies de la setmana",
|
"LabelWeekdaysToRun": "Executar en dies de la setmana",
|
||||||
"LabelXBooks": "{0} llibres",
|
"LabelXBooks": "{0} llibres",
|
||||||
"LabelXItems": "{0} elements",
|
"LabelXItems": "{0} elements",
|
||||||
"LabelYearReviewHide": "Oculta resum de l'any",
|
"LabelYearReviewHide": "Oculta resum de l'any",
|
||||||
"LabelYearReviewShow": "Mostra resum de l'any",
|
"LabelYearReviewShow": "Mostra resum de l'any",
|
||||||
"LabelYourAudiobookDuration": "Duració del teu audiollibre",
|
"LabelYourAudiobookDuration": "Duració del vostre audiollibre",
|
||||||
"LabelYourBookmarks": "Els vostres marcadors",
|
"LabelYourBookmarks": "Els vostres marcadors",
|
||||||
"LabelYourPlaylists": "Les teves llistes",
|
"LabelYourPlaylists": "Les vostres llistes",
|
||||||
"LabelYourProgress": "El vostre progrés",
|
"LabelYourProgress": "El vostre progrés",
|
||||||
"MessageAddToPlayerQueue": "Afegeix a la cua del reproductor",
|
"MessageAddToPlayerQueue": "Afegeix a la cua del reproductor",
|
||||||
"MessageAppriseDescription": "Per utilitzar aquesta funció, hauràs de tenir l'<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API d'Apprise</a> en funcionament o una API que gestioni resultats similars. <br/>La URL de l'API d'Apprise ha de tenir la mateixa ruta d'arxius que on s'envien les notificacions. Per exemple: si la teva API és a <code>http://192.168.1.1:8337</code>, llavors posaries <code>http://192.168.1.1:8337/notify</code>.",
|
"MessageAppriseDescription": "Per utilitzar aquesta funció, hauràs de tenir l'<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API d'Apprise</a> en funcionament o una API que gestioni resultats similars. <br/>La URL de l'API d'Apprise ha de tenir la mateixa ruta d'arxius que on s'envien les notificacions. Per exemple: si la teva API és a <code>http://192.168.1.1:8337</code>, llavors posaries <code>http://192.168.1.1:8337/notify</code>.",
|
||||||
|
"MessageAuthenticationOIDCChangesRestart": "Reengegueu el servidor després de desar perquè s'hi apliquin els canvis d'OIDC.",
|
||||||
"MessageBackupsDescription": "Les còpies de seguretat inclouen: usuaris, progrés dels usuaris, detalls dels elements de la biblioteca, configuració del servidor i imatges a <code>/metadata/items</code> i <code>/metadata/authors</code>. Les còpies de seguretat <strong>NO</strong> inclouen cap fitxer guardat a la carpeta de la teva biblioteca.",
|
"MessageBackupsDescription": "Les còpies de seguretat inclouen: usuaris, progrés dels usuaris, detalls dels elements de la biblioteca, configuració del servidor i imatges a <code>/metadata/items</code> i <code>/metadata/authors</code>. Les còpies de seguretat <strong>NO</strong> inclouen cap fitxer guardat a la carpeta de la teva biblioteca.",
|
||||||
"MessageBackupsLocationEditNote": "Nota: Actualitzar la ubicació de la còpia de seguretat no mourà ni modificarà les còpies existents",
|
"MessageBackupsLocationEditNote": "Nota: Actualitzar la ubicació de la còpia de seguretat no mourà ni modificarà les còpies existents",
|
||||||
"MessageBackupsLocationNoEditNote": "Nota: La ubicació de la còpia de seguretat es defineix mitjançant una variable d'entorn i no es pot modificar aquí.",
|
"MessageBackupsLocationNoEditNote": "Nota: La ubicació de la còpia de seguretat es defineix mitjançant una variable d'entorn i no es pot modificar aquí.",
|
||||||
"MessageBackupsLocationPathEmpty": "La ruta de la còpia de seguretat no pot estar buida",
|
"MessageBackupsLocationPathEmpty": "La ruta de la còpia de seguretat no pot estar buida",
|
||||||
"MessageBatchQuickMatchDescription": "La funció \"Troba Ràpid\" intentarà afegir portades i metadades que falten als elements seleccionats. Activa l'opció següent perquè \"Troba Ràpid\" pugui sobreescriure portades i/o metadades existents.",
|
"MessageBatchQuickMatchDescription": "La funció \"Troba Ràpid\" intentarà afegir portades i metadades que falten als elements seleccionats. Activa l'opció següent perquè \"Troba Ràpid\" pugui sobreescriure portades i/o metadades existents.",
|
||||||
"MessageBookshelfNoCollections": "No tens cap col·lecció",
|
"MessageBookshelfNoCollections": "Encara no heu fet cap col·lecció",
|
||||||
|
"MessageBookshelfNoCollectionsHelp": "Les col·leccions són públiques. Tots els usuaris amb accés a la biblioteca les podran veure.",
|
||||||
"MessageBookshelfNoRSSFeeds": "Cap font RSS està oberta",
|
"MessageBookshelfNoRSSFeeds": "Cap font RSS està oberta",
|
||||||
"MessageBookshelfNoResultsForFilter": "Cap resultat per al filtre \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Cap resultat per al filtre «{0}: {1}»",
|
||||||
"MessageBookshelfNoResultsForQuery": "Cap resultat per a la consulta",
|
"MessageBookshelfNoResultsForQuery": "Cap resultat per a la consulta",
|
||||||
"MessageBookshelfNoSeries": "No tens cap sèrie",
|
"MessageBookshelfNoSeries": "No teniu cap sèrie",
|
||||||
"MessageChapterEndIsAfter": "El final del capítol és després del final del teu audiollibre",
|
"MessageChapterEndIsAfter": "El final del capítol és després del final del teu audiollibre",
|
||||||
"MessageChapterErrorFirstNotZero": "El primer capítol ha de començar a 0",
|
"MessageChapterErrorFirstNotZero": "El primer capítol ha de començar a 0",
|
||||||
"MessageChapterErrorStartGteDuration": "El temps d'inici no és vàlid: ha de ser inferior a la durada de l'audiollibre",
|
"MessageChapterErrorStartGteDuration": "El temps d'inici no és vàlid: ha de ser inferior a la durada de l'audiollibre",
|
||||||
"MessageChapterErrorStartLtPrev": "El temps d'inici no és vàlid: ha de ser igual o més gran que el temps d'inici del capítol anterior",
|
"MessageChapterErrorStartLtPrev": "El temps d'inici no és vàlid: ha de ser igual o més gran que el temps d'inici del capítol anterior",
|
||||||
"MessageChapterStartIsAfter": "L'inici del capítol és després del final del teu audiollibre",
|
"MessageChapterStartIsAfter": "L'inici del capítol és després del final del teu audiollibre",
|
||||||
|
"MessageChaptersNotFound": "No s'han trobat els capítols",
|
||||||
"MessageCheckingCron": "Comprovant cron...",
|
"MessageCheckingCron": "Comprovant cron...",
|
||||||
"MessageConfirmCloseFeed": "Estàs segur que vols tancar aquesta font?",
|
"MessageConfirmCloseFeed": "Segur que voleu tancar aquest canal?",
|
||||||
"MessageConfirmDeleteBackup": "Estàs segur que vols eliminar la còpia de seguretat {0}?",
|
"MessageConfirmDeleteBackup": "Segur que voleu suprimir la còpia de seguretat de {0}?",
|
||||||
"MessageConfirmDeleteDevice": "Estàs segur que vols eliminar el lector electrònic \"{0}\"?",
|
"MessageConfirmDeleteDevice": "Segur que voleu suprimir el lector electrònic «{0}»?",
|
||||||
"MessageConfirmDeleteFile": "Això eliminarà el fitxer del teu sistema. Estàs segur?",
|
"MessageConfirmDeleteFile": "Això suprimirà el fitxer del vostre sistema de fitxers. N'esteu segur?",
|
||||||
"MessageConfirmDeleteLibrary": "Estàs segur que vols eliminar permanentment la biblioteca \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Segur que voleu suprimir permanentment la biblioteca «{0}»?",
|
||||||
"MessageConfirmDeleteLibraryItem": "Això eliminarà l'element de la base de dades i del sistema. Estàs segur?",
|
"MessageConfirmDeleteLibraryItem": "Això suprimirà l’element de la base de dades i del sistema de fitxers. N’esteu segur?",
|
||||||
"MessageConfirmDeleteLibraryItems": "Això eliminarà {0} element(s) de la base de dades i del sistema. Estàs segur?",
|
"MessageConfirmDeleteLibraryItems": "Això suprimirà {0} element(s) de la base de dades i del sistema de fitxers. N'esteu segur?",
|
||||||
"MessageConfirmDeleteMetadataProvider": "Estàs segur que vols eliminar el proveïdor de metadades personalitzat \"{0}\"?",
|
"MessageConfirmDeleteMetadataProvider": "Segur que voleu suprimir el proveïdor de metadades personalitzat «{0}»?",
|
||||||
"MessageConfirmDeleteNotification": "Estàs segur que vols eliminar aquesta notificació?",
|
"MessageConfirmDeleteNotification": "Segur que voleu suprimir aquesta notificació?",
|
||||||
"MessageConfirmDeleteSession": "Estàs segur que vols eliminar aquesta sessió?",
|
"MessageConfirmDeleteSession": "Segur que voleu suprimir aquesta sessió?",
|
||||||
"MessageConfirmEmbedMetadataInAudioFiles": "Estàs segur que vols incrustar metadades a {0} fitxer(s) d'àudio?",
|
"MessageConfirmEmbedMetadataInAudioFiles": "Segur que voleu incrustar metadades a {0} fitxer(s) d'àudio?",
|
||||||
"MessageConfirmForceReScan": "Estàs segur que vols forçar un reescaneig?",
|
"MessageConfirmForceReScan": "Segur que voleu forçar un reescaneig?",
|
||||||
"MessageConfirmMarkAllEpisodesFinished": "Estàs segur que vols marcar tots els episodis com a acabats?",
|
"MessageConfirmMarkAllEpisodesFinished": "Segur que voleu marcar tots els episodis com a acabats?",
|
||||||
"MessageConfirmMarkAllEpisodesNotFinished": "Estàs segur que vols marcar tots els episodis com a no acabats?",
|
"MessageConfirmMarkAllEpisodesNotFinished": "Segur que voleu marcar tots els episodis com a no acabats?",
|
||||||
"MessageConfirmMarkItemFinished": "Estàs segur que vols marcar \"{0}\" com a acabat?",
|
"MessageConfirmMarkItemFinished": "Segur que voleu marcar «{0}» com a acabat?",
|
||||||
"MessageConfirmMarkItemNotFinished": "Estàs segur que vols marcar \"{0}\" com a no acabat?",
|
"MessageConfirmMarkItemNotFinished": "Segur que voleu marcar «{0}» com a no acabat?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Estàs segur que vols marcar tots els llibres d'aquesta sèrie com a acabats?",
|
"MessageConfirmMarkSeriesFinished": "Segur que voleu marcar tots els llibres d'aquesta sèrie com a acabats?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Estàs segur que vols marcar tots els llibres d'aquesta sèrie com a no acabats?",
|
"MessageConfirmMarkSeriesNotFinished": "Segur que voleu marcar tots els llibres d'aquesta sèrie com a no acabats?",
|
||||||
"MessageConfirmNotificationTestTrigger": "Vols activar aquesta notificació amb dades de prova?",
|
"MessageConfirmNotificationTestTrigger": "Voleu activar aquesta notificació amb dades de prova?",
|
||||||
"MessageConfirmPurgeCache": "Esborrar la memòria cau eliminarà tot el directori localitzat a <code>/metadata/cache</code>. <br /><br />Estàs segur que vols eliminar-lo?",
|
"MessageConfirmPurgeCache": "Purgar la memòria cau suprimirà tot el directori localitzat a <code>/metadata/cache</code>. <br /><br />Segur que voleu eliminar-lo?",
|
||||||
"MessageConfirmPurgeItemsCache": "Esborrar la memòria cau dels elements eliminarà el directori <code>/metadata/cache/items</code>.<br />Estàs segur?",
|
"MessageConfirmPurgeItemsCache": "Esborrar la memòria cau dels elements eliminarà el directori <code>/metadata/cache/items</code>.<br />Estàs segur?",
|
||||||
"MessageConfirmQuickEmbed": "Advertència! La integració ràpida no fa còpies de seguretat dels teus fitxers d'àudio. Assegura't d'haver-ne fet una còpia abans. <br><br>Vols continuar?",
|
"MessageConfirmQuickEmbed": "Avís: la incrustació ràpida no fa còpies de seguretat dels vostres fitxers d'àudio. Assegureu-vos d'haver-ne fet una còpia abans. <br><br>Voleu continuar?",
|
||||||
"MessageConfirmQuickMatchEpisodes": "El reconeixement ràpid sobreescriurà els detalls si es troba una coincidència. Estàs segur?",
|
"MessageConfirmQuickMatchEpisodes": "El reconeixement ràpid sobreescriurà els detalls si es troba una coincidència. Estàs segur?",
|
||||||
"MessageConfirmReScanLibraryItems": "Estàs segur que vols reescanejar {0} element(s)?",
|
"MessageConfirmReScanLibraryItems": "Segur que voleu reescanejar {0} element(s)?",
|
||||||
"MessageConfirmRemoveAllChapters": "Estàs segur que vols eliminar tots els capítols?",
|
"MessageConfirmRemoveAllChapters": "Segur que voleu eliminar tots els capítols?",
|
||||||
"MessageConfirmRemoveAuthor": "Estàs segur que vols eliminar l'autor \"{0}\"?",
|
"MessageConfirmRemoveAuthor": "Segur que voleu eliminar l'autor «{0}»?",
|
||||||
"MessageConfirmRemoveCollection": "Estàs segur que vols eliminar la col·lecció \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Segur que voleu eliminar la col·lecció «{0}»?",
|
||||||
"MessageConfirmRemoveEpisode": "Estàs segur que vols eliminar l'episodi \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Segur que voleu eliminar l'episodi «{0}»?",
|
||||||
"MessageConfirmRemoveEpisodes": "Estàs segur que vols eliminar {0} episodis?",
|
"MessageConfirmRemoveEpisodes": "Segur que voleu eliminar {0} episodis?",
|
||||||
"MessageConfirmRemoveListeningSessions": "Estàs segur que vols eliminar {0} sessions d'escolta?",
|
"MessageConfirmRemoveListeningSessions": "Segur que voleu eliminar {0} sessions d'escolta?",
|
||||||
"MessageConfirmRemoveMetadataFiles": "Estàs segur que vols eliminar tots els fitxers de metadades.{0} de les carpetes dels elements de la teva biblioteca?",
|
"MessageConfirmRemoveMetadataFiles": "Segur que voleu eliminar tots els fitxers metadata.{0} de les carpetes dels elements de la vostra biblioteca?",
|
||||||
"MessageConfirmRemoveNarrator": "Estàs segur que vols eliminar el narrador \"{0}\"?",
|
"MessageConfirmRemoveNarrator": "Segur que voleu eliminar el narrador «{0}»?",
|
||||||
"MessageConfirmRemovePlaylist": "Estàs segur que vols eliminar la llista de reproducció \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Segur que voleu eliminar la llista de reproducció «{0}»?",
|
||||||
"MessageConfirmRenameGenre": "Estàs segur que vols canviar el gènere \"{0}\" a \"{1}\" per a tots els elements?",
|
"MessageConfirmRenameGenre": "Segur que voleu canviar el nom del gènere «{0}» a «{1}» per a tots els elements?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "Nota: Aquest gènere ja existeix, i es fusionarà.",
|
"MessageConfirmRenameGenreMergeNote": "Nota: Aquest gènere ja existeix, i es fusionarà.",
|
||||||
"MessageConfirmRenameGenreWarning": "Advertència! Ja existeix un gènere similar \"{0}\".",
|
"MessageConfirmRenameGenreWarning": "Advertència! Ja existeix un gènere similar \"{0}\".",
|
||||||
"MessageConfirmRenameTag": "Estàs segur que vols canviar l'etiqueta \"{0}\" a \"{1}\" per a tots els elements?",
|
"MessageConfirmRenameTag": "Segur que voleu canviar el nom de l'etiqueta «{0}» a «{1}» per a tots els elements?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Nota: Aquesta etiqueta ja existeix, i es fusionarà.",
|
"MessageConfirmRenameTagMergeNote": "Nota: Aquesta etiqueta ja existeix, i es fusionarà.",
|
||||||
"MessageConfirmRenameTagWarning": "Advertència! Ja existeix una etiqueta similar \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Advertència! Ja existeix una etiqueta similar \"{0}\".",
|
||||||
"MessageConfirmResetProgress": "Estàs segur que vols reiniciar el teu progrés?",
|
"MessageConfirmResetProgress": "Segur que voleu reinicialitzar el vostre progrés?",
|
||||||
"MessageConfirmSendEbookToDevice": "Estàs segur que vols enviar {0} ebook(s) \"{1}\" al dispositiu \"{2}\"?",
|
"MessageConfirmSendEbookToDevice": "Segur que voleu enviar {0} llibre(s) «{1}» al dispositiu «{2}»?",
|
||||||
"MessageConfirmUnlinkOpenId": "Estàs segur que vols desvincular aquest usuari d'OpenID?",
|
"MessageConfirmUnlinkOpenId": "Segur que voleu desenllaçar aquest usuari d'OpenID?",
|
||||||
"MessageDaysListenedInTheLastYear": "{0} dies escoltats l'any passat",
|
"MessageDaysListenedInTheLastYear": "{0} dies escoltats l'any passat",
|
||||||
"MessageDownloadingEpisode": "S'està baixant l'episodi",
|
"MessageDownloadingEpisode": "S'està baixant l'episodi",
|
||||||
"MessageDragFilesIntoTrackOrder": "Arrossega els fitxers en l'ordre correcte de les pistes",
|
"MessageDragFilesIntoTrackOrder": "Arrossega els fitxers en l'ordre correcte de les pistes",
|
||||||
"MessageEmbedFailed": "Error en incrustar!",
|
"MessageEmbedFailed": "Error en incrustar!",
|
||||||
"MessageEmbedFinished": "Incrustació acabada!",
|
"MessageEmbedFinished": "Incrustació acabada!",
|
||||||
"MessageEmbedQueue": "En cua per incrustar metadades ({0} en cua)",
|
"MessageEmbedQueue": "En cua per incrustar metadades ({0} en cua)",
|
||||||
|
"MessageFeedURLWillBe": "L'URL del canal serà {0}",
|
||||||
"MessageFetching": "S'està recuperant...",
|
"MessageFetching": "S'està recuperant...",
|
||||||
"MessageImportantNotice": "Avís important",
|
"MessageImportantNotice": "Avís important",
|
||||||
|
"MessageInsertChapterBelow": "Insereix un capítol a sota",
|
||||||
|
"MessageInvalidAsin": "L'ASIN no és vàlid",
|
||||||
"MessageItemsSelected": "{0} elements seleccionats",
|
"MessageItemsSelected": "{0} elements seleccionats",
|
||||||
"MessageItemsUpdated": "{0} elements actualitzats",
|
"MessageItemsUpdated": "{0} elements actualitzats",
|
||||||
|
"MessageJoinUsOn": "Uniu-vos a nosaltres a",
|
||||||
"MessageLoading": "S'està carregant...",
|
"MessageLoading": "S'està carregant...",
|
||||||
"MessageLoadingFolders": "S'estan carregant les carpetes...",
|
"MessageLoadingFolders": "S'estan carregant les carpetes...",
|
||||||
|
"MessageMarkAllEpisodesFinished": "Marca tots els episodis com a acabats",
|
||||||
|
"MessageMarkAllEpisodesNotFinished": "Marca tots els episodis com a inacabats",
|
||||||
"MessageMarkAsFinished": "Marcar com acabat",
|
"MessageMarkAsFinished": "Marcar com acabat",
|
||||||
"MessageMarkAsNotFinished": "Marcar com no acabat",
|
"MessageMarkAsNotFinished": "Marcar com no acabat",
|
||||||
"MessageMatchBooksDescription": "S'intentarà fer coincidir els llibres de la biblioteca amb un llibre del proveïdor de cerca seleccionat, i s'ompliran els detalls buits i la portada. No sobreescriu els detalls.",
|
"MessageMatchBooksDescription": "S'intentarà fer coincidir els llibres de la biblioteca amb un llibre del proveïdor de cerca seleccionat, i s'ompliran els detalls buits i la portada. No sobreescriu els detalls.",
|
||||||
@@ -777,38 +799,40 @@
|
|||||||
"MessagePauseChapter": "Pausar la reproducció del capítol",
|
"MessagePauseChapter": "Pausar la reproducció del capítol",
|
||||||
"MessagePlayChapter": "Escoltar l'inici del capítol",
|
"MessagePlayChapter": "Escoltar l'inici del capítol",
|
||||||
"MessagePlaylistCreateFromCollection": "Crear una llista de reproducció a partir d'una col·lecció",
|
"MessagePlaylistCreateFromCollection": "Crear una llista de reproducció a partir d'una col·lecció",
|
||||||
"MessagePleaseWait": "Espera si us plau...",
|
"MessagePleaseWait": "Espereu...",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "El podcast no té una URL de font RSS que es pugui utilitzar",
|
"MessagePodcastHasNoRSSFeedForMatching": "El pòdcast no té un URL de canal RSS que es pugui utilitzar",
|
||||||
"MessagePodcastSearchField": "Introdueix el terme de cerca o la URL de la font RSS",
|
"MessagePodcastSearchField": "Introduïu el terme de cerca o l'URL del canal RSS",
|
||||||
"MessageQuickEmbedInProgress": "Integració ràpida en procés",
|
"MessageQuickEmbedInProgress": "Integració ràpida en procés",
|
||||||
"MessageQuickEmbedQueue": "En cua per a inserció ràpida ({0} en cua)",
|
"MessageQuickEmbedQueue": "En cua per a inserció ràpida ({0} en cua)",
|
||||||
"MessageQuickMatchAllEpisodes": "Combina ràpidament tots els episodis",
|
"MessageQuickMatchAllEpisodes": "Combina ràpidament tots els episodis",
|
||||||
"MessageQuickMatchDescription": "Omple detalls d'elements buits i portades amb els primers resultats de '{0}'. No sobreescriu els detalls tret que l'opció \"Preferir metadades trobades\" del servidor estigui habilitada.",
|
"MessageQuickMatchDescription": "Emplena els detalls i la coberta dels elements buits amb el resultat de la primera coincidència de «{0}». No sobreescriu els detalls tret que s'activi el paràmetre del servidor «Prefereix metadades coincidents».",
|
||||||
"MessageRemoveChapter": "Eliminar capítols",
|
"MessageRemoveChapter": "Elimina el capítol",
|
||||||
"MessageRemoveEpisodes": "Eliminar {0} episodi(s)",
|
"MessageRemoveEpisodes": "Elimina {0} episodi(s)",
|
||||||
"MessageRemoveFromPlayerQueue": "Eliminar de la cua del reproductor",
|
"MessageRemoveFromPlayerQueue": "Elimina de la cua del reproductor",
|
||||||
"MessageRemoveUserWarning": "Estàs segur que vols eliminar l'usuari \"{0}\"?",
|
"MessageRemoveUserWarning": "Segur que voleu suprimir permanentment l'usuari «{0}»?",
|
||||||
"MessageReportBugsAndContribute": "Informa d'errors, sol·licita funcions i contribueix a",
|
"MessageReportBugsAndContribute": "Informa d'errors, sol·licita funcions i contribueix a",
|
||||||
"MessageResetChaptersConfirm": "Estàs segur que vols desfer els canvis i revertir els capítols al seu estat original?",
|
"MessageResetChaptersConfirm": "Segur que voleu desfer els canvis i revertir els capítols al seu estat original?",
|
||||||
"MessageRestoreBackupConfirm": "Estàs segur que vols restaurar la còpia de seguretat creada a",
|
"MessageRestoreBackupConfirm": "Segur que voleu restaurar la còpia de seguretat creada a",
|
||||||
"MessageRestoreBackupWarning": "Restaurar sobreescriurà tota la base de dades situada a /config i les imatges de portades a /metadata/items i /metadata/authors.<br /><br />La còpia de seguretat no modifica cap fitxer a les carpetes de la teva biblioteca. Si has activat l'opció del servidor per guardar portades i metadades a les carpetes de la biblioteca, aquests fitxers no es guarden ni sobreescriuen.<br /><br />Tots els clients que utilitzin el teu servidor s'actualitzaran automàticament.",
|
"MessageRestoreBackupWarning": "Restaurar sobreescriurà tota la base de dades situada a /config i les imatges de portades a /metadata/items i /metadata/authors.<br /><br />La còpia de seguretat no modifica cap fitxer a les carpetes de la teva biblioteca. Si has activat l'opció del servidor per guardar portades i metadades a les carpetes de la biblioteca, aquests fitxers no es guarden ni sobreescriuen.<br /><br />Tots els clients que utilitzin el teu servidor s'actualitzaran automàticament.",
|
||||||
|
"MessageScheduleRunEveryWeekdayAtTime": "Executa cada {0} a les {1}",
|
||||||
"MessageSearchResultsFor": "Resultats de la cerca de",
|
"MessageSearchResultsFor": "Resultats de la cerca de",
|
||||||
"MessageSelected": "{0} seleccionat(s)",
|
"MessageSelected": "{0} seleccionat(s)",
|
||||||
|
"MessageSeriesSequenceCannotContainSpaces": "La seqüència de la sèrie no pot contenir espais",
|
||||||
"MessageServerCouldNotBeReached": "No es va poder establir la connexió amb el servidor",
|
"MessageServerCouldNotBeReached": "No es va poder establir la connexió amb el servidor",
|
||||||
"MessageSetChaptersFromTracksDescription": "Establir capítols utilitzant cada fitxer d'àudio com un capítol i el títol del capítol com el nom del fitxer d'àudio",
|
"MessageSetChaptersFromTracksDescription": "Establir capítols utilitzant cada fitxer d'àudio com un capítol i el títol del capítol com el nom del fitxer d'àudio",
|
||||||
"MessageShareExpirationWillBe": "La caducitat serà <strong>{0}</strong>",
|
"MessageShareExpirationWillBe": "La caducitat serà <strong>{0}</strong>",
|
||||||
"MessageShareExpiresIn": "Caduca en {0}",
|
"MessageShareExpiresIn": "Caduca en {0}",
|
||||||
"MessageShareURLWillBe": "La URL per compartir serà <strong>{0}</strong>",
|
"MessageShareURLWillBe": "La URL per compartir serà <strong>{0}</strong>",
|
||||||
"MessageStartPlaybackAtTime": "Començar la reproducció per a \"{0}\" a {1}?",
|
"MessageStartPlaybackAtTime": "Voleu començar la reproducció per a «{0}» a {1}?",
|
||||||
"MessageTaskAudioFileNotWritable": "El fitxer d'àudio \"{0}\" no es pot escriure",
|
"MessageTaskAudioFileNotWritable": "El fitxer d'àudio «{0}» no es pot escriure",
|
||||||
"MessageTaskCanceledByUser": "Tasca cancel·lada per l'usuari",
|
"MessageTaskCanceledByUser": "Tasca cancel·lada per l'usuari",
|
||||||
"MessageTaskDownloadingEpisodeDescription": "Descarregant l'episodi \"{0}\"",
|
"MessageTaskDownloadingEpisodeDescription": "S'està baixant l'episodi «{0}»",
|
||||||
"MessageTaskEmbeddingMetadata": "Inserint metadades",
|
"MessageTaskEmbeddingMetadata": "Inserint metadades",
|
||||||
"MessageTaskEmbeddingMetadataDescription": "Inserint metadades en l'audiollibre \"{0}\"",
|
"MessageTaskEmbeddingMetadataDescription": "Inserint metadades en l'audiollibre \"{0}\"",
|
||||||
"MessageTaskEncodingM4b": "Codificant M4B",
|
"MessageTaskEncodingM4b": "Codificant M4B",
|
||||||
"MessageTaskEncodingM4bDescription": "Codificant l'audiollibre \"{0}\" en un únic fitxer M4B",
|
"MessageTaskEncodingM4bDescription": "S'està codificant l'audiollibre «{0}» en un únic fitxer M4B",
|
||||||
"MessageTaskFailed": "Fallada",
|
"MessageTaskFailed": "Fallada",
|
||||||
"MessageTaskFailedToBackupAudioFile": "Error en fer una còpia de seguretat del fitxer d'àudio \"{0}\"",
|
"MessageTaskFailedToBackupAudioFile": "No s'ha pogut fer una còpia de seguretat del fitxer d'àudio «{0}»",
|
||||||
"MessageTaskFailedToCreateCacheDirectory": "Error en crear el directori de la memòria cau",
|
"MessageTaskFailedToCreateCacheDirectory": "Error en crear el directori de la memòria cau",
|
||||||
"MessageTaskFailedToEmbedMetadataInFile": "Error en incrustar metadades en el fitxer \"{0}\"",
|
"MessageTaskFailedToEmbedMetadataInFile": "Error en incrustar metadades en el fitxer \"{0}\"",
|
||||||
"MessageTaskFailedToMergeAudioFiles": "Error en fusionar fitxers d'àudio",
|
"MessageTaskFailedToMergeAudioFiles": "Error en fusionar fitxers d'àudio",
|
||||||
@@ -817,14 +841,14 @@
|
|||||||
"MessageTaskMatchingBooksInLibrary": "Coincidint llibres a la biblioteca \"{0}\"",
|
"MessageTaskMatchingBooksInLibrary": "Coincidint llibres a la biblioteca \"{0}\"",
|
||||||
"MessageTaskNoFilesToScan": "Sense fitxers per escanejar",
|
"MessageTaskNoFilesToScan": "Sense fitxers per escanejar",
|
||||||
"MessageTaskOpmlImport": "Importar OPML",
|
"MessageTaskOpmlImport": "Importar OPML",
|
||||||
"MessageTaskOpmlImportDescription": "Creant podcasts a partir de {0} fonts RSS",
|
"MessageTaskOpmlImportDescription": "S'estan creant pòdcasts a partir de {0} canals RSS",
|
||||||
"MessageTaskOpmlImportFeed": "Importació de feed OPML",
|
"MessageTaskOpmlImportFeed": "Importació d'un canal OPML",
|
||||||
"MessageTaskOpmlImportFeedDescription": "Importació del feed RSS \"{0}\"",
|
"MessageTaskOpmlImportFeedDescription": "S'està important el canal RSS «{0}»",
|
||||||
"MessageTaskOpmlImportFeedFailed": "No es pot obtenir el podcast",
|
"MessageTaskOpmlImportFeedFailed": "No s'ha pogut obtenir el canal del pòdcast",
|
||||||
"MessageTaskOpmlImportFeedPodcastDescription": "Creant el podcast \"{0}\"",
|
"MessageTaskOpmlImportFeedPodcastDescription": "S'està creant el pòdcast «{0}»",
|
||||||
"MessageTaskOpmlImportFeedPodcastExists": "El podcast ja existeix a la ruta",
|
"MessageTaskOpmlImportFeedPodcastExists": "El pòdcast ja existeix al camí",
|
||||||
"MessageTaskOpmlImportFeedPodcastFailed": "Error en crear el podcast",
|
"MessageTaskOpmlImportFeedPodcastFailed": "No s'ha pogut crear el pòdcast",
|
||||||
"MessageTaskOpmlImportFinished": "Afegit {0} podcasts",
|
"MessageTaskOpmlImportFinished": "S'han afegit {0} pòdcasts",
|
||||||
"MessageTaskOpmlParseFailed": "No s'ha pogut analitzar el fitxer OPML",
|
"MessageTaskOpmlParseFailed": "No s'ha pogut analitzar el fitxer OPML",
|
||||||
"MessageTaskOpmlParseFastFail": "No s'ha trobat l'etiqueta <opml> o <outline> al fitxer OPML",
|
"MessageTaskOpmlParseFastFail": "No s'ha trobat l'etiqueta <opml> o <outline> al fitxer OPML",
|
||||||
"MessageTaskOpmlParseNoneFound": "No s'han trobat fonts al fitxer OPML",
|
"MessageTaskOpmlParseNoneFound": "No s'han trobat fonts al fitxer OPML",
|
||||||
@@ -842,13 +866,13 @@
|
|||||||
"MessageValidCronExpression": "Expressió de cron vàlida",
|
"MessageValidCronExpression": "Expressió de cron vàlida",
|
||||||
"MessageWatcherIsDisabledGlobally": "El watcher està desactivat globalment a la configuració del servidor",
|
"MessageWatcherIsDisabledGlobally": "El watcher està desactivat globalment a la configuració del servidor",
|
||||||
"MessageXLibraryIsEmpty": "La biblioteca {0} està buida!",
|
"MessageXLibraryIsEmpty": "La biblioteca {0} està buida!",
|
||||||
"MessageYourAudiobookDurationIsLonger": "La durada del teu audiollibre és més llarga que la durada trobada",
|
"MessageYourAudiobookDurationIsLonger": "La durada del vostre audiollibre és major que la durada trobada",
|
||||||
"MessageYourAudiobookDurationIsShorter": "La durada del teu audiollibre és més curta que la durada trobada",
|
"MessageYourAudiobookDurationIsShorter": "La durada del vostre audiollibre és menor que la durada trobada",
|
||||||
"NoteChangeRootPassword": "L'usuari Root és l'únic usuari que pot no tenir una contrasenya",
|
"NoteChangeRootPassword": "L'usuari Root és l'únic usuari que pot no tenir una contrasenya",
|
||||||
"NoteChapterEditorTimes": "Nota: El temps d'inici del primer capítol ha de romandre a 0:00, i el temps d'inici de l'últim capítol no pot superar la durada de l'audiollibre.",
|
"NoteChapterEditorTimes": "Nota: el temps d'inici del primer capítol ha de romandre a 0:00, i el temps d'inici de l'últim capítol no pot superar la durada de l'audiollibre.",
|
||||||
"NoteFolderPicker": "Nota: Les carpetes ja assignades no es mostraran",
|
"NoteFolderPicker": "Nota: les carpetes ja assignades no es mostraran",
|
||||||
"NoteRSSFeedPodcastAppsHttps": "Advertència: La majoria d'aplicacions de podcast requereixen que la URL de la font RSS utilitzi HTTPS",
|
"NoteRSSFeedPodcastAppsHttps": "Avís: la majoria d'aplicacions de pòdcast requereixen que l'URL del canal RSS utilitzi HTTPS",
|
||||||
"NoteRSSFeedPodcastAppsPubDate": "Advertència: Un o més dels teus episodis no tenen data de publicació. Algunes aplicacions de podcast ho requereixen.",
|
"NoteRSSFeedPodcastAppsPubDate": "Avís: un o més dels vostres episodis no tenen data de publicació. Algunes aplicacions de pòdcast ho requereixen.",
|
||||||
"NoteUploaderFoldersWithMediaFiles": "Les carpetes amb fitxers multimèdia es gestionaran com a elements separats a la biblioteca.",
|
"NoteUploaderFoldersWithMediaFiles": "Les carpetes amb fitxers multimèdia es gestionaran com a elements separats a la biblioteca.",
|
||||||
"NoteUploaderOnlyAudioFiles": "Si només puges fitxers d'àudio, cada fitxer es gestionarà com un audiollibre separat.",
|
"NoteUploaderOnlyAudioFiles": "Si només puges fitxers d'àudio, cada fitxer es gestionarà com un audiollibre separat.",
|
||||||
"NoteUploaderUnsupportedFiles": "Els fitxers no compatibles seran ignorats. Si selecciona o arrossega una carpeta, els fitxers que no estiguin dins d'una subcarpeta seran ignorats.",
|
"NoteUploaderUnsupportedFiles": "Els fitxers no compatibles seran ignorats. Si selecciona o arrossega una carpeta, els fitxers que no estiguin dins d'una subcarpeta seran ignorats.",
|
||||||
@@ -857,7 +881,7 @@
|
|||||||
"NotificationOnEpisodeDownloadedDescription": "S'activa quan es descarrega automàticament un episodi d'un podcast",
|
"NotificationOnEpisodeDownloadedDescription": "S'activa quan es descarrega automàticament un episodi d'un podcast",
|
||||||
"NotificationOnTestDescription": "Esdeveniment per provar el sistema de notificacions",
|
"NotificationOnTestDescription": "Esdeveniment per provar el sistema de notificacions",
|
||||||
"PlaceholderNewCollection": "Nou nom de la col·lecció",
|
"PlaceholderNewCollection": "Nou nom de la col·lecció",
|
||||||
"PlaceholderNewFolderPath": "Nova ruta de carpeta",
|
"PlaceholderNewFolderPath": "Camí de carpeta nou",
|
||||||
"PlaceholderNewPlaylist": "Nou nom de la llista de reproducció",
|
"PlaceholderNewPlaylist": "Nou nom de la llista de reproducció",
|
||||||
"PlaceholderSearch": "Cerca...",
|
"PlaceholderSearch": "Cerca...",
|
||||||
"PlaceholderSearchEpisode": "Cerca d'episodis...",
|
"PlaceholderSearchEpisode": "Cerca d'episodis...",
|
||||||
@@ -883,7 +907,7 @@
|
|||||||
"ToastAppriseUrlRequired": "Cal introduir una URL de Apprise",
|
"ToastAppriseUrlRequired": "Cal introduir una URL de Apprise",
|
||||||
"ToastAsinRequired": "ASIN requerit",
|
"ToastAsinRequired": "ASIN requerit",
|
||||||
"ToastAuthorImageRemoveSuccess": "S'ha eliminat la imatge de l'autor",
|
"ToastAuthorImageRemoveSuccess": "S'ha eliminat la imatge de l'autor",
|
||||||
"ToastAuthorNotFound": "No s'ha trobat l'autor \"{0}\"",
|
"ToastAuthorNotFound": "No s'ha trobat l'autor «{0}»",
|
||||||
"ToastAuthorRemoveSuccess": "Autor eliminat",
|
"ToastAuthorRemoveSuccess": "Autor eliminat",
|
||||||
"ToastAuthorSearchNotFound": "No s'ha trobat l'autor",
|
"ToastAuthorSearchNotFound": "No s'ha trobat l'autor",
|
||||||
"ToastAuthorUpdateMerged": "Autor combinat",
|
"ToastAuthorUpdateMerged": "Autor combinat",
|
||||||
@@ -899,6 +923,7 @@
|
|||||||
"ToastBackupRestoreFailed": "Error en restaurar la còpia de seguretat",
|
"ToastBackupRestoreFailed": "Error en restaurar la còpia de seguretat",
|
||||||
"ToastBackupUploadFailed": "Error en carregar la còpia de seguretat",
|
"ToastBackupUploadFailed": "Error en carregar la còpia de seguretat",
|
||||||
"ToastBackupUploadSuccess": "Còpia de seguretat carregada",
|
"ToastBackupUploadSuccess": "Còpia de seguretat carregada",
|
||||||
|
"ToastBatchApplyDetailsToItemsSuccess": "S'han aplicat els detalls als elements",
|
||||||
"ToastBatchDeleteFailed": "Error en l'eliminació per lots",
|
"ToastBatchDeleteFailed": "Error en l'eliminació per lots",
|
||||||
"ToastBatchDeleteSuccess": "Eliminació per lots correcte",
|
"ToastBatchDeleteSuccess": "Eliminació per lots correcte",
|
||||||
"ToastBatchQuickMatchFailed": "Error en la sincronització ràpida per lots!",
|
"ToastBatchQuickMatchFailed": "Error en la sincronització ràpida per lots!",
|
||||||
@@ -911,6 +936,8 @@
|
|||||||
"ToastCachePurgeFailed": "Error en purgar la memòria cau",
|
"ToastCachePurgeFailed": "Error en purgar la memòria cau",
|
||||||
"ToastCachePurgeSuccess": "Memòria cau purgada amb èxit",
|
"ToastCachePurgeSuccess": "Memòria cau purgada amb èxit",
|
||||||
"ToastChaptersHaveErrors": "Els capítols tenen errors",
|
"ToastChaptersHaveErrors": "Els capítols tenen errors",
|
||||||
|
"ToastChaptersInvalidShiftAmountLast": "La quantitat de desplaçament no és vàlida. L'hora d'inici de l'últim capítol s'estendria més enllà de la durada d'aquest audiollibre.",
|
||||||
|
"ToastChaptersInvalidShiftAmountStart": "La quantitat de desplaçament no és vàlida. El primer capítol tindria una durada zero o negativa i el sobreescriuria el segon capítol. Augmenteu la durada inicial del segon capítol.",
|
||||||
"ToastChaptersMustHaveTitles": "Els capítols han de tenir un títol",
|
"ToastChaptersMustHaveTitles": "Els capítols han de tenir un títol",
|
||||||
"ToastChaptersRemoved": "Capítols eliminats",
|
"ToastChaptersRemoved": "Capítols eliminats",
|
||||||
"ToastChaptersUpdated": "Capítols actualitzats",
|
"ToastChaptersUpdated": "Capítols actualitzats",
|
||||||
@@ -918,6 +945,7 @@
|
|||||||
"ToastCollectionRemoveSuccess": "Col·lecció eliminada",
|
"ToastCollectionRemoveSuccess": "Col·lecció eliminada",
|
||||||
"ToastCollectionUpdateSuccess": "Col·lecció actualitzada",
|
"ToastCollectionUpdateSuccess": "Col·lecció actualitzada",
|
||||||
"ToastCoverUpdateFailed": "Error en actualitzar la portada",
|
"ToastCoverUpdateFailed": "Error en actualitzar la portada",
|
||||||
|
"ToastDateTimeInvalidOrIncomplete": "La data i hora no és vàlida o està incompleta",
|
||||||
"ToastDeleteFileFailed": "No s'ha pogut suprimir el fitxer",
|
"ToastDeleteFileFailed": "No s'ha pogut suprimir el fitxer",
|
||||||
"ToastDeleteFileSuccess": "Fitxer suprimit",
|
"ToastDeleteFileSuccess": "Fitxer suprimit",
|
||||||
"ToastDeviceAddFailed": "Error en afegir el dispositiu",
|
"ToastDeviceAddFailed": "Error en afegir el dispositiu",
|
||||||
@@ -948,34 +976,35 @@
|
|||||||
"ToastItemMarkedAsNotFinishedSuccess": "Element marcat com a no acabat",
|
"ToastItemMarkedAsNotFinishedSuccess": "Element marcat com a no acabat",
|
||||||
"ToastItemUpdateSuccess": "Element actualitzat",
|
"ToastItemUpdateSuccess": "Element actualitzat",
|
||||||
"ToastLibraryCreateFailed": "Error en crear la biblioteca",
|
"ToastLibraryCreateFailed": "Error en crear la biblioteca",
|
||||||
"ToastLibraryCreateSuccess": "Biblioteca \"{0}\" creada",
|
"ToastLibraryCreateSuccess": "S'ha creat la biblioteca «{0}»",
|
||||||
"ToastLibraryDeleteFailed": "Error en eliminar la biblioteca",
|
"ToastLibraryDeleteFailed": "Error en eliminar la biblioteca",
|
||||||
"ToastLibraryDeleteSuccess": "Biblioteca eliminada",
|
"ToastLibraryDeleteSuccess": "Biblioteca eliminada",
|
||||||
"ToastLibraryScanFailedToStart": "Error en iniciar l'escaneig",
|
"ToastLibraryScanFailedToStart": "Error en iniciar l'escaneig",
|
||||||
"ToastLibraryScanStarted": "S'ha iniciat l'escaneig de la biblioteca",
|
"ToastLibraryScanStarted": "S'ha iniciat l'escaneig de la biblioteca",
|
||||||
"ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" actualitzada",
|
"ToastLibraryUpdateSuccess": "S'ha actualitzat la biblioteca «{0}»",
|
||||||
"ToastMatchAllAuthorsFailed": "No coincideix amb tots els autors",
|
"ToastMatchAllAuthorsFailed": "No coincideix amb tots els autors",
|
||||||
"ToastMetadataFilesRemovedError": "Error en eliminar metadades de {0} arxius",
|
"ToastMetadataFilesRemovedError": "S’ha produït un error en eliminar els fitxers metadata.{0}",
|
||||||
"ToastMetadataFilesRemovedNoneFound": "No s'han trobat metadades en {0} arxius",
|
"ToastMetadataFilesRemovedNoneFound": "No hi ha cap fitxer metadata.{0} a la biblioteca",
|
||||||
"ToastMetadataFilesRemovedNoneRemoved": "Cap metadada eliminada en {0} arxius",
|
"ToastMetadataFilesRemovedNoneRemoved": "No s'ha eliminat cap fitxer metadata.{0}",
|
||||||
"ToastMetadataFilesRemovedSuccess": "{0} metadades eliminades en {1} arxius",
|
"ToastMetadataFilesRemovedSuccess": "{0} metadades eliminades en {1} arxius",
|
||||||
"ToastMustHaveAtLeastOnePath": "Ha de tenir almenys una ruta",
|
"ToastMustHaveAtLeastOnePath": "Ha de tenir almenys una ruta",
|
||||||
"ToastNameEmailRequired": "El nom i el correu electrònic són obligatoris",
|
"ToastNameEmailRequired": "El nom i el correu electrònic són obligatoris",
|
||||||
"ToastNameRequired": "Nom obligatori",
|
"ToastNameRequired": "Nom obligatori",
|
||||||
"ToastNewEpisodesFound": "{0} episodi(s) nou(s) trobat(s)",
|
"ToastNewEpisodesFound": "{0} episodi(s) nou(s) trobat(s)",
|
||||||
"ToastNewUserCreatedFailed": "Error en crear el compte: \"{0}\"",
|
"ToastNewUserCreatedFailed": "No s'ha pogut crear el compte: «{0}»",
|
||||||
"ToastNewUserCreatedSuccess": "Nou compte creat",
|
"ToastNewUserCreatedSuccess": "Nou compte creat",
|
||||||
"ToastNewUserLibraryError": "Ha de seleccionar almenys una biblioteca",
|
"ToastNewUserLibraryError": "S'ha de seleccionar almenys una biblioteca",
|
||||||
"ToastNewUserPasswordError": "Necessites una contrasenya, només el root pot estar sense contrasenya",
|
"ToastNewUserPasswordError": "Cal una contrasenya; només l'usuari primari pot estar sense contrasenya",
|
||||||
"ToastNewUserTagError": "Selecciona almenys una etiqueta",
|
"ToastNewUserTagError": "S'ha de seleccionar almenys una etiqueta",
|
||||||
"ToastNewUserUsernameError": "Introdueix un nom d'usuari",
|
"ToastNewUserUsernameError": "Introduïu un nom d'usuari",
|
||||||
"ToastNoNewEpisodesFound": "No s'han trobat nous episodis",
|
"ToastNoNewEpisodesFound": "No s'han trobat nous episodis",
|
||||||
|
"ToastNoRSSFeed": "El pòdcast no té canal RSS",
|
||||||
"ToastNoUpdatesNecessary": "No cal actualitzar",
|
"ToastNoUpdatesNecessary": "No cal actualitzar",
|
||||||
"ToastNotificationCreateFailed": "Error en crear la notificació",
|
"ToastNotificationCreateFailed": "No s'ha pogut crear la notificació",
|
||||||
"ToastNotificationDeleteFailed": "Error en eliminar la notificació",
|
"ToastNotificationDeleteFailed": "No s'ha pogut suprimir la notificació",
|
||||||
"ToastNotificationFailedMaximum": "El nombre màxim d'intents fallits ha de ser ≥ 0",
|
"ToastNotificationFailedMaximum": "El nombre màxim d'intents fallits ha de ser ≥ 0",
|
||||||
"ToastNotificationQueueMaximum": "La cua de notificació màxima ha de ser ≥ 0",
|
"ToastNotificationQueueMaximum": "La cua de notificació màxima ha de ser ≥ 0",
|
||||||
"ToastNotificationSettingsUpdateSuccess": "Configuració de notificació actualitzada",
|
"ToastNotificationSettingsUpdateSuccess": "S'han actualitzat els paràmetres de notificacions",
|
||||||
"ToastNotificationTestTriggerFailed": "No s'ha pogut activar la notificació de prova",
|
"ToastNotificationTestTriggerFailed": "No s'ha pogut activar la notificació de prova",
|
||||||
"ToastNotificationTestTriggerSuccess": "Notificació de prova activada",
|
"ToastNotificationTestTriggerSuccess": "Notificació de prova activada",
|
||||||
"ToastNotificationUpdateSuccess": "Notificació actualitzada",
|
"ToastNotificationUpdateSuccess": "Notificació actualitzada",
|
||||||
@@ -985,16 +1014,16 @@
|
|||||||
"ToastPlaylistUpdateSuccess": "Llista de reproducció actualitzada",
|
"ToastPlaylistUpdateSuccess": "Llista de reproducció actualitzada",
|
||||||
"ToastPodcastCreateFailed": "No s'ha pogut crear el pòdcast",
|
"ToastPodcastCreateFailed": "No s'ha pogut crear el pòdcast",
|
||||||
"ToastPodcastCreateSuccess": "S'ha creat el pòdcast correctament",
|
"ToastPodcastCreateSuccess": "S'ha creat el pòdcast correctament",
|
||||||
"ToastPodcastGetFeedFailed": "No s'ha pogut obtenir el podcast",
|
"ToastPodcastGetFeedFailed": "No s'ha pogut obtenir el canal del pòdcast",
|
||||||
"ToastPodcastNoEpisodesInFeed": "No s'han trobat episodis en el feed RSS",
|
"ToastPodcastNoEpisodesInFeed": "No s'ha trobat cap episodi al canal RSS",
|
||||||
"ToastPodcastNoRssFeed": "El podcast no té un feed RSS",
|
"ToastPodcastNoRssFeed": "El pòdcast no té un canal RSS",
|
||||||
"ToastProgressIsNotBeingSynced": "El progrés no s'està sincronitzant, reinicia la reproducció",
|
"ToastProgressIsNotBeingSynced": "El progrés no s'està sincronitzant, reinicia la reproducció",
|
||||||
"ToastProviderCreatedFailed": "Error en afegir el proveïdor",
|
"ToastProviderCreatedFailed": "Error en afegir el proveïdor",
|
||||||
"ToastProviderCreatedSuccess": "Nou proveïdor afegit",
|
"ToastProviderCreatedSuccess": "Nou proveïdor afegit",
|
||||||
"ToastProviderNameAndUrlRequired": "Nom i URL obligatoris",
|
"ToastProviderNameAndUrlRequired": "Nom i URL obligatoris",
|
||||||
"ToastProviderRemoveSuccess": "Proveïdor eliminat",
|
"ToastProviderRemoveSuccess": "Proveïdor eliminat",
|
||||||
"ToastRSSFeedCloseFailed": "Error en tancar el feed RSS",
|
"ToastRSSFeedCloseFailed": "No s'ha pogut tancar el canal RSS",
|
||||||
"ToastRSSFeedCloseSuccess": "Feed RSS tancat",
|
"ToastRSSFeedCloseSuccess": "Canal RSS tancat",
|
||||||
"ToastRemoveFailed": "Error en eliminar",
|
"ToastRemoveFailed": "Error en eliminar",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Error en eliminar l'element de la col·lecció",
|
"ToastRemoveItemFromCollectionFailed": "Error en eliminar l'element de la col·lecció",
|
||||||
"ToastRemoveItemFromCollectionSuccess": "Element eliminat de la col·lecció",
|
"ToastRemoveItemFromCollectionSuccess": "Element eliminat de la col·lecció",
|
||||||
@@ -1008,7 +1037,8 @@
|
|||||||
"ToastScanFailed": "No s'ha pogut escanejar l'element de la biblioteca",
|
"ToastScanFailed": "No s'ha pogut escanejar l'element de la biblioteca",
|
||||||
"ToastSelectAtLeastOneUser": "Selecciona almenys un usuari",
|
"ToastSelectAtLeastOneUser": "Selecciona almenys un usuari",
|
||||||
"ToastSendEbookToDeviceFailed": "Error en enviar l'ebook al dispositiu",
|
"ToastSendEbookToDeviceFailed": "Error en enviar l'ebook al dispositiu",
|
||||||
"ToastSendEbookToDeviceSuccess": "Ebook enviat al dispositiu \"{0}\"",
|
"ToastSendEbookToDeviceSuccess": "El llibre electrònic s'ha enviat al dispositiu «{0}»",
|
||||||
|
"ToastSeriesSubmitFailedSameName": "No és possible afegir dues sèries amb el mateix nom",
|
||||||
"ToastSeriesUpdateFailed": "Error en actualitzar la sèrie",
|
"ToastSeriesUpdateFailed": "Error en actualitzar la sèrie",
|
||||||
"ToastSeriesUpdateSuccess": "Sèrie actualitzada",
|
"ToastSeriesUpdateSuccess": "Sèrie actualitzada",
|
||||||
"ToastServerSettingsUpdateSuccess": "Configuració del servidor actualitzada",
|
"ToastServerSettingsUpdateSuccess": "Configuració del servidor actualitzada",
|
||||||
|
|||||||
+68
-13
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"ButtonAdd": "Přidat",
|
"ButtonAdd": "Přidat",
|
||||||
|
"ButtonAddApiKey": "Přidat API klíč",
|
||||||
"ButtonAddChapters": "Přidat kapitoly",
|
"ButtonAddChapters": "Přidat kapitoly",
|
||||||
"ButtonAddDevice": "Přidat zařízení",
|
"ButtonAddDevice": "Přidat zařízení",
|
||||||
"ButtonAddLibrary": "Přidat knihovnu",
|
"ButtonAddLibrary": "Přidat knihovnu",
|
||||||
@@ -10,6 +11,8 @@
|
|||||||
"ButtonApplyChapters": "Aplikovat kapitoly",
|
"ButtonApplyChapters": "Aplikovat kapitoly",
|
||||||
"ButtonAuthors": "Autoři",
|
"ButtonAuthors": "Autoři",
|
||||||
"ButtonBack": "Zpět",
|
"ButtonBack": "Zpět",
|
||||||
|
"ButtonBatchEditPopulateFromExisting": "Vytvořit z existujících",
|
||||||
|
"ButtonBatchEditPopulateMapDetails": "Předvyplnit podrobnosti mapování",
|
||||||
"ButtonBrowseForFolder": "Vyhledat složku",
|
"ButtonBrowseForFolder": "Vyhledat složku",
|
||||||
"ButtonCancel": "Zrušit",
|
"ButtonCancel": "Zrušit",
|
||||||
"ButtonCancelEncode": "Zrušit kódování",
|
"ButtonCancelEncode": "Zrušit kódování",
|
||||||
@@ -18,6 +21,7 @@
|
|||||||
"ButtonChooseAFolder": "Vybrat složku",
|
"ButtonChooseAFolder": "Vybrat složku",
|
||||||
"ButtonChooseFiles": "Vybrat soubory",
|
"ButtonChooseFiles": "Vybrat soubory",
|
||||||
"ButtonClearFilter": "Vymazat filtr",
|
"ButtonClearFilter": "Vymazat filtr",
|
||||||
|
"ButtonClose": "Zavřít",
|
||||||
"ButtonCloseFeed": "Zavřít kanál",
|
"ButtonCloseFeed": "Zavřít kanál",
|
||||||
"ButtonCloseSession": "Zavřít otevřenou relaci",
|
"ButtonCloseSession": "Zavřít otevřenou relaci",
|
||||||
"ButtonCollections": "Kolekce",
|
"ButtonCollections": "Kolekce",
|
||||||
@@ -117,6 +121,7 @@
|
|||||||
"HeaderAccount": "Účet",
|
"HeaderAccount": "Účet",
|
||||||
"HeaderAddCustomMetadataProvider": "Přidat vlastního poskytovatele metadat",
|
"HeaderAddCustomMetadataProvider": "Přidat vlastního poskytovatele metadat",
|
||||||
"HeaderAdvanced": "Pokročilé",
|
"HeaderAdvanced": "Pokročilé",
|
||||||
|
"HeaderApiKeys": "API klíče",
|
||||||
"HeaderAppriseNotificationSettings": "Nastavení oznámení Apprise",
|
"HeaderAppriseNotificationSettings": "Nastavení oznámení Apprise",
|
||||||
"HeaderAudioTracks": "Zvukové stopy",
|
"HeaderAudioTracks": "Zvukové stopy",
|
||||||
"HeaderAudiobookTools": "Nástroje pro správu souborů audioknih",
|
"HeaderAudiobookTools": "Nástroje pro správu souborů audioknih",
|
||||||
@@ -145,14 +150,14 @@
|
|||||||
"HeaderItemFiles": "Soubory položek",
|
"HeaderItemFiles": "Soubory položek",
|
||||||
"HeaderItemMetadataUtils": "Nástroje metadat položek",
|
"HeaderItemMetadataUtils": "Nástroje metadat položek",
|
||||||
"HeaderLastListeningSession": "Poslední poslechová relace",
|
"HeaderLastListeningSession": "Poslední poslechová relace",
|
||||||
"HeaderLatestEpisodes": "Nejnovější epizody",
|
"HeaderLatestEpisodes": "Nové epizody",
|
||||||
"HeaderLibraries": "Knihovny",
|
"HeaderLibraries": "Knihovny",
|
||||||
"HeaderLibraryFiles": "Soubory knihovny",
|
"HeaderLibraryFiles": "Soubory knihovny",
|
||||||
"HeaderLibraryStats": "Statistiky knihovny",
|
"HeaderLibraryStats": "Statistiky knihovny",
|
||||||
"HeaderListeningSessions": "Poslechové relace",
|
"HeaderListeningSessions": "Poslechové relace",
|
||||||
"HeaderListeningStats": "Statistiky poslechu",
|
"HeaderListeningStats": "Statistiky poslechu",
|
||||||
"HeaderLogin": "Přihlásit",
|
"HeaderLogin": "Přihlásit",
|
||||||
"HeaderLogs": "Záznamy",
|
"HeaderLogs": "Logy",
|
||||||
"HeaderManageGenres": "Spravovat žánry",
|
"HeaderManageGenres": "Spravovat žánry",
|
||||||
"HeaderManageTags": "Spravovat štítky",
|
"HeaderManageTags": "Spravovat štítky",
|
||||||
"HeaderMapDetails": "Podrobnosti mapování",
|
"HeaderMapDetails": "Podrobnosti mapování",
|
||||||
@@ -160,6 +165,7 @@
|
|||||||
"HeaderMetadataOrderOfPrecedence": "Pořadí priorit metadat",
|
"HeaderMetadataOrderOfPrecedence": "Pořadí priorit metadat",
|
||||||
"HeaderMetadataToEmbed": "Metadata k vložení",
|
"HeaderMetadataToEmbed": "Metadata k vložení",
|
||||||
"HeaderNewAccount": "Nový účet",
|
"HeaderNewAccount": "Nový účet",
|
||||||
|
"HeaderNewApiKey": "Nový API klíč",
|
||||||
"HeaderNewLibrary": "Nová knihovna",
|
"HeaderNewLibrary": "Nová knihovna",
|
||||||
"HeaderNotificationCreate": "Vytvořit notifikaci",
|
"HeaderNotificationCreate": "Vytvořit notifikaci",
|
||||||
"HeaderNotificationUpdate": "Aktualizovat notifikaci",
|
"HeaderNotificationUpdate": "Aktualizovat notifikaci",
|
||||||
@@ -175,6 +181,7 @@
|
|||||||
"HeaderPlaylist": "Seznam skladeb",
|
"HeaderPlaylist": "Seznam skladeb",
|
||||||
"HeaderPlaylistItems": "Položky seznamu přehrávání",
|
"HeaderPlaylistItems": "Položky seznamu přehrávání",
|
||||||
"HeaderPodcastsToAdd": "Podcasty k přidání",
|
"HeaderPodcastsToAdd": "Podcasty k přidání",
|
||||||
|
"HeaderPresets": "Předvolba",
|
||||||
"HeaderPreviewCover": "Náhled obálky",
|
"HeaderPreviewCover": "Náhled obálky",
|
||||||
"HeaderRSSFeedGeneral": "Podrobnosti o RSS",
|
"HeaderRSSFeedGeneral": "Podrobnosti o RSS",
|
||||||
"HeaderRSSFeedIsOpen": "Informační kanál RSS je otevřený",
|
"HeaderRSSFeedIsOpen": "Informační kanál RSS je otevřený",
|
||||||
@@ -192,6 +199,7 @@
|
|||||||
"HeaderSettingsExperimental": "Experimentální funkce",
|
"HeaderSettingsExperimental": "Experimentální funkce",
|
||||||
"HeaderSettingsGeneral": "Obecné",
|
"HeaderSettingsGeneral": "Obecné",
|
||||||
"HeaderSettingsScanner": "Skener",
|
"HeaderSettingsScanner": "Skener",
|
||||||
|
"HeaderSettingsSecurity": "Zabezpečení",
|
||||||
"HeaderSettingsWebClient": "Webový klient",
|
"HeaderSettingsWebClient": "Webový klient",
|
||||||
"HeaderSleepTimer": "Časovač vypnutí",
|
"HeaderSleepTimer": "Časovač vypnutí",
|
||||||
"HeaderStatsLargestItems": "Největší položky",
|
"HeaderStatsLargestItems": "Největší položky",
|
||||||
@@ -203,6 +211,7 @@
|
|||||||
"HeaderTableOfContents": "Obsah",
|
"HeaderTableOfContents": "Obsah",
|
||||||
"HeaderTools": "Nástroje",
|
"HeaderTools": "Nástroje",
|
||||||
"HeaderUpdateAccount": "Aktualizovat účet",
|
"HeaderUpdateAccount": "Aktualizovat účet",
|
||||||
|
"HeaderUpdateApiKey": "Aktualizovat API klíč",
|
||||||
"HeaderUpdateAuthor": "Aktualizovat autora",
|
"HeaderUpdateAuthor": "Aktualizovat autora",
|
||||||
"HeaderUpdateDetails": "Aktualizovat podrobnosti",
|
"HeaderUpdateDetails": "Aktualizovat podrobnosti",
|
||||||
"HeaderUpdateLibrary": "Aktualizovat knihovnu",
|
"HeaderUpdateLibrary": "Aktualizovat knihovnu",
|
||||||
@@ -227,10 +236,15 @@
|
|||||||
"LabelAddedDate": "Přidáno {0}",
|
"LabelAddedDate": "Přidáno {0}",
|
||||||
"LabelAdminUsersOnly": "Pouze administrátoři",
|
"LabelAdminUsersOnly": "Pouze administrátoři",
|
||||||
"LabelAll": "Vše",
|
"LabelAll": "Vše",
|
||||||
|
"LabelAllEpisodesDownloaded": "Všechny epizody staženy",
|
||||||
"LabelAllUsers": "Všichni uživatelé",
|
"LabelAllUsers": "Všichni uživatelé",
|
||||||
"LabelAllUsersExcludingGuests": "Všichni uživatelé kromě hostů",
|
"LabelAllUsersExcludingGuests": "Všichni uživatelé kromě hostů",
|
||||||
"LabelAllUsersIncludingGuests": "Všichni uživatelé včetně hostů",
|
"LabelAllUsersIncludingGuests": "Všichni uživatelé včetně hostů",
|
||||||
"LabelAlreadyInYourLibrary": "Již ve vaší knihovně",
|
"LabelAlreadyInYourLibrary": "Již ve vaší knihovně",
|
||||||
|
"LabelApiKeyCreated": "API klíč \"{0}\" byl úspěšně vytvořen.",
|
||||||
|
"LabelApiKeyCreatedDescription": "Zkopírujte si API klíč nyní, později již nebude možné jej zobrazit.",
|
||||||
|
"LabelApiKeyUser": "Vydávat se za uživatele",
|
||||||
|
"LabelApiKeyUserDescription": "Tento API klíč bude mít stejná oprávnění jako uživatel za něhož vystupuje. V protokolech to bude vypadat jako kdyby požadavky vytvářel přímo daný uživatel.",
|
||||||
"LabelApiToken": "API Token",
|
"LabelApiToken": "API Token",
|
||||||
"LabelAppend": "Připojit",
|
"LabelAppend": "Připojit",
|
||||||
"LabelAudioBitrate": "Bitový tok zvuku (např. 128k)",
|
"LabelAudioBitrate": "Bitový tok zvuku (např. 128k)",
|
||||||
@@ -250,7 +264,7 @@
|
|||||||
"LabelBackToUser": "Zpět k uživateli",
|
"LabelBackToUser": "Zpět k uživateli",
|
||||||
"LabelBackupAudioFiles": "Zálohovat zvukové soubory",
|
"LabelBackupAudioFiles": "Zálohovat zvukové soubory",
|
||||||
"LabelBackupLocation": "Umístění zálohy",
|
"LabelBackupLocation": "Umístění zálohy",
|
||||||
"LabelBackupsEnableAutomaticBackups": "Povolit automatické zálohování",
|
"LabelBackupsEnableAutomaticBackups": "Automatické zálohování",
|
||||||
"LabelBackupsEnableAutomaticBackupsHelp": "Zálohy uložené v /metadata/backups",
|
"LabelBackupsEnableAutomaticBackupsHelp": "Zálohy uložené v /metadata/backups",
|
||||||
"LabelBackupsMaxBackupSize": "Maximální velikost zálohy (v GB) (0 bez omezení)",
|
"LabelBackupsMaxBackupSize": "Maximální velikost zálohy (v GB) (0 bez omezení)",
|
||||||
"LabelBackupsMaxBackupSizeHelp": "Ochrana proti chybné konfiguraci: Zálohování se nezdaří, pokud překročí nastavenou velikost.",
|
"LabelBackupsMaxBackupSizeHelp": "Ochrana proti chybné konfiguraci: Zálohování se nezdaří, pokud překročí nastavenou velikost.",
|
||||||
@@ -280,8 +294,10 @@
|
|||||||
"LabelContinueListening": "Pokračovat v poslechu",
|
"LabelContinueListening": "Pokračovat v poslechu",
|
||||||
"LabelContinueReading": "Pokračovat ve čtení",
|
"LabelContinueReading": "Pokračovat ve čtení",
|
||||||
"LabelContinueSeries": "Pokračovat v sérii",
|
"LabelContinueSeries": "Pokračovat v sérii",
|
||||||
|
"LabelCorsAllowed": "Povolené CORS Origins",
|
||||||
"LabelCover": "Obálka",
|
"LabelCover": "Obálka",
|
||||||
"LabelCoverImageURL": "URL obrázku obálky",
|
"LabelCoverImageURL": "URL obrázku obálky",
|
||||||
|
"LabelCoverProvider": "Poskytovatel obálky",
|
||||||
"LabelCreatedAt": "Vytvořeno v",
|
"LabelCreatedAt": "Vytvořeno v",
|
||||||
"LabelCronExpression": "Výraz Cronu",
|
"LabelCronExpression": "Výraz Cronu",
|
||||||
"LabelCurrent": "Aktuální",
|
"LabelCurrent": "Aktuální",
|
||||||
@@ -341,11 +357,15 @@
|
|||||||
"LabelExample": "Příklad",
|
"LabelExample": "Příklad",
|
||||||
"LabelExpandSeries": "Rozbalit série",
|
"LabelExpandSeries": "Rozbalit série",
|
||||||
"LabelExpandSubSeries": "Rozbalit podsérie",
|
"LabelExpandSubSeries": "Rozbalit podsérie",
|
||||||
"LabelExplicit": "Explicitní",
|
"LabelExpired": "Expirovaný",
|
||||||
|
"LabelExpiresAt": "Expiruje v",
|
||||||
|
"LabelExpiresInSeconds": "Expiruje za (sekundy)",
|
||||||
|
"LabelExpiresNever": "Nikdy",
|
||||||
|
"LabelExplicit": "Explicitně",
|
||||||
"LabelExplicitChecked": "Explicitní (zaškrtnuto)",
|
"LabelExplicitChecked": "Explicitní (zaškrtnuto)",
|
||||||
"LabelExplicitUnchecked": "Není explicitní (nezaškrtnuto)",
|
"LabelExplicitUnchecked": "Není explicitní (nezaškrtnuto)",
|
||||||
"LabelExportOPML": "Export OPML",
|
"LabelExportOPML": "Export OPML",
|
||||||
"LabelFeedURL": "URL zdroje",
|
"LabelFeedURL": "URL kanálu",
|
||||||
"LabelFetchingMetadata": "Získávání metadat",
|
"LabelFetchingMetadata": "Získávání metadat",
|
||||||
"LabelFile": "Soubor",
|
"LabelFile": "Soubor",
|
||||||
"LabelFileBirthtime": "Čas vzniku souboru",
|
"LabelFileBirthtime": "Čas vzniku souboru",
|
||||||
@@ -400,6 +420,7 @@
|
|||||||
"LabelLanguages": "Jazyky",
|
"LabelLanguages": "Jazyky",
|
||||||
"LabelLastBookAdded": "Poslední kniha přidána",
|
"LabelLastBookAdded": "Poslední kniha přidána",
|
||||||
"LabelLastBookUpdated": "Poslední kniha aktualizována",
|
"LabelLastBookUpdated": "Poslední kniha aktualizována",
|
||||||
|
"LabelLastProgressDate": "Poslední pokrok: {0}",
|
||||||
"LabelLastSeen": "Naposledy viděno",
|
"LabelLastSeen": "Naposledy viděno",
|
||||||
"LabelLastTime": "Naposledy",
|
"LabelLastTime": "Naposledy",
|
||||||
"LabelLastUpdate": "Poslední aktualizace",
|
"LabelLastUpdate": "Poslední aktualizace",
|
||||||
@@ -412,6 +433,7 @@
|
|||||||
"LabelLibraryFilterSublistEmpty": "Žádné {0}",
|
"LabelLibraryFilterSublistEmpty": "Žádné {0}",
|
||||||
"LabelLibraryItem": "Položka knihovny",
|
"LabelLibraryItem": "Položka knihovny",
|
||||||
"LabelLibraryName": "Název knihovny",
|
"LabelLibraryName": "Název knihovny",
|
||||||
|
"LabelLibrarySortByProgress": "Aktualizace pokroku",
|
||||||
"LabelLimit": "Omezit",
|
"LabelLimit": "Omezit",
|
||||||
"LabelLineSpacing": "Řádkování",
|
"LabelLineSpacing": "Řádkování",
|
||||||
"LabelListenAgain": "Poslouchat znovu",
|
"LabelListenAgain": "Poslouchat znovu",
|
||||||
@@ -420,8 +442,9 @@
|
|||||||
"LabelLogLevelWarn": "Varovat",
|
"LabelLogLevelWarn": "Varovat",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Hledat nové epizody po tomto datu",
|
"LabelLookForNewEpisodesAfterDate": "Hledat nové epizody po tomto datu",
|
||||||
"LabelLowestPriority": "Nejnižší priorita",
|
"LabelLowestPriority": "Nejnižší priorita",
|
||||||
|
"LabelMatchConfidence": "Jistota",
|
||||||
"LabelMatchExistingUsersBy": "Přiřadit stávající uživatele podle",
|
"LabelMatchExistingUsersBy": "Přiřadit stávající uživatele podle",
|
||||||
"LabelMatchExistingUsersByDescription": "Slouží k propojení stávajících uživatelů. Po propojení budou uživatelé přiřazeni k jedinečnému ID od poskytovatele SSO.",
|
"LabelMatchExistingUsersByDescription": "Slouží k propojení stávajících uživatelů. Po propojení budou uživatelé přiřazeni k jedinečnému ID od poskytovatele SSO",
|
||||||
"LabelMaxEpisodesToDownload": "Maximální # epizod pro stažení. Použijte 0 pro bez omezení.",
|
"LabelMaxEpisodesToDownload": "Maximální # epizod pro stažení. Použijte 0 pro bez omezení.",
|
||||||
"LabelMaxEpisodesToDownloadPerCheck": "Maximální počet nových epizod ke stažení při jedné kontrole",
|
"LabelMaxEpisodesToDownloadPerCheck": "Maximální počet nových epizod ke stažení při jedné kontrole",
|
||||||
"LabelMaxEpisodesToKeep": "Maximální počet epizod k zachování",
|
"LabelMaxEpisodesToKeep": "Maximální počet epizod k zachování",
|
||||||
@@ -430,7 +453,7 @@
|
|||||||
"LabelMediaType": "Typ média",
|
"LabelMediaType": "Typ média",
|
||||||
"LabelMetaTag": "Metaznačka",
|
"LabelMetaTag": "Metaznačka",
|
||||||
"LabelMetaTags": "Metaznačky",
|
"LabelMetaTags": "Metaznačky",
|
||||||
"LabelMetadataOrderOfPrecedenceDescription": "Zdroje metadat s vyšší prioritou budou mít přednost před zdroji metadat s nižší prioritou.",
|
"LabelMetadataOrderOfPrecedenceDescription": "Zdroje metadat s vyšší prioritou budou mít přednost před zdroji metadat s nižší prioritou",
|
||||||
"LabelMetadataProvider": "Poskytovatel metadat",
|
"LabelMetadataProvider": "Poskytovatel metadat",
|
||||||
"LabelMinute": "Minuta",
|
"LabelMinute": "Minuta",
|
||||||
"LabelMinutes": "Minuty",
|
"LabelMinutes": "Minuty",
|
||||||
@@ -450,6 +473,7 @@
|
|||||||
"LabelNewestEpisodes": "Nejnovější epizody",
|
"LabelNewestEpisodes": "Nejnovější epizody",
|
||||||
"LabelNextBackupDate": "Datum příští zálohy",
|
"LabelNextBackupDate": "Datum příští zálohy",
|
||||||
"LabelNextScheduledRun": "Další naplánované spuštění",
|
"LabelNextScheduledRun": "Další naplánované spuštění",
|
||||||
|
"LabelNoApiKeys": "Žádné API klíče",
|
||||||
"LabelNoCustomMetadataProviders": "Žádní vlastní poskytovatelé metadat",
|
"LabelNoCustomMetadataProviders": "Žádní vlastní poskytovatelé metadat",
|
||||||
"LabelNoEpisodesSelected": "Nebyly vybrány žádné epizody",
|
"LabelNoEpisodesSelected": "Nebyly vybrány žádné epizody",
|
||||||
"LabelNotFinished": "Nedokončeno",
|
"LabelNotFinished": "Nedokončeno",
|
||||||
@@ -509,9 +533,9 @@
|
|||||||
"LabelPublishers": "Vydavatelé",
|
"LabelPublishers": "Vydavatelé",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka",
|
"LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka",
|
||||||
"LabelRSSFeedCustomOwnerName": "Vlastní jméno vlastníka",
|
"LabelRSSFeedCustomOwnerName": "Vlastní jméno vlastníka",
|
||||||
"LabelRSSFeedOpen": "Otevření RSS kanálu",
|
"LabelRSSFeedOpen": "RSS kanál otevřen",
|
||||||
"LabelRSSFeedPreventIndexing": "Zabránit indexování",
|
"LabelRSSFeedPreventIndexing": "Zabránit indexování",
|
||||||
"LabelRSSFeedSlug": "RSS kanál Slug",
|
"LabelRSSFeedSlug": "Klíčové slovo kanálu RSS",
|
||||||
"LabelRSSFeedURL": "URL RSS kanálu",
|
"LabelRSSFeedURL": "URL RSS kanálu",
|
||||||
"LabelRandomly": "Náhodně",
|
"LabelRandomly": "Náhodně",
|
||||||
"LabelReAddSeriesToContinueListening": "Znovu přidat sérii k pokračování poslechu",
|
"LabelReAddSeriesToContinueListening": "Znovu přidat sérii k pokračování poslechu",
|
||||||
@@ -526,6 +550,7 @@
|
|||||||
"LabelReleaseDate": "Datum vydání",
|
"LabelReleaseDate": "Datum vydání",
|
||||||
"LabelRemoveAllMetadataAbs": "Odebrat všechny soubory metadata.abs",
|
"LabelRemoveAllMetadataAbs": "Odebrat všechny soubory metadata.abs",
|
||||||
"LabelRemoveAllMetadataJson": "Smazat všechny soubory metadata.json",
|
"LabelRemoveAllMetadataJson": "Smazat všechny soubory metadata.json",
|
||||||
|
"LabelRemoveAudibleBranding": "Odebrat úvod a závěr Audible z kapitol",
|
||||||
"LabelRemoveCover": "Odstranit obálku",
|
"LabelRemoveCover": "Odstranit obálku",
|
||||||
"LabelRemoveMetadataFile": "Odstranit soubory metadat ve složkách položek knihovny",
|
"LabelRemoveMetadataFile": "Odstranit soubory metadat ve složkách položek knihovny",
|
||||||
"LabelRemoveMetadataFileHelp": "Odstraníte všechny soubory metadata.json a metadata.abs ve svých složkách {0}.",
|
"LabelRemoveMetadataFileHelp": "Odstraníte všechny soubory metadata.json a metadata.abs ve svých složkách {0}.",
|
||||||
@@ -538,6 +563,7 @@
|
|||||||
"LabelSelectAll": "Vybrat vše",
|
"LabelSelectAll": "Vybrat vše",
|
||||||
"LabelSelectAllEpisodes": "Vybrat všechny epizody",
|
"LabelSelectAllEpisodes": "Vybrat všechny epizody",
|
||||||
"LabelSelectEpisodesShowing": "Vyberte {0} epizody, které se zobrazují",
|
"LabelSelectEpisodesShowing": "Vyberte {0} epizody, které se zobrazují",
|
||||||
|
"LabelSelectUser": "Vybrat uživatele",
|
||||||
"LabelSelectUsers": "Vybrat uživatele",
|
"LabelSelectUsers": "Vybrat uživatele",
|
||||||
"LabelSendEbookToDevice": "Odeslat e-knihu do...",
|
"LabelSendEbookToDevice": "Odeslat e-knihu do...",
|
||||||
"LabelSequence": "Sekvence",
|
"LabelSequence": "Sekvence",
|
||||||
@@ -545,7 +571,7 @@
|
|||||||
"LabelSeries": "Série",
|
"LabelSeries": "Série",
|
||||||
"LabelSeriesName": "Název série",
|
"LabelSeriesName": "Název série",
|
||||||
"LabelSeriesProgress": "Průběh série",
|
"LabelSeriesProgress": "Průběh série",
|
||||||
"LabelServerLogLevel": "Úroveň protokolu serveru",
|
"LabelServerLogLevel": "Úroveň Logování serveru",
|
||||||
"LabelServerYearReview": "Přehled roku na serveru ({0})",
|
"LabelServerYearReview": "Přehled roku na serveru ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Nastavit jako primární",
|
"LabelSetEbookAsPrimary": "Nastavit jako primární",
|
||||||
"LabelSetEbookAsSupplementary": "Nastavit jako doplňkové",
|
"LabelSetEbookAsSupplementary": "Nastavit jako doplňkové",
|
||||||
@@ -555,6 +581,8 @@
|
|||||||
"LabelSettingsBookshelfViewHelp": "Skeumorfní design s dřevěnými policemi",
|
"LabelSettingsBookshelfViewHelp": "Skeumorfní design s dřevěnými policemi",
|
||||||
"LabelSettingsChromecastSupport": "Podpora Chromecastu",
|
"LabelSettingsChromecastSupport": "Podpora Chromecastu",
|
||||||
"LabelSettingsDateFormat": "Formát data",
|
"LabelSettingsDateFormat": "Formát data",
|
||||||
|
"LabelSettingsEnableWatcher": "Automaticky skenovat změny v knihovnách",
|
||||||
|
"LabelSettingsEnableWatcherForLibrary": "Automaticky skenovat změny v knihovně",
|
||||||
"LabelSettingsEnableWatcherHelp": "Povoluje automatické přidávání/aktualizaci položek, když jsou zjištěny změny souborů. *Vyžaduje restart serveru",
|
"LabelSettingsEnableWatcherHelp": "Povoluje automatické přidávání/aktualizaci položek, když jsou zjištěny změny souborů. *Vyžaduje restart serveru",
|
||||||
"LabelSettingsEpubsAllowScriptedContent": "Povolení skriptovaného obsahu v epubu",
|
"LabelSettingsEpubsAllowScriptedContent": "Povolení skriptovaného obsahu v epubu",
|
||||||
"LabelSettingsEpubsAllowScriptedContentHelp": "Povolení spouštění skriptů v souborech epub. Doporučujeme toto nastavení vypnout, pokud nedůvěřujete zdroji souborů epub.",
|
"LabelSettingsEpubsAllowScriptedContentHelp": "Povolení spouštění skriptů v souborech epub. Doporučujeme toto nastavení vypnout, pokud nedůvěřujete zdroji souborů epub.",
|
||||||
@@ -598,6 +626,7 @@
|
|||||||
"LabelSlug": "URL název",
|
"LabelSlug": "URL název",
|
||||||
"LabelSortAscending": "Vzestupně",
|
"LabelSortAscending": "Vzestupně",
|
||||||
"LabelSortDescending": "Sestupně",
|
"LabelSortDescending": "Sestupně",
|
||||||
|
"LabelSortPubDate": "Seřadit podle datumu publikování",
|
||||||
"LabelStart": "Spustit",
|
"LabelStart": "Spustit",
|
||||||
"LabelStartTime": "Čas Spuštění",
|
"LabelStartTime": "Čas Spuštění",
|
||||||
"LabelStarted": "Spuštěno",
|
"LabelStarted": "Spuštěno",
|
||||||
@@ -698,10 +727,16 @@
|
|||||||
"LabelYourProgress": "Váš pokrok",
|
"LabelYourProgress": "Váš pokrok",
|
||||||
"MessageAddToPlayerQueue": "Přidat do fronty přehrávače",
|
"MessageAddToPlayerQueue": "Přidat do fronty přehrávače",
|
||||||
"MessageAppriseDescription": "Abyste mohli používat tuto funkci, musíte mít spuštěnou instanci <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> nebo API, které bude zpracovávat stejné požadavky. <br />Adresa URL API Apprise by měla být úplná URL cesta pro odeslání oznámení, např. pokud je vaše instance API obsluhována na adrese <code>http://192.168.1.1:8337</code> pak byste měli zadat <code>http://192.168.1.1:8337/notify</code>.",
|
"MessageAppriseDescription": "Abyste mohli používat tuto funkci, musíte mít spuštěnou instanci <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> nebo API, které bude zpracovávat stejné požadavky. <br />Adresa URL API Apprise by měla být úplná URL cesta pro odeslání oznámení, např. pokud je vaše instance API obsluhována na adrese <code>http://192.168.1.1:8337</code> pak byste měli zadat <code>http://192.168.1.1:8337/notify</code>.",
|
||||||
|
"MessageAsinCheck": "Ujistěte se, že používáte ASIN ze správného regionu Audible a ne z Amazonu.",
|
||||||
|
"MessageAuthenticationLegacyTokenWarning": "Zastaralé API tokeny budou v budoucnu odstraněny. Použijte místo nich <a href=\"/config/api-keys\">API klíče</a>.",
|
||||||
|
"MessageAuthenticationOIDCChangesRestart": "Po uložení restartujte server, aby se změny OIDC použily.",
|
||||||
|
"MessageAuthenticationSecurityMessage": "Bezpečnost autentizace byla vylepšena. Všichni uživatelé se musí znovu přihlásit.",
|
||||||
"MessageBackupsDescription": "Zálohy zahrnují uživatele, průběh uživatele, podrobnosti o položkách knihovny, nastavení serveru a obrázky uložené v <code>/metadata/items</code> a <code>/metadata/authors</code>. Zálohy <strong>ne</strong> zahrnují všechny soubory uložené ve složkách knihovny.",
|
"MessageBackupsDescription": "Zálohy zahrnují uživatele, průběh uživatele, podrobnosti o položkách knihovny, nastavení serveru a obrázky uložené v <code>/metadata/items</code> a <code>/metadata/authors</code>. Zálohy <strong>ne</strong> zahrnují všechny soubory uložené ve složkách knihovny.",
|
||||||
"MessageBackupsLocationEditNote": "Poznámka: Změna umístění záloh nepřesune ani nezmění existující zálohy",
|
"MessageBackupsLocationEditNote": "Poznámka: Změna umístění záloh nepřesune ani nezmění existující zálohy",
|
||||||
"MessageBackupsLocationNoEditNote": "Poznámka: Umístění záloh je nastavené z proměnných prostředí a nelze zde změnit.",
|
"MessageBackupsLocationNoEditNote": "Poznámka: Umístění záloh je nastavené z proměnných prostředí a nelze zde změnit.",
|
||||||
"MessageBackupsLocationPathEmpty": "Umístění záloh nemůže být prázdné",
|
"MessageBackupsLocationPathEmpty": "Umístění záloh nemůže být prázdné",
|
||||||
|
"MessageBatchEditPopulateMapDetailsAllHelp": "Předvyplnit vybraná pole datami ze všech položek. Pole s více hodnotami budou sloučena",
|
||||||
|
"MessageBatchEditPopulateMapDetailsItemHelp": "Předvyplnit povolená pole mapování daty z této položky",
|
||||||
"MessageBatchQuickMatchDescription": "Rychlá párování se pokusí přidat chybějící obálky a metadata pro vybrané položky. Povolením níže uvedených možností umožníte funkci Rychlé párování přepsat stávající obálky a/nebo metadata.",
|
"MessageBatchQuickMatchDescription": "Rychlá párování se pokusí přidat chybějící obálky a metadata pro vybrané položky. Povolením níže uvedených možností umožníte funkci Rychlé párování přepsat stávající obálky a/nebo metadata.",
|
||||||
"MessageBookshelfNoCollections": "Ještě jste nevytvořili žádnou sbírku",
|
"MessageBookshelfNoCollections": "Ještě jste nevytvořili žádnou sbírku",
|
||||||
"MessageBookshelfNoCollectionsHelp": "Kolekce jsou veřejné. Mohou je zobrazit všichni uživatelé s přístupem do knihovny.",
|
"MessageBookshelfNoCollectionsHelp": "Kolekce jsou veřejné. Mohou je zobrazit všichni uživatelé s přístupem do knihovny.",
|
||||||
@@ -714,8 +749,10 @@
|
|||||||
"MessageChapterErrorStartGteDuration": "Neplatný čas začátku, musí být kratší než doba trvání audioknihy",
|
"MessageChapterErrorStartGteDuration": "Neplatný čas začátku, musí být kratší než doba trvání audioknihy",
|
||||||
"MessageChapterErrorStartLtPrev": "Neplatný čas začátku, musí být větší nebo roven času začátku předchozí kapitoly",
|
"MessageChapterErrorStartLtPrev": "Neplatný čas začátku, musí být větší nebo roven času začátku předchozí kapitoly",
|
||||||
"MessageChapterStartIsAfter": "Začátek kapitoly přesahuje konec audioknihy",
|
"MessageChapterStartIsAfter": "Začátek kapitoly přesahuje konec audioknihy",
|
||||||
|
"MessageChaptersNotFound": "Kapitoly nenalezeny",
|
||||||
"MessageCheckingCron": "Kontrola cronu...",
|
"MessageCheckingCron": "Kontrola cronu...",
|
||||||
"MessageConfirmCloseFeed": "Opravdu chcete zavřít tento kanál?",
|
"MessageConfirmCloseFeed": "Opravdu chcete zavřít tento kanál?",
|
||||||
|
"MessageConfirmDeleteApiKey": "Opravdu chcete vymazat API klíč \"{0}\"?",
|
||||||
"MessageConfirmDeleteBackup": "Opravdu chcete smazat zálohu pro {0}?",
|
"MessageConfirmDeleteBackup": "Opravdu chcete smazat zálohu pro {0}?",
|
||||||
"MessageConfirmDeleteDevice": "Opravdu chcete vymazat zařízení e-reader \"{0}\"?",
|
"MessageConfirmDeleteDevice": "Opravdu chcete vymazat zařízení e-reader \"{0}\"?",
|
||||||
"MessageConfirmDeleteFile": "Tento krok smaže soubor ze souborového systému. Jsi si jisti?",
|
"MessageConfirmDeleteFile": "Tento krok smaže soubor ze souborového systému. Jsi si jisti?",
|
||||||
@@ -743,6 +780,7 @@
|
|||||||
"MessageConfirmRemoveAuthor": "Opravdu chcete odstranit autora \"{0}\"?",
|
"MessageConfirmRemoveAuthor": "Opravdu chcete odstranit autora \"{0}\"?",
|
||||||
"MessageConfirmRemoveCollection": "Opravdu chcete odstranit kolekci \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Opravdu chcete odstranit kolekci \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Opravdu chcete odstranit epizodu \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Opravdu chcete odstranit epizodu \"{0}\"?",
|
||||||
|
"MessageConfirmRemoveEpisodeNote": "Poznámka: Tím se zvukový soubor neodstraní, pokud nepřepnete volbu “Tvrdé odstranění souboru“",
|
||||||
"MessageConfirmRemoveEpisodes": "Opravdu chcete odstranit {0} epizody?",
|
"MessageConfirmRemoveEpisodes": "Opravdu chcete odstranit {0} epizody?",
|
||||||
"MessageConfirmRemoveListeningSessions": "Opravdu chcete odebrat {0} poslechových relací?",
|
"MessageConfirmRemoveListeningSessions": "Opravdu chcete odebrat {0} poslechových relací?",
|
||||||
"MessageConfirmRemoveMetadataFiles": "Jste si jisti, že chcete odstranit všechny metadata.{0} soubory ve složkách s položkami ve vaší knihovně?",
|
"MessageConfirmRemoveMetadataFiles": "Jste si jisti, že chcete odstranit všechny metadata.{0} soubory ve složkách s položkami ve vaší knihovně?",
|
||||||
@@ -768,14 +806,17 @@
|
|||||||
"MessageFeedURLWillBe": "URL zdroje bude {0}",
|
"MessageFeedURLWillBe": "URL zdroje bude {0}",
|
||||||
"MessageFetching": "Načítání...",
|
"MessageFetching": "Načítání...",
|
||||||
"MessageForceReScanDescription": "znovu prohledá všechny soubory jako při novém skenování. ID3 tagy zvukových souborů OPF soubory a textové soubory budou skenovány jako nové.",
|
"MessageForceReScanDescription": "znovu prohledá všechny soubory jako při novém skenování. ID3 tagy zvukových souborů OPF soubory a textové soubory budou skenovány jako nové.",
|
||||||
|
"MessageHeatmapListeningTimeTooltip": "<strong>{0} poslechnuto</strong> na {1}",
|
||||||
|
"MessageHeatmapNoListeningSessions": "Žádné relace poslouchání na {0}",
|
||||||
"MessageImportantNotice": "Důležité upozornění!",
|
"MessageImportantNotice": "Důležité upozornění!",
|
||||||
"MessageInsertChapterBelow": "Vložit kapitolu níže",
|
"MessageInsertChapterBelow": "Vložit kapitolu níže",
|
||||||
|
"MessageInvalidAsin": "Neplatný ASIN",
|
||||||
"MessageItemsSelected": "{0} vybraných položek",
|
"MessageItemsSelected": "{0} vybraných položek",
|
||||||
"MessageItemsUpdated": "{0} položky byly aktualizovány",
|
"MessageItemsUpdated": "{0} položky byly aktualizovány",
|
||||||
"MessageJoinUsOn": "Přidejte se k nám",
|
"MessageJoinUsOn": "Přidejte se k nám",
|
||||||
"MessageLoading": "Načítá se...",
|
"MessageLoading": "Načítá se...",
|
||||||
"MessageLoadingFolders": "Načítám složky...",
|
"MessageLoadingFolders": "Načítám složky...",
|
||||||
"MessageLogsDescription": "Protokoly se ukládají do souborů JSON v <code>/metadata/logs</code>. Protokoly o pádech jsou uloženy v <code>/metadata/logs/crash_logs.txt</code>.",
|
"MessageLogsDescription": "Logy se ukládají do souborů JSON v <code>/metadata/logs</code>. Logy o pádech jsou uloženy v <code>/metadata/logs/crash_logs.txt</code>.",
|
||||||
"MessageM4BFailed": "M4B se nezdařil!",
|
"MessageM4BFailed": "M4B se nezdařil!",
|
||||||
"MessageM4BFinished": "M4B dokončen!",
|
"MessageM4BFinished": "M4B dokončen!",
|
||||||
"MessageMapChapterTitles": "Mapování názvů kapitol ke stávajícím kapitolám audioknihy bez úpravy časových razítek",
|
"MessageMapChapterTitles": "Mapování názvů kapitol ke stávajícím kapitolám audioknihy bez úpravy časových razítek",
|
||||||
@@ -799,11 +840,11 @@
|
|||||||
"MessageNoEpisodes": "Žádné epizody",
|
"MessageNoEpisodes": "Žádné epizody",
|
||||||
"MessageNoFoldersAvailable": "Nejsou k dispozici žádné složky",
|
"MessageNoFoldersAvailable": "Nejsou k dispozici žádné složky",
|
||||||
"MessageNoGenres": "Žádné žánry",
|
"MessageNoGenres": "Žádné žánry",
|
||||||
"MessageNoIssues": "Žádné výtisk",
|
"MessageNoIssues": "Žádné problémy",
|
||||||
"MessageNoItems": "Žádné položky",
|
"MessageNoItems": "Žádné položky",
|
||||||
"MessageNoItemsFound": "Nebyly nalezeny žádné položky",
|
"MessageNoItemsFound": "Nebyly nalezeny žádné položky",
|
||||||
"MessageNoListeningSessions": "Žádné poslechové relace",
|
"MessageNoListeningSessions": "Žádné poslechové relace",
|
||||||
"MessageNoLogs": "Žádné protokoly",
|
"MessageNoLogs": "Žádné logy",
|
||||||
"MessageNoMediaProgress": "Žádný průběh médií",
|
"MessageNoMediaProgress": "Žádný průběh médií",
|
||||||
"MessageNoNotifications": "Žádná oznámení",
|
"MessageNoNotifications": "Žádná oznámení",
|
||||||
"MessageNoPodcastFeed": "Neplatný podcast: Žádný kanál",
|
"MessageNoPodcastFeed": "Neplatný podcast: Žádný kanál",
|
||||||
@@ -838,8 +879,10 @@
|
|||||||
"MessageRestoreBackupConfirm": "Opravdu chcete obnovit zálohu vytvořenou dne",
|
"MessageRestoreBackupConfirm": "Opravdu chcete obnovit zálohu vytvořenou dne",
|
||||||
"MessageRestoreBackupWarning": "Obnovení zálohy přepíše celou databázi umístěnou v /config a obálku obrázků v /metadata/items & /metadata/authors.<br /><br />Backups nezmění žádné soubory ve složkách knihovny. Pokud jste povolili nastavení serveru pro ukládání obrázků obalu a metadat do složek knihovny, nebudou zálohovány ani přepsány.<br /><br />Všichni klienti používající váš server budou automaticky obnoveni.",
|
"MessageRestoreBackupWarning": "Obnovení zálohy přepíše celou databázi umístěnou v /config a obálku obrázků v /metadata/items & /metadata/authors.<br /><br />Backups nezmění žádné soubory ve složkách knihovny. Pokud jste povolili nastavení serveru pro ukládání obrázků obalu a metadat do složek knihovny, nebudou zálohovány ani přepsány.<br /><br />Všichni klienti používající váš server budou automaticky obnoveni.",
|
||||||
"MessageScheduleLibraryScanNote": "Většině uživatelů se doporučuje ponechat tuto funkci vypnutou a ponechat zapnuté nastavení sledování složek. Sledování složek automaticky zjistí změny ve složkách vaší knihovny. Sledování složek nefunguje pro každý souborový systém (jako je NFS), takže místo toho lze použít plánované skenování knihoven.",
|
"MessageScheduleLibraryScanNote": "Většině uživatelů se doporučuje ponechat tuto funkci vypnutou a ponechat zapnuté nastavení sledování složek. Sledování složek automaticky zjistí změny ve složkách vaší knihovny. Sledování složek nefunguje pro každý souborový systém (jako je NFS), takže místo toho lze použít plánované skenování knihoven.",
|
||||||
|
"MessageScheduleRunEveryWeekdayAtTime": "Spusť každý {0} v {1}",
|
||||||
"MessageSearchResultsFor": "Výsledky hledání pro",
|
"MessageSearchResultsFor": "Výsledky hledání pro",
|
||||||
"MessageSelected": "{0} vybráno",
|
"MessageSelected": "{0} vybráno",
|
||||||
|
"MessageSeriesSequenceCannotContainSpaces": "Sekvence série nesmí obsahovat mezery",
|
||||||
"MessageServerCouldNotBeReached": "Server je nedostupný",
|
"MessageServerCouldNotBeReached": "Server je nedostupný",
|
||||||
"MessageSetChaptersFromTracksDescription": "Nastavit kapitoly jako kapitolu a název kapitoly jako název zvukového souboru",
|
"MessageSetChaptersFromTracksDescription": "Nastavit kapitoly jako kapitolu a název kapitoly jako název zvukového souboru",
|
||||||
"MessageShareExpirationWillBe": "Expiruje <strong>{0}</strong>",
|
"MessageShareExpirationWillBe": "Expiruje <strong>{0}</strong>",
|
||||||
@@ -901,6 +944,8 @@
|
|||||||
"NotificationOnBackupCompletedDescription": "Spuštěno po dokončení zálohování",
|
"NotificationOnBackupCompletedDescription": "Spuštěno po dokončení zálohování",
|
||||||
"NotificationOnBackupFailedDescription": "Spuštěno pokud zálohování selže",
|
"NotificationOnBackupFailedDescription": "Spuštěno pokud zálohování selže",
|
||||||
"NotificationOnEpisodeDownloadedDescription": "Spuštěno při automatickém stažení epizody podcastu",
|
"NotificationOnEpisodeDownloadedDescription": "Spuštěno při automatickém stažení epizody podcastu",
|
||||||
|
"NotificationOnRSSFeedDisabledDescription": "Aktivováno když je automatické stahování pozastaveno z důvodu příliš mnoho neůspěšných pokusů",
|
||||||
|
"NotificationOnRSSFeedFailedDescription": "Aktivováno když selže RSS kanál pro stahování epizod",
|
||||||
"NotificationOnTestDescription": "Akce pro otestování upozorňovacího systému",
|
"NotificationOnTestDescription": "Akce pro otestování upozorňovacího systému",
|
||||||
"PlaceholderNewCollection": "Nový název kolekce",
|
"PlaceholderNewCollection": "Nový název kolekce",
|
||||||
"PlaceholderNewFolderPath": "Nová cesta ke složce",
|
"PlaceholderNewFolderPath": "Nová cesta ke složce",
|
||||||
@@ -945,6 +990,7 @@
|
|||||||
"ToastBackupRestoreFailed": "Nepodařilo se obnovit zálohu",
|
"ToastBackupRestoreFailed": "Nepodařilo se obnovit zálohu",
|
||||||
"ToastBackupUploadFailed": "Nepodařilo se nahrát zálohu",
|
"ToastBackupUploadFailed": "Nepodařilo se nahrát zálohu",
|
||||||
"ToastBackupUploadSuccess": "Záloha nahrána",
|
"ToastBackupUploadSuccess": "Záloha nahrána",
|
||||||
|
"ToastBatchApplyDetailsToItemsSuccess": "Detaily byly aplikované na položky",
|
||||||
"ToastBatchDeleteFailed": "Hromadné smazání selhalo",
|
"ToastBatchDeleteFailed": "Hromadné smazání selhalo",
|
||||||
"ToastBatchDeleteSuccess": "Hromadné smazání proběhlo úspěšně",
|
"ToastBatchDeleteSuccess": "Hromadné smazání proběhlo úspěšně",
|
||||||
"ToastBatchQuickMatchFailed": "Rychlá schoda dávky se nezdařila!",
|
"ToastBatchQuickMatchFailed": "Rychlá schoda dávky se nezdařila!",
|
||||||
@@ -957,6 +1003,8 @@
|
|||||||
"ToastCachePurgeFailed": "Nepodařilo se vyčistit mezipaměť",
|
"ToastCachePurgeFailed": "Nepodařilo se vyčistit mezipaměť",
|
||||||
"ToastCachePurgeSuccess": "Vyrovnávací paměť úspěšně vyčištěna",
|
"ToastCachePurgeSuccess": "Vyrovnávací paměť úspěšně vyčištěna",
|
||||||
"ToastChaptersHaveErrors": "Kapitoly obsahují chyby",
|
"ToastChaptersHaveErrors": "Kapitoly obsahují chyby",
|
||||||
|
"ToastChaptersInvalidShiftAmountLast": "Nesprávná délka posunu. Čas začátku poslední kapitoly by přesáhl dobu trvání této audioknihy.",
|
||||||
|
"ToastChaptersInvalidShiftAmountStart": "Nesprávná délka posunu. První kapitola by měla nulovou nebo zápornou délku a byla by přepsána druhou kapitolou. Zvětšete čas začátku druhé kapitoly.",
|
||||||
"ToastChaptersMustHaveTitles": "Kapitoly musí mít názvy",
|
"ToastChaptersMustHaveTitles": "Kapitoly musí mít názvy",
|
||||||
"ToastChaptersRemoved": "Kapitoly odstraněny",
|
"ToastChaptersRemoved": "Kapitoly odstraněny",
|
||||||
"ToastChaptersUpdated": "Kapitola aktualizována",
|
"ToastChaptersUpdated": "Kapitola aktualizována",
|
||||||
@@ -978,6 +1026,8 @@
|
|||||||
"ToastEpisodeDownloadQueueClearSuccess": "Fronta stahování epizod je prázdná",
|
"ToastEpisodeDownloadQueueClearSuccess": "Fronta stahování epizod je prázdná",
|
||||||
"ToastEpisodeUpdateSuccess": "{0} epizod aktualizováno",
|
"ToastEpisodeUpdateSuccess": "{0} epizod aktualizováno",
|
||||||
"ToastErrorCannotShare": "Na tomto zařízení nelze nativně sdílet",
|
"ToastErrorCannotShare": "Na tomto zařízení nelze nativně sdílet",
|
||||||
|
"ToastFailedToCreate": "Nepodařilo se vytvořit",
|
||||||
|
"ToastFailedToDelete": "Nepodařilo se odstranit",
|
||||||
"ToastFailedToLoadData": "Nepodařilo se načíst data",
|
"ToastFailedToLoadData": "Nepodařilo se načíst data",
|
||||||
"ToastFailedToMatch": "Nepodařilo se spárovat",
|
"ToastFailedToMatch": "Nepodařilo se spárovat",
|
||||||
"ToastFailedToShare": "Sdílení selhalo",
|
"ToastFailedToShare": "Sdílení selhalo",
|
||||||
@@ -985,6 +1035,7 @@
|
|||||||
"ToastInvalidImageUrl": "Neplatná URL obrázku",
|
"ToastInvalidImageUrl": "Neplatná URL obrázku",
|
||||||
"ToastInvalidMaxEpisodesToDownload": "Neplatný maximální počet epizod ke stažení",
|
"ToastInvalidMaxEpisodesToDownload": "Neplatný maximální počet epizod ke stažení",
|
||||||
"ToastInvalidUrl": "Neplatná URL",
|
"ToastInvalidUrl": "Neplatná URL",
|
||||||
|
"ToastInvalidUrls": "Alespoň jedna URL je neplatná",
|
||||||
"ToastItemCoverUpdateSuccess": "Obálka předmětu byl aktualizována",
|
"ToastItemCoverUpdateSuccess": "Obálka předmětu byl aktualizována",
|
||||||
"ToastItemDeletedFailed": "Smazání položky selhalo",
|
"ToastItemDeletedFailed": "Smazání položky selhalo",
|
||||||
"ToastItemDeletedSuccess": "Položka smazána",
|
"ToastItemDeletedSuccess": "Položka smazána",
|
||||||
@@ -1009,6 +1060,7 @@
|
|||||||
"ToastMustHaveAtLeastOnePath": "Musí mít minimálně jednu cestu",
|
"ToastMustHaveAtLeastOnePath": "Musí mít minimálně jednu cestu",
|
||||||
"ToastNameEmailRequired": "Jméno a email jsou vyžadovány",
|
"ToastNameEmailRequired": "Jméno a email jsou vyžadovány",
|
||||||
"ToastNameRequired": "Jméno je vyžadováno",
|
"ToastNameRequired": "Jméno je vyžadováno",
|
||||||
|
"ToastNewApiKeyUserError": "Je nutné vybrat uživatele",
|
||||||
"ToastNewEpisodesFound": "{0} nových epizod bylo nalezeno",
|
"ToastNewEpisodesFound": "{0} nových epizod bylo nalezeno",
|
||||||
"ToastNewUserCreatedFailed": "Chyba při vytváření účtu: \"{0}\"",
|
"ToastNewUserCreatedFailed": "Chyba při vytváření účtu: \"{0}\"",
|
||||||
"ToastNewUserCreatedSuccess": "Vytvořen nový účet",
|
"ToastNewUserCreatedSuccess": "Vytvořen nový účet",
|
||||||
@@ -1057,6 +1109,7 @@
|
|||||||
"ToastSelectAtLeastOneUser": "Vyberte alespoň jednoho uživatele",
|
"ToastSelectAtLeastOneUser": "Vyberte alespoň jednoho uživatele",
|
||||||
"ToastSendEbookToDeviceFailed": "Odeslání e-knihy do zařízení se nezdařilo",
|
"ToastSendEbookToDeviceFailed": "Odeslání e-knihy do zařízení se nezdařilo",
|
||||||
"ToastSendEbookToDeviceSuccess": "E-kniha odeslána do zařízení \"{0}\"",
|
"ToastSendEbookToDeviceSuccess": "E-kniha odeslána do zařízení \"{0}\"",
|
||||||
|
"ToastSeriesSubmitFailedSameName": "Nelze přidat dvě série se stejným názvem",
|
||||||
"ToastSeriesUpdateFailed": "Aktualizace série se nezdařila",
|
"ToastSeriesUpdateFailed": "Aktualizace série se nezdařila",
|
||||||
"ToastSeriesUpdateSuccess": "Aktualizace série byla úspěšná",
|
"ToastSeriesUpdateSuccess": "Aktualizace série byla úspěšná",
|
||||||
"ToastServerSettingsUpdateSuccess": "Nastavení serveru aktualizováno",
|
"ToastServerSettingsUpdateSuccess": "Nastavení serveru aktualizováno",
|
||||||
@@ -1075,6 +1128,8 @@
|
|||||||
"ToastUnknownError": "Neznámý error",
|
"ToastUnknownError": "Neznámý error",
|
||||||
"ToastUnlinkOpenIdFailed": "Chyba při odpárování uživatele z OpenID",
|
"ToastUnlinkOpenIdFailed": "Chyba při odpárování uživatele z OpenID",
|
||||||
"ToastUnlinkOpenIdSuccess": "Uživatel odpárován z uživatele z OpenID",
|
"ToastUnlinkOpenIdSuccess": "Uživatel odpárován z uživatele z OpenID",
|
||||||
|
"ToastUploaderFilepathExistsError": "Soubor \"{0}\" na serveru již existuje",
|
||||||
|
"ToastUploaderItemExistsInSubdirectoryError": "Položka \"{0}\" používá podadresář cesty pro nahrání.",
|
||||||
"ToastUserDeleteFailed": "Nepodařilo se smazat uživatele",
|
"ToastUserDeleteFailed": "Nepodařilo se smazat uživatele",
|
||||||
"ToastUserDeleteSuccess": "Uživatel smazán",
|
"ToastUserDeleteSuccess": "Uživatel smazán",
|
||||||
"ToastUserPasswordChangeSuccess": "Heslo bylo změněno úspěšně",
|
"ToastUserPasswordChangeSuccess": "Heslo bylo změněno úspěšně",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user