mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-07-13 19:04:57 +02:00
Compare commits
478 commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
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 | ||
|
d21fe49ce2 | ||
|
a992400d6a | ||
|
108b2a60f5 | ||
|
af684e6a69 | ||
|
5336d0525e | ||
|
bb4eec9355 | ||
|
28404f37b8 | ||
|
7b92c15a46 | ||
|
c150ed4e98 | ||
|
cb7632b216 | ||
|
b8849677de | ||
|
9bf8d7de11 | ||
|
6634ce8fd4 | ||
|
9d4303ef7b | ||
|
1f7be58124 | ||
|
6b8b27b04f | ||
|
ba4061e5a4 | ||
|
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 | ||
|
0c5d05d319 | ||
|
4a3eb7727b | ||
|
81640464ba | ||
|
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 | ||
|
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 | ||
|
bc1b99efd6 | ||
|
26309019e7 | ||
|
b47d7b734d | ||
|
62194b8781 | ||
|
7c114a051a | ||
|
26c0c89b94 | ||
|
c81071a7b3 | ||
|
31f48edcc3 | ||
|
f7109a055c | ||
|
9d0e7759e0 | ||
|
d15ccbd2fc | ||
|
cfeb1743df | ||
|
a7e0330b06 | ||
|
5f69e83d46 | ||
|
fdde62896f | ||
|
f1909d0fc7 | ||
|
28225618fd | ||
|
1a562a5f23 | ||
|
bfbbcba160 | ||
|
21e4d17ef3 | ||
|
87faebc7d9 | ||
|
5d868d1355 | ||
|
ac0fd41740 | ||
|
1202e95b66 | ||
|
c05cf9718b | ||
|
d8f1e43e85 | ||
|
ed7e87b168 | ||
|
4ca2c9e97f | ||
|
01dd1c0615 | ||
|
cd387b8fed | ||
|
c85cd69152 | ||
|
9e9b52a252 | ||
|
292d5783a9 | ||
|
c7faafd0f3 | ||
|
ca7388b14e | ||
|
ddf2ca3670 | ||
|
96825c3c2b | ||
|
6ed66fea16 | ||
|
ddcda197b4 | ||
|
8bea5d83f5 | ||
|
73c1ea92f3 | ||
|
4fb5330308 | ||
|
f853cff920 | ||
|
dc3c978f8d | ||
|
e87825f8df | ||
|
718433183b | ||
|
7f3421f78a | ||
|
7013600697 | ||
|
d3fe52692b | ||
|
d94e7975dc | ||
|
13fac2d5bc | ||
|
792424c4cd | ||
|
366fd1a6da | ||
|
2ce0c7ea47 | ||
|
973bd22199 | ||
|
252f4cb96a | ||
|
8e824366b0 | ||
|
52517c3bec | ||
|
c2862d4941 | ||
|
b4621b7e19 | ||
|
ac3c41dd88 | ||
|
9dca5f0bc0 | ||
|
daeb2b3f56 | ||
|
37e079866e | ||
|
ccdc76e88c | ||
|
381f91edd8 | ||
|
bbb0fbe63f | ||
|
378651265a | ||
|
c2cd962607 | ||
|
e5535c7fc4 | ||
|
a46394b2fe | ||
|
5e5a30cd5c | ||
|
a2e84d9c24 | ||
|
26ba8fd12f | ||
|
4ffc3b30f4 | ||
|
6be37632ff | ||
|
2954d6dd47 | ||
|
45f7ceeef6 | ||
|
ae65c7d650 | ||
|
9edc8b4d21 | ||
|
de2efda577 | ||
|
6760fa3498 | ||
|
af50fabdbf | ||
|
e16d3d72b6 | ||
|
59e099f950 | ||
|
ec4f275d52 | ||
|
006af2d616 | ||
|
b18da959db | ||
|
8d0676b780 | ||
|
f88e40ea3e | ||
|
37dc537830 | ||
|
f1af5a1cef | ||
|
1ea86a0e7a | ||
|
fd0af6b2dd | ||
|
84f99370ee | ||
|
bef3fa473c | ||
|
91c1f50a51 | ||
|
89d0257a76 | ||
|
ca7852171b | ||
|
06919af6b8 | ||
|
73c10aac7d | ||
|
bd91e466fd | ||
|
64b2c34031 | ||
|
92bb3527de | ||
|
7d0f61663e | ||
|
3b7db82bf0 | ||
|
ff36a9327c | ||
|
1def32aa50 | ||
|
3c9966e849 | ||
|
9b79aab4d5 | ||
|
0a9a846a33 | ||
|
0123dacb29 | ||
|
6ea2ef0845 | ||
|
30db5d50fb | ||
|
d60ccff5e8 | ||
|
39d2a25d01 | ||
|
121805ba39 | ||
|
f9bbd71174 | ||
|
2fbb31e0ea | ||
|
89167543fa | ||
|
33e0987d73 |
312 changed files with 11225 additions and 4010 deletions
48
.github/workflows/component-tests.yml
vendored
Normal file
48
.github/workflows/component-tests.yml
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
name: Run Component Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'Branch/Tag/SHA to test'
|
||||
required: true
|
||||
pull_request:
|
||||
paths:
|
||||
- 'client/**'
|
||||
- '.github/workflows/component-tests.yml'
|
||||
push:
|
||||
paths:
|
||||
- 'client/**'
|
||||
- '.github/workflows/component-tests.yml'
|
||||
|
||||
jobs:
|
||||
run-component-tests:
|
||||
name: Run Component Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout (push/pull request)
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
|
||||
- name: Checkout (workflow_dispatch)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd client
|
||||
npm ci
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd client
|
||||
npm test
|
2
.github/workflows/docker-build.yml
vendored
2
.github/workflows/docker-build.yml
vendored
|
@ -23,7 +23,7 @@ on:
|
|||
jobs:
|
||||
build:
|
||||
if: ${{ !contains(github.event.head_commit.message, 'skip ci') && github.repository == 'advplyr/audiobookshelf' }}
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Check out
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -23,3 +23,4 @@ sw.*
|
|||
.DS_STORE
|
||||
.idea/*
|
||||
tailwind.compiled.css
|
||||
tailwind.config.js
|
||||
|
|
50
Dockerfile
50
Dockerfile
|
@ -1,34 +1,32 @@
|
|||
ARG NUSQLITE3_DIR="/usr/local/lib/nusqlite3"
|
||||
ARG NUSQLITE3_PATH="${NUSQLITE3_DIR}/libnusqlite3.so"
|
||||
|
||||
### STAGE 0: Build client ###
|
||||
FROM node:20-alpine AS build
|
||||
FROM node:20-alpine AS build-client
|
||||
|
||||
WORKDIR /client
|
||||
COPY /client /client
|
||||
RUN npm ci && npm cache clean --force
|
||||
RUN npm run generate
|
||||
|
||||
### STAGE 1: Build server ###
|
||||
FROM node:20-alpine
|
||||
FROM node:20-alpine AS build-server
|
||||
|
||||
ARG NUSQLITE3_DIR
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache --update \
|
||||
RUN apk add --no-cache --update \
|
||||
curl \
|
||||
tzdata \
|
||||
ffmpeg \
|
||||
make \
|
||||
python3 \
|
||||
g++ \
|
||||
tini \
|
||||
unzip
|
||||
|
||||
COPY --from=build /client/dist /client/dist
|
||||
COPY index.js package* /
|
||||
COPY server server
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
ENV NUSQLITE3_DIR="/usr/local/lib/nusqlite3"
|
||||
ENV NUSQLITE3_PATH="${NUSQLITE3_DIR}/libnusqlite3.so"
|
||||
WORKDIR /server
|
||||
COPY index.js package* /server
|
||||
COPY /server /server/server
|
||||
|
||||
RUN case "$TARGETPLATFORM" in \
|
||||
"linux/amd64") \
|
||||
|
@ -42,14 +40,34 @@ RUN case "$TARGETPLATFORM" in \
|
|||
|
||||
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
|
||||
|
||||
ENV PORT=80
|
||||
ENV NODE_ENV=production
|
||||
ENV CONFIG_PATH="/config"
|
||||
ENV METADATA_PATH="/metadata"
|
||||
ENV SOURCE="docker"
|
||||
ENV NUSQLITE3_DIR=${NUSQLITE3_DIR}
|
||||
ENV NUSQLITE3_PATH=${NUSQLITE3_PATH}
|
||||
|
||||
ENTRYPOINT ["tini", "--"]
|
||||
CMD ["node", "index.js"]
|
||||
|
|
|
@ -1,3 +1,85 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import 'tailwindcss';
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
|
||||
[role='button'],
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@theme {
|
||||
--spacing-0\.5e: 0.125em;
|
||||
--spacing-1e: 0.25em;
|
||||
--spacing-1\.5e: 0.375em;
|
||||
--spacing-2e: 0.5em;
|
||||
--spacing-2\.5e: 0.625em;
|
||||
--spacing-3e: 0.75em;
|
||||
--spacing-3\.5e: 0.875em;
|
||||
--spacing-4e: 1em;
|
||||
--spacing-5e: 1.25em;
|
||||
--spacing-6e: 1.5em;
|
||||
--spacing-7e: 1.75em;
|
||||
--spacing-8e: 2em;
|
||||
--spacing-9e: 2.25em;
|
||||
--spacing-10e: 2.5em;
|
||||
--spacing-11e: 2.75em;
|
||||
--spacing-12e: 3em;
|
||||
--spacing-14e: 3.5em;
|
||||
--spacing-16e: 4em;
|
||||
--spacing-20e: 5em;
|
||||
--spacing-24e: 6em;
|
||||
--spacing-28e: 7em;
|
||||
--spacing-32e: 8em;
|
||||
--spacing-36e: 9em;
|
||||
--spacing-40e: 10em;
|
||||
--spacing-44e: 11em;
|
||||
--spacing-48e: 12em;
|
||||
--spacing-52e: 13em;
|
||||
--spacing-56e: 14em;
|
||||
--spacing-60e: 15em;
|
||||
--spacing-64e: 16em;
|
||||
--spacing-72e: 18em;
|
||||
--spacing-80e: 20em;
|
||||
--spacing-96e: 24em;
|
||||
|
||||
--color-bg: #373838;
|
||||
--color-primary: #232323;
|
||||
--color-accent: #1ad691;
|
||||
--color-error: #ff5252;
|
||||
--color-info: #2196f3;
|
||||
--color-success: #4caf50;
|
||||
--color-warning: #fb8c00;
|
||||
--color-darkgreen: rgb(34, 127, 35);
|
||||
--color-black-50: #bbbbbb;
|
||||
--color-black-100: #666666;
|
||||
--color-black-200: #555555;
|
||||
--color-black-300: #444444;
|
||||
--color-black-400: #333333;
|
||||
--color-black-500: #222222;
|
||||
--color-black-600: #111111;
|
||||
--color-black-700: #101010;
|
||||
|
||||
--font-sans: 'Source Sans Pro';
|
||||
--font-mono: 'Ubuntu Mono';
|
||||
|
||||
--text-xxs: 0.625rem;
|
||||
--text-1\.5xl: 1.375rem;
|
||||
--text-2\.5xl: 1.6875rem;
|
||||
--text-4\.5xl: 2.625rem;
|
||||
}
|
||||
|
|
|
@ -13,10 +13,10 @@
|
|||
<ui-libraries-dropdown class="mr-2" />
|
||||
|
||||
<controls-global-search v-if="currentLibrary" class="mr-1 sm:mr-0" />
|
||||
<div class="flex-grow" />
|
||||
<div class="grow" />
|
||||
|
||||
<ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
|
||||
<span class="material-symbols text-2xl text-warning text-opacity-50"> cast </span>
|
||||
<span class="material-symbols text-2xl text-warning/50"> cast </span>
|
||||
</ui-tooltip>
|
||||
<div v-if="isChromecastInitialized" class="w-6 min-w-6 h-6 ml-2 mr-1 sm:mx-2 cursor-pointer">
|
||||
<google-cast-launcher></google-cast-launcher>
|
||||
|
@ -42,7 +42,7 @@
|
|||
</ui-tooltip>
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link to="/account" class="relative w-9 h-9 md:w-32 bg-fg border border-gray-500 rounded shadow-sm ml-1.5 sm:ml-3 md:ml-5 md:pl-3 md:pr-10 py-2 text-left sm:text-sm cursor-pointer hover:bg-bg hover:bg-opacity-40" aria-haspopup="listbox" aria-expanded="true">
|
||||
<nuxt-link to="/account" class="relative w-9 h-9 md:w-32 bg-fg border border-gray-500 rounded-sm shadow-xs ml-1.5 sm:ml-3 md:ml-5 md:pl-3 md:pr-10 py-2 text-left sm:text-sm cursor-pointer hover:bg-bg/40" aria-haspopup="listbox" aria-expanded="true">
|
||||
<span class="items-center hidden md:flex">
|
||||
<span class="block truncate">{{ username }}</span>
|
||||
</span>
|
||||
|
@ -53,8 +53,8 @@
|
|||
</div>
|
||||
<div v-show="numMediaItemsSelected" class="absolute top-0 left-0 w-full h-full px-4 bg-primary flex items-center">
|
||||
<h1 class="text-lg md:text-2xl px-4">{{ $getString('MessageItemsSelected', [numMediaItemsSelected]) }}</h1>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn v-if="!isPodcastLibrary && selectedMediaItemsArePlayable" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playSelectedItems">
|
||||
<div class="grow" />
|
||||
<ui-btn v-if="!isPodcastLibrary && selectedMediaItemsArePlayable" color="bg-success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playSelectedItems">
|
||||
<span class="material-symbols fill text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||
{{ $strings.ButtonPlay }}
|
||||
</ui-btn>
|
||||
|
@ -66,11 +66,11 @@
|
|||
</ui-tooltip>
|
||||
<template v-if="userCanUpdate">
|
||||
<ui-tooltip :text="$strings.LabelEdit" direction="bottom">
|
||||
<ui-icon-btn :disabled="processingBatch" icon="edit" bg-color="warning" class="mx-1.5" @click="batchEditClick" />
|
||||
<ui-icon-btn :disabled="processingBatch" icon="edit" bg-color="bg-warning" class="mx-1.5" @click="batchEditClick" />
|
||||
</ui-tooltip>
|
||||
</template>
|
||||
<ui-tooltip v-if="userCanDelete" :text="$strings.ButtonRemove" direction="bottom">
|
||||
<ui-icon-btn :disabled="processingBatch" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" />
|
||||
<ui-icon-btn :disabled="processingBatch" icon="delete" bg-color="bg-error" class="mx-1.5" @click="batchDeleteClick" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-context-menu-dropdown v-if="contextMenuItems.length && !processingBatch" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" />
|
||||
|
@ -180,6 +180,15 @@ export default {
|
|||
action: 'rescan'
|
||||
})
|
||||
|
||||
// The limit of 50 is introduced because of the URL length. Each id has 36 chars, so 36 * 40 = 1440
|
||||
// + 40 , separators = 1480 chars + base path 280 chars = 1760 chars. This keeps the URL under 2000 chars even with longer domains
|
||||
if (this.selectedMediaItems.length <= 40) {
|
||||
options.push({
|
||||
text: this.$strings.LabelDownload,
|
||||
action: 'download'
|
||||
})
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
},
|
||||
|
@ -215,6 +224,8 @@ export default {
|
|||
this.batchAutoMatchClick()
|
||||
} else if (action === 'rescan') {
|
||||
this.batchRescan()
|
||||
} else if (action === 'download') {
|
||||
this.batchDownload()
|
||||
}
|
||||
},
|
||||
async batchRescan() {
|
||||
|
@ -241,6 +252,11 @@ export default {
|
|||
}
|
||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||
},
|
||||
async batchDownload() {
|
||||
const libraryItemIds = this.selectedMediaItems.map((i) => i.id)
|
||||
console.log('Downloading library items', libraryItemIds)
|
||||
this.$downloadFile(`/api/libraries/${this.$store.state.libraries.currentLibraryId}/download?token=${this.$store.getters['user/getToken']}&ids=${libraryItemIds.join(',')}`)
|
||||
},
|
||||
async playSelectedItems() {
|
||||
this.$store.commit('setProcessingBatch', true)
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
<div v-if="loaded && !shelves.length && !search" class="w-full flex flex-col items-center justify-center py-12">
|
||||
<p class="text-center text-2xl mb-4 py-4">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p>
|
||||
<div v-if="userIsAdminOrUp" class="flex">
|
||||
<ui-btn to="/config" color="primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn>
|
||||
<ui-btn color="success" class="w-52" :loading="isScanningLibrary || tempIsScanning" @click="scan">{{ $strings.ButtonScanLibrary }}</ui-btn>
|
||||
<ui-btn to="/config" color="bg-primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn>
|
||||
<ui-btn color="bg-success" class="w-52" :loading="isScanningLibrary || tempIsScanning" @click="scan">{{ $strings.ButtonScanLibrary }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="loaded && !shelves.length && search" class="w-full h-40 flex items-center justify-center">
|
||||
|
@ -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) {
|
||||
shelves.push({
|
||||
id: 'series',
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
</div>
|
||||
<div class="relative">
|
||||
<div class="relative text-center categoryPlacard transform z-30 top-0 left-4e md:left-8e w-44e rounded-md">
|
||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em 0.5em` }">
|
||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-xs border" :style="{ padding: `0em 0.5em` }">
|
||||
<h2 :style="{ fontSize: 0.9 + 'em' }">{{ $strings[shelf.labelStringKey] }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,36 +1,36 @@
|
|||
<template>
|
||||
<div class="w-full h-20 md:h-10 relative">
|
||||
<div class="flex md:hidden h-10 items-center">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}`" class="flex-grow h-full flex justify-center items-center" :class="isHomePage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-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>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
</nuxt-link>
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="flex-grow h-full flex justify-center items-center" :class="isLibraryPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-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>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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 v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-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>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="flex-grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-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>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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 v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="flex-grow h-full flex justify-center items-center" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<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>
|
||||
<span v-else class="material-symbols text-lg"></span>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary/80' : 'bg-primary/40'">
|
||||
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
|
||||
<span v-else class="material-symbols text-lg"></span>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<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>
|
||||
<svg v-else class="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
|
@ -39,10 +39,10 @@
|
|||
/>
|
||||
</svg>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary/80' : 'bg-primary/40'">
|
||||
<p class="text-sm">{{ $strings.ButtonAdd }}</p>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="grow h-full flex justify-center items-center" :class="isPodcastDownloadQueuePage ? 'bg-primary/80' : 'bg-primary/40'">
|
||||
<p class="text-sm">{{ $strings.ButtonDownloadQueue }}</p>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
|
@ -52,14 +52,14 @@
|
|||
<p class="pl-2 text-base md:text-lg">
|
||||
{{ seriesName }}
|
||||
</p>
|
||||
<div class="w-6 h-6 rounded-full bg-black bg-opacity-30 flex items-center justify-center ml-3">
|
||||
<div class="w-6 h-6 rounded-full bg-black/30 flex items-center justify-center ml-3">
|
||||
<span class="font-mono">{{ $formatNumber(numShowing) }}</span>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div class="grow" />
|
||||
|
||||
<!-- RSS feed -->
|
||||
<ui-tooltip v-if="seriesRssFeed" :text="$strings.LabelOpenRSSFeed" direction="top">
|
||||
<ui-icon-btn icon="rss_feed" class="mx-0.5" :size="7" icon-font-size="1.2rem" bg-color="success" outlined @click="showOpenSeriesRSSFeed" />
|
||||
<ui-icon-btn icon="rss_feed" class="mx-0.5" :size="7" icon-font-size="1.2rem" bg-color="bg-success" outlined @click="showOpenSeriesRSSFeed" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-context-menu-dropdown v-if="!isBatchSelecting && seriesContextMenuItems.length" :items="seriesContextMenuItems" class="mx-px" @action="seriesContextMenuAction" />
|
||||
|
@ -68,7 +68,7 @@
|
|||
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome && !isAuthorsPage">
|
||||
<p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p>
|
||||
|
||||
<div class="flex-grow hidden sm:inline-block" />
|
||||
<div class="grow hidden sm:inline-block" />
|
||||
|
||||
<!-- library filter select -->
|
||||
<controls-library-filter-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
|
||||
|
@ -83,30 +83,30 @@
|
|||
<controls-sort-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesSortBy" :descending.sync="settings.seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" />
|
||||
|
||||
<!-- issues page remove all button -->
|
||||
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ $formatNumber(numShowing) }} {{ entityName }}</ui-btn>
|
||||
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="bg-error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ $formatNumber(numShowing) }} {{ entityName }}</ui-btn>
|
||||
|
||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
|
||||
</template>
|
||||
<!-- search page -->
|
||||
<template v-else-if="page === 'search'">
|
||||
<div class="flex-grow" />
|
||||
<div class="grow" />
|
||||
<p>{{ $strings.MessageSearchResultsFor }} "{{ searchQuery }}"</p>
|
||||
<div class="flex-grow" />
|
||||
<div class="grow" />
|
||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
|
||||
</template>
|
||||
<!-- authors page -->
|
||||
<template v-else-if="isAuthorsPage">
|
||||
<p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p>
|
||||
|
||||
<div class="flex-grow hidden sm:inline-block" />
|
||||
<ui-btn v-if="userCanUpdate && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
|
||||
<div class="grow hidden sm:inline-block" />
|
||||
<ui-btn v-if="userCanUpdate && !isBatchSelecting" :loading="processingAuthors" color="bg-primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
|
||||
|
||||
<!-- author sort select -->
|
||||
<controls-sort-select v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" />
|
||||
</template>
|
||||
<!-- home page -->
|
||||
<template v-else-if="isHome">
|
||||
<div class="flex-grow" />
|
||||
<div class="grow" />
|
||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
|
||||
</template>
|
||||
</div>
|
||||
|
@ -274,15 +274,10 @@ export default {
|
|||
isAuthorsPage() {
|
||||
return this.page === 'authors'
|
||||
},
|
||||
isAlbumsPage() {
|
||||
return this.page === 'albums'
|
||||
},
|
||||
numShowing() {
|
||||
return this.totalEntities
|
||||
},
|
||||
entityName() {
|
||||
if (this.isAlbumsPage) return 'Albums'
|
||||
|
||||
if (this.isPodcastLibrary) return this.$strings.LabelPodcasts
|
||||
if (!this.page) return this.$strings.LabelBooks
|
||||
if (this.isSeriesPage) return this.$strings.LabelSeries
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div role="toolbar" aria-orientation="vertical" aria-label="Config Sidebar">
|
||||
<div role="navigation" aria-label="Config Navigation" class="w-44 fixed left-0 top-16 bg-bg bg-opacity-100 md:bg-opacity-70 shadow-lg border-r border-white border-opacity-5 py-3 transform transition-transform mb-12 overflow-y-auto" :class="wrapperClass + ' ' + (streamLibraryItem ? 'h-[calc(100%-270px)]' : 'h-[calc(100%-110px)]')" v-click-outside="clickOutside">
|
||||
<div role="navigation" aria-label="Config Navigation" class="w-44 fixed left-0 top-16 bg-bg/100 md:bg-bg/70 shadow-lg border-r border-white/5 py-3 transform transition-transform mb-12 overflow-y-auto" :class="wrapperClass + ' ' + (streamLibraryItem ? 'h-[calc(100%-270px)]' : 'h-[calc(100%-110px)]')" v-click-outside="clickOutside">
|
||||
<div v-show="isMobilePortrait" class="flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer">
|
||||
<span class="material-symbols text-2xl">arrow_back</span>
|
||||
</div>
|
||||
|
||||
<nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-3 h-12 border-b border-primary border-opacity-30 flex items-center cursor-pointer relative" :class="routeName === route.id ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
|
||||
<nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-3 h-12 border-b border-primary/30 flex items-center cursor-pointer relative" :class="routeName === route.id ? 'bg-primary/70' : 'hover:bg-primary/30'">
|
||||
<p class="leading-4">{{ route.title }}</p>
|
||||
<div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
@ -13,7 +13,7 @@
|
|||
<modals-changelog-view-modal v-model="showChangelogModal" :versionData="versionData" />
|
||||
</div>
|
||||
|
||||
<div class="w-44 h-12 px-4 border-t bg-bg border-black border-opacity-20 fixed left-0 flex flex-col justify-center" :class="wrapperClass" :style="{ bottom: streamLibraryItem ? '160px' : '0px' }">
|
||||
<div class="w-44 h-12 px-4 border-t bg-bg border-black/20 fixed left-0 flex flex-col justify-center" :class="wrapperClass" :style="{ bottom: streamLibraryItem ? '160px' : '0px' }">
|
||||
<div class="flex items-center justify-between">
|
||||
<button type="button" class="underline font-mono text-sm" @click="clickChangelog">v{{ $config.version }}</button>
|
||||
|
||||
|
@ -70,6 +70,11 @@ export default {
|
|||
title: this.$strings.HeaderUsers,
|
||||
path: '/config/users'
|
||||
},
|
||||
{
|
||||
id: 'config-api-keys',
|
||||
title: this.$strings.HeaderApiKeys,
|
||||
path: '/config/api-keys'
|
||||
},
|
||||
{
|
||||
id: 'config-sessions',
|
||||
title: this.$strings.HeaderListeningSessions,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div :key="shelf" :id="`shelf-${shelf - 1}`" class="w-full px-4e sm:px-8e relative" :class="{ bookshelfRow: !isAlternativeBookshelfView }" :style="{ height: shelfHeight + 'px' }">
|
||||
<!-- Card skeletons -->
|
||||
<template v-for="entityIndex in entitiesInShelf(shelf)">
|
||||
<div :key="entityIndex" class="w-full h-full absolute rounded z-5 top-0 left-0 bg-primary box-shadow-book" :style="{ transform: entityTransform(entityIndex), width: cardWidth + 'px', height: coverHeight + 'px' }" />
|
||||
<div :key="entityIndex" class="w-full h-full absolute rounded-sm z-5 top-0 left-0 bg-primary box-shadow-book" :style="{ transform: entityTransform(entityIndex), width: cardWidth + 'px', height: coverHeight + 'px' }" />
|
||||
</template>
|
||||
<div v-if="!isAlternativeBookshelfView" class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-20 h-6e" />
|
||||
</div>
|
||||
|
@ -13,8 +13,8 @@
|
|||
<div v-if="initialized && !totalShelves && !hasFilter && entityName === 'items'" class="w-full flex flex-col items-center justify-center py-12">
|
||||
<p class="text-center text-2xl mb-4 py-4">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p>
|
||||
<div v-if="userIsAdminOrUp" class="flex">
|
||||
<ui-btn to="/config" color="primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn>
|
||||
<ui-btn color="success" class="w-52" :loading="isScanningLibrary || tempIsScanning" @click="scan">{{ $strings.ButtonScanLibrary }}</ui-btn>
|
||||
<ui-btn to="/config" color="bg-primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn>
|
||||
<ui-btn color="bg-success" class="w-52" :loading="isScanningLibrary || tempIsScanning" @click="scan">{{ $strings.ButtonScanLibrary }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="!totalShelves && initialized" class="w-full py-16">
|
||||
|
@ -29,7 +29,7 @@
|
|||
</div>
|
||||
<!-- Clear filter only available on Library bookshelf -->
|
||||
<div v-if="entityName === 'items'" class="flex justify-center mt-2">
|
||||
<ui-btn v-if="hasFilter" color="primary" @click="clearFilter">{{ $strings.ButtonClearFilter }}</ui-btn>
|
||||
<ui-btn v-if="hasFilter" color="bg-primary" @click="clearFilter">{{ $strings.ButtonClearFilter }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -778,10 +778,6 @@ export default {
|
|||
windowResize() {
|
||||
this.executeRebuild()
|
||||
},
|
||||
socketInit() {
|
||||
// Server settings are set on socket init
|
||||
this.executeRebuild()
|
||||
},
|
||||
initListeners() {
|
||||
window.addEventListener('resize', this.windowResize)
|
||||
|
||||
|
@ -794,7 +790,6 @@ export default {
|
|||
})
|
||||
|
||||
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||
this.$eventBus.$on('socket_init', this.socketInit)
|
||||
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||
|
||||
if (this.$root.socket) {
|
||||
|
@ -826,7 +821,6 @@ export default {
|
|||
}
|
||||
|
||||
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||
this.$eventBus.$off('socket_init', this.socketInit)
|
||||
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||
|
||||
if (this.$root.socket) {
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<p class="font-mono text-xs sm:text-sm pl-1 sm:pl-1.5 pb-px">{{ totalDurationPretty }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div class="grow" />
|
||||
<ui-tooltip direction="top" :text="$strings.LabelClosePlayer">
|
||||
<button :aria-label="$strings.LabelClosePlayer" class="material-symbols sm:px-2 py-1 lg:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</button>
|
||||
</ui-tooltip>
|
||||
|
@ -156,7 +156,7 @@ export default {
|
|||
return this.mediaMetadata.authors || []
|
||||
},
|
||||
libraryId() {
|
||||
return this.streamLibraryItem ? this.streamLibraryItem.libraryId : null
|
||||
return this.streamLibraryItem?.libraryId || null
|
||||
},
|
||||
totalDurationPretty() {
|
||||
// Adjusted by playback rate
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-2 sm:p-4 mb-8">
|
||||
<div class="bg-bg rounded-md shadow-lg border border-white/5 p-2 sm:p-4 mb-8">
|
||||
<div class="flex items-center mb-2">
|
||||
<slot name="header-prefix"></slot>
|
||||
<h1 class="text-xl">{{ headerText }}</h1>
|
||||
|
@ -39,4 +39,4 @@ export default {
|
|||
color: white;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div v-if="isShowingBookshelfToolbar" class="absolute top-0 -right-4 w-4 bg-bg h-10 pointer-events-none" />
|
||||
|
||||
<div id="siderail-buttons-container" role="navigation" aria-label="Library Navigation" :class="{ 'player-open': streamLibraryItem }" class="w-full overflow-y-auto overflow-x-hidden">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="homePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary/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">
|
||||
<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>
|
||||
|
@ -14,7 +14,7 @@
|
|||
<div v-show="homePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
|
||||
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p>
|
||||
|
@ -22,7 +22,7 @@
|
|||
<div v-show="isPodcastLatestPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</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 border-opacity-70 hover:bg-primary cursor-pointer relative" :class="showLibrary ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-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">
|
||||
<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>
|
||||
|
@ -32,7 +32,7 @@
|
|||
<div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</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 text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-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">
|
||||
<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>
|
||||
|
@ -42,7 +42,7 @@
|
|||
<div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" 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="paramId === 'collections' ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
|
||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
|
||||
|
@ -50,7 +50,7 @@
|
|||
<div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" 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="isPlaylistsPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<span class="material-symbols text-2.5xl"></span>
|
||||
|
||||
<p class="pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
|
||||
|
@ -58,7 +58,7 @@
|
|||
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<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">
|
||||
<path
|
||||
fill="currentColor"
|
||||
|
@ -71,7 +71,7 @@
|
|||
<div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/narrators`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isNarratorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/narrators`" 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="isNarratorsPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
|
||||
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.LabelNarrators }}</p>
|
||||
|
@ -79,7 +79,7 @@
|
|||
<div v-show="isNarratorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/stats`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isStatsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<nuxt-link v-if="isBookLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/stats`" 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="isStatsPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
|
||||
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonStats }}</p>
|
||||
|
@ -87,7 +87,7 @@
|
|||
<div v-show="isStatsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<span class="abs-icons icon-podcast text-xl"></span>
|
||||
|
||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAdd }}</p>
|
||||
|
@ -95,7 +95,7 @@
|
|||
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
|
||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonDownloadQueue }}</p>
|
||||
|
@ -103,13 +103,13 @@
|
|||
<div v-show="isPodcastDownloadQueuePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : 'bg-error bg-opacity-20'">
|
||||
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-error/40 cursor-pointer relative" :class="showingIssues ? 'bg-error/40' : 'bg-error/20'">
|
||||
<span class="material-symbols text-2xl">warning</span>
|
||||
|
||||
<p class="pt-1.5 text-center leading-4" style="font-size: 1rem">{{ $strings.ButtonIssues }}</p>
|
||||
|
||||
<div v-show="showingIssues" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
<div class="absolute top-1 right-1 w-4 h-4 rounded-full bg-white bg-opacity-30 flex items-center justify-center">
|
||||
<div class="absolute top-1 right-1 w-4 h-4 rounded-full bg-white/30 flex items-center justify-center">
|
||||
<p class="text-xs font-mono pb-0.5">{{ numIssues }}</p>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<covers-author-image :author="author" />
|
||||
|
||||
<!-- Author name & num books overlay -->
|
||||
<div cy-id="textInline" v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1e bg-black bg-opacity-60 px-2e">
|
||||
<div cy-id="textInline" v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1e bg-black/60 px-2e">
|
||||
<p class="text-center font-semibold truncate" :style="{ fontSize: 0.75 + 'em' }">{{ name }}</p>
|
||||
<p class="text-center text-gray-200" :style="{ fontSize: 0.65 + 'em' }">{{ numBooks }} {{ $strings.LabelBooks }}</p>
|
||||
</div>
|
||||
|
@ -25,7 +25,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Loading spinner -->
|
||||
<div cy-id="spinner" v-show="searching" class="absolute top-0 left-0 z-10 w-full h-full bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div cy-id="spinner" v-show="searching" class="absolute top-0 left-0 z-10 w-full h-full bg-black/50 flex items-center justify-center">
|
||||
<widgets-loading-spinner size="" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -71,9 +71,6 @@ export default {
|
|||
coverHeight() {
|
||||
return this.cardHeight
|
||||
},
|
||||
userToken() {
|
||||
return this.store.getters['user/getToken']
|
||||
},
|
||||
_author() {
|
||||
return this.author || {}
|
||||
},
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div class="flex h-full px-1 overflow-hidden">
|
||||
<div class="overflow-hidden bg-primary rounded" style="height: 50px; width: 40px">
|
||||
<div class="overflow-hidden bg-primary rounded-sm" style="height: 50px; width: 40px">
|
||||
<covers-author-image :author="author" />
|
||||
</div>
|
||||
<div class="flex-grow px-2 authorSearchCardContent h-full">
|
||||
<div class="grow px-2 authorSearchCardContent h-full">
|
||||
<p class="truncate text-sm">{{ name }}</p>
|
||||
<p class="text-xs text-gray-400">{{ $getString('LabelXBooks', [numBooks]) }}</p>
|
||||
</div>
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
<template>
|
||||
<div v-if="book" class="w-full border-b border-gray-700 pb-2">
|
||||
<div class="flex py-1 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer" @click="selectMatch">
|
||||
<div class="flex py-1 hover:bg-gray-300/10 cursor-pointer" @click="selectMatch">
|
||||
<div class="min-w-12 max-w-12 md:min-w-20 md:max-w-20">
|
||||
<div class="w-full bg-primary">
|
||||
<img v-if="selectedCover" :src="selectedCover" class="h-full w-full object-contain" />
|
||||
<div v-else class="w-12 h-12 md:w-20 md:h-20 bg-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isPodcast" class="px-2 md:px-4 flex-grow">
|
||||
<div v-if="!isPodcast" class="px-2 md:px-4 grow">
|
||||
<div class="flex items-center">
|
||||
<h1 class="text-sm md:text-base">{{ book.title }}</h1>
|
||||
<div class="flex-grow" />
|
||||
<div class="grow" />
|
||||
<p class="text-sm md:text-base">{{ book.publishedYear }}</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 v-if="book.series?.length" class="flex py-1 -mx-1">
|
||||
<div v-for="(series, index) in book.series" :key="index" class="bg-white bg-opacity-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">
|
||||
{{ series.series }}<span v-if="series.sequence"> #{{ series.sequence }}</span>
|
||||
</p>
|
||||
|
@ -27,7 +27,7 @@
|
|||
<p class="text-gray-500 text-xs">{{ book.descriptionPlain }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="px-4 flex-grow">
|
||||
<div v-else class="px-4 grow">
|
||||
<h1>
|
||||
<div class="flex items-center">{{ book.title }}<widgets-explicit-indicator v-if="book.explicit" /></div>
|
||||
</h1>
|
||||
|
|
|
@ -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>
|
|
@ -3,7 +3,7 @@
|
|||
<div class="w-10 h-10 flex items-center justify-center">
|
||||
<span class="material-symbols text-2xl text-gray-200">category</span>
|
||||
</div>
|
||||
<div class="flex-grow px-2 tagSearchCardContent h-full">
|
||||
<div class="grow px-2 tagSearchCardContent h-full">
|
||||
<p class="truncate text-sm">{{ genre }}</p>
|
||||
<p class="text-xs text-gray-400">{{ $getString('LabelXItems', [numItems]) }}</p>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div class="relative">
|
||||
<div class="rounded-sm h-full relative" :style="{ width: cardWidth + 'px', height: cardHeight + 'px' }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard">
|
||||
<div class="rounded-xs h-full relative" :style="{ width: cardWidth + 'px', height: cardHeight + 'px' }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard">
|
||||
<nuxt-link :to="groupTo" class="cursor-pointer">
|
||||
<div class="w-full h-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'">
|
||||
<covers-group-cover ref="groupcover" :id="groupEncode" :name="groupName" :type="groupType" :book-items="bookItems" :width="cardWidth" :height="cardHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
|
||||
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
||||
<div v-if="hasValidCovers" class="bg-black/60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
||||
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ groupName }}</p>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<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="flex-grow px-2 audiobookSearchCardContent">
|
||||
<div class="grow px-2 audiobookSearchCardContent">
|
||||
<p class="truncate text-sm">{{ title }}</p>
|
||||
<p v-if="subtitle" class="truncate text-xs text-gray-300">{{ subtitle }}</p>
|
||||
<p class="text-xs text-gray-200 truncate">{{ $getString('LabelByAuthor', [authorName]) }}</p>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<span v-if="isFinished" :class="taskIconStatus" class="material-symbols text-base">{{ actionIcon }}</span>
|
||||
<widgets-loading-spinner v-else />
|
||||
</div>
|
||||
<div class="flex-grow px-2 taskRunningCardContent">
|
||||
<div class="grow px-2 taskRunningCardContent">
|
||||
<p class="truncate text-sm">{{ title }}</p>
|
||||
|
||||
<p class="truncate text-xs text-gray-300">{{ description }}</p>
|
||||
|
@ -13,7 +13,7 @@
|
|||
<p v-if="isFailed && failedMessage" class="text-xs truncate text-red-500">{{ failedMessage }}</p>
|
||||
<p v-else-if="!isFinished && cancelingScan" class="text-xs truncate">Canceling...</p>
|
||||
</div>
|
||||
<ui-btn v-if="userIsAdminOrUp && !isFinished && isLibraryScan && !cancelingScan" color="primary" :padding-y="1" :padding-x="1" class="text-xs w-16 max-w-16 truncate mr-1" @click.stop="cancelScan">{{ this.$strings.ButtonCancel }}</ui-btn>
|
||||
<ui-btn v-if="userIsAdminOrUp && !isFinished && isLibraryScan && !cancelingScan" color="bg-primary" :padding-y="1" :padding-x="1" class="text-xs w-16 max-w-16 truncate mr-1" @click.stop="cancelScan">{{ this.$strings.ButtonCancel }}</ui-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div class="relative w-full py-4 px-6 border border-white border-opacity-10 shadow-lg rounded-md my-6">
|
||||
<div class="absolute -top-3 -left-3 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full">
|
||||
<p class="text-base text-white text-opacity-80 font-mono">#{{ item.index }}</p>
|
||||
<div class="relative w-full py-4 px-6 border border-white/10 shadow-lg rounded-md my-6">
|
||||
<div class="absolute -top-3 -left-3 w-8 h-8 bg-bg border border-white/10 flex items-center justify-center rounded-full">
|
||||
<p class="text-base text-white/80 font-mono">#{{ item.index }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="!processing && !uploadFailed && !uploadSuccess" class="absolute -top-3 -right-3 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-error cursor-pointer" @click="$emit('remove')">
|
||||
<span class="text-base text-white text-opacity-80 font-mono material-symbols">close</span>
|
||||
<div v-if="!processing && !uploadFailed && !uploadSuccess" class="absolute -top-3 -right-3 w-8 h-8 bg-bg border border-white/10 flex items-center justify-center rounded-full hover:bg-error cursor-pointer" @click="$emit('remove')">
|
||||
<span class="text-base text-white/80 font-mono material-symbols">close</span>
|
||||
</div>
|
||||
|
||||
<template v-if="!uploadSuccess && !uploadFailed">
|
||||
|
@ -20,10 +20,10 @@
|
|||
<div class="w-1/2 px-2">
|
||||
<div v-if="!isPodcast" class="flex items-end">
|
||||
<ui-text-input-with-label v-model.trim="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
|
||||
<ui-tooltip :text="$strings.LabelUploaderItemFetchMetadataHelp">
|
||||
<div class="ml-2 mb-1 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-primary cursor-pointer" @click="fetchMetadata">
|
||||
<span class="text-base text-white text-opacity-80 font-mono material-symbols">sync</span>
|
||||
</div>
|
||||
<ui-tooltip direction="top" :text="$strings.LabelUploaderItemFetchMetadataHelp">
|
||||
<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>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<div v-else class="w-full">
|
||||
|
@ -61,7 +61,7 @@
|
|||
<p class="text-base">"{{ itemData.title }}" {{ $strings.MessageUploaderItemFailed }}</p>
|
||||
</widgets-alert>
|
||||
|
||||
<div v-if="isNonInteractable" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20">
|
||||
<div v-if="isNonInteractable" class="absolute top-0 left-0 w-full h-full bg-black/50 flex items-center justify-center z-20">
|
||||
<ui-loading-indicator :text="nonInteractionLabel" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,142 +0,0 @@
|
|||
<template>
|
||||
<div ref="card" :id="`album-card-${index}`" :style="{ width: cardWidth + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div class="relative" :style="{ height: coverHeight + 'px' }">
|
||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||
<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-sm 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>
|
|
@ -1,19 +1,19 @@
|
|||
<template>
|
||||
<div ref="card" :id="`book-card-${index}`" tabindex="0" :style="{ minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }" class="absolute rounded-sm z-10 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div :id="`cover-area-${index}`" class="relative w-full top-0 left-0 rounded overflow-hidden z-10 bg-primary box-shadow-book" :style="{ height: coverHeight + 'px ' }">
|
||||
<div ref="card" :id="`book-card-${index}`" tabindex="0" :style="{ minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }" class="absolute rounded-xs z-10 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div :id="`cover-area-${index}`" class="relative w-full top-0 left-0 rounded-sm overflow-hidden z-10 bg-primary box-shadow-book" :style="{ height: coverHeight + 'px ' }">
|
||||
<!-- When cover image does not fill -->
|
||||
<div cy-id="coverBg" v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
||||
<div cy-id="coverBg" v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-xs bg-primary">
|
||||
<div class="absolute cover-bg" ref="coverBg" />
|
||||
</div>
|
||||
|
||||
<div cy-id="seriesSequenceList" v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #78350f">
|
||||
<div cy-id="seriesSequenceList" v-if="seriesSequenceList" class="absolute rounded-lg bg-black/90 box-shadow-md z-20 text-right" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #78350f">
|
||||
<p :style="{ fontSize: 0.8 + 'em' }">#{{ seriesSequenceList }}</p>
|
||||
</div>
|
||||
<div cy-id="booksInSeries" v-else-if="booksInSeries" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #cd9d49dd">
|
||||
<div cy-id="booksInSeries" v-else-if="booksInSeries" class="absolute rounded-lg bg-black/90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #cd9d49dd">
|
||||
<p :style="{ fontSize: 0.8 + 'em' }">{{ booksInSeries }}</p>
|
||||
</div>
|
||||
|
||||
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
||||
<div class="w-full h-full absolute top-0 left-0 rounded-sm overflow-hidden z-10">
|
||||
<div cy-id="titleImageNotReady" v-show="libraryItem && !imageReady" aria-hidden="true" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: 0.5 + 'em' }">
|
||||
<p :style="{ fontSize: 0.8 + 'em' }" class="text-gray-300 text-center">{{ title }}</p>
|
||||
</div>
|
||||
|
@ -35,7 +35,7 @@
|
|||
<div cy-id="progressBar" v-if="!isPodcast || episodeProgress" class="absolute bottom-0 left-0 h-1e max-w-full z-20 rounded-b box-shadow-progressbar" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * userProgressPercent + 'px' }"></div>
|
||||
|
||||
<!-- Overlay is not shown if collapsing series in library -->
|
||||
<div cy-id="overlay" v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen) && !processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded md:block" :class="overlayWrapperClasslist">
|
||||
<div cy-id="overlay" v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen) && !processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded-sm md:block" :class="overlayWrapperClasslist">
|
||||
<div cy-id="playButton" v-show="showPlayButton" class="h-full flex items-center justify-center pointer-events-none">
|
||||
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="play">
|
||||
<span class="material-symbols fill" :style="{ fontSize: playIconFontSize + 'em' }">play_arrow</span>
|
||||
|
@ -68,12 +68,12 @@
|
|||
</div>
|
||||
|
||||
<!-- Processing/loading spinner overlay -->
|
||||
<div cy-id="loadingSpinner" v-if="processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 rounded flex items-center justify-center">
|
||||
<div cy-id="loadingSpinner" v-if="processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black/40 rounded-sm flex items-center justify-center">
|
||||
<widgets-loading-spinner size="la-lg" />
|
||||
</div>
|
||||
|
||||
<!-- Series name overlay -->
|
||||
<div cy-id="seriesNameOverlay" v-if="booksInSeries && libraryItem && isHovering" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-60 rounded flex items-center justify-center" :style="{ padding: 1 + 'em' }">
|
||||
<div cy-id="seriesNameOverlay" v-if="booksInSeries && libraryItem && isHovering" class="w-full h-full absolute top-0 left-0 z-10 bg-black/60 rounded-sm flex items-center justify-center" :style="{ padding: 1 + 'em' }">
|
||||
<p v-if="seriesName" class="text-gray-200 text-center" :style="{ fontSize: 1.1 + 'em' }">{{ seriesName }}</p>
|
||||
</div>
|
||||
|
||||
|
@ -94,19 +94,19 @@
|
|||
</div>
|
||||
|
||||
<!-- Series sequence -->
|
||||
<div cy-id="seriesSequence" v-if="seriesSequence && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-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="seriesSequence" v-if="seriesSequence && !isHovering && !isSelectionMode" 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' }">#{{ seriesSequence }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Podcast Episode # -->
|
||||
<div cy-id="podcastEpisodeNumber" v-if="recentEpisodeNumber !== null && !isHovering && !isSelectionMode && !processing" class="absolute rounded-lg bg-black bg-opacity-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' }">
|
||||
Episode<span v-if="recentEpisodeNumber"> #{{ recentEpisodeNumber }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Podcast Num Episodes -->
|
||||
<div cy-id="numEpisodes" v-else-if="!numEpisodesIncomplete && numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', width: 1.25 + 'em', height: 1.25 + 'em' }">
|
||||
<div cy-id="numEpisodes" v-else-if="!numEpisodesIncomplete && numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black/90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', width: 1.25 + 'em', height: 1.25 + 'em' }">
|
||||
<p :style="{ fontSize: 0.8 + 'em' }" role="status" :aria-label="$strings.LabelNumberOfEpisodes">{{ numEpisodes }}</p>
|
||||
</div>
|
||||
|
||||
|
@ -198,7 +198,7 @@ export default {
|
|||
return this.store.getters['user/getSizeMultiplier']
|
||||
},
|
||||
dateFormat() {
|
||||
return this.store.state.serverSettings.dateFormat
|
||||
return this.store.getters['getServerSetting']('dateFormat')
|
||||
},
|
||||
_libraryItem() {
|
||||
return this.libraryItem || {}
|
||||
|
@ -223,8 +223,7 @@ export default {
|
|||
return this.mediaMetadata.explicit || false
|
||||
},
|
||||
placeholderUrl() {
|
||||
const config = this.$config || this.$nuxt.$config
|
||||
return `${config.routerBasePath}/book_placeholder.jpg`
|
||||
return this.store.getters['globals/getPlaceholderCoverSrc']
|
||||
},
|
||||
bookCoverSrc() {
|
||||
return this.store.getters['globals/getLibraryItemCoverSrc'](this._libraryItem, this.placeholderUrl)
|
||||
|
@ -430,8 +429,8 @@ export default {
|
|||
},
|
||||
overlayWrapperClasslist() {
|
||||
const classes = []
|
||||
if (this.isSelectionMode) classes.push('bg-opacity-60')
|
||||
else classes.push('bg-opacity-40')
|
||||
if (this.isSelectionMode) classes.push('bg-black/60')
|
||||
else classes.push('bg-black/40')
|
||||
if (this.selected) {
|
||||
classes.push('border-2 border-yellow-400')
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<div ref="card" :id="`collection-card-${index}`" role="button" :style="{ width: cardWidth + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div ref="card" :id="`collection-card-${index}`" role="button" :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 overflow-hidden">
|
||||
<div class="w-full h-full bg-primary relative rounded-sm overflow-hidden">
|
||||
<covers-collection-cover ref="cover" :book-items="books" :width="cardWidth" :height="coverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</div>
|
||||
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
|
||||
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black/40 pointer-events-none">
|
||||
<div class="absolute pointer-events-auto" :style="{ top: 0.5 + 'em', right: 0.5 + 'em' }" @click.stop.prevent="clickEdit">
|
||||
<span class="material-symbols text-white text-opacity-75 hover:text-opacity-100" :style="{ fontSize: 1.25 + 'em' }">edit</span>
|
||||
<span class="material-symbols text-white/75 hover:text-white/100" :style="{ fontSize: 1.25 + 'em' }">edit</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
|||
</div>
|
||||
|
||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6e h-6e rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
|
||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em ${0.5}em` }">
|
||||
<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>
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
<template>
|
||||
<div ref="card" :id="`playlist-card-${index}`" role="button" :style="{ width: cardWidth + 'px', fontSize: sizeMultiplier + 'rem' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div ref="card" :id="`playlist-card-${index}`" role="button" :style="{ width: cardWidth + 'px', fontSize: sizeMultiplier + 'rem' }" 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 overflow-hidden">
|
||||
<div class="w-full h-full bg-primary relative rounded-sm overflow-hidden">
|
||||
<covers-playlist-cover ref="cover" :items="items" :width="cardWidth" :height="coverHeight" />
|
||||
</div>
|
||||
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
|
||||
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black/40 pointer-events-none">
|
||||
<div class="absolute pointer-events-auto" :style="{ top: 0.5 + 'em', right: 0.5 + 'em' }" @click.stop.prevent="clickEdit">
|
||||
<span class="material-symbols text-white text-opacity-75 hover:text-opacity-100" :style="{ fontSize: 1.25 + 'em' }">edit</span>
|
||||
<span class="material-symbols text-white/75 hover:text-white/100" :style="{ fontSize: 1.25 + 'em' }">edit</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 -bottom-6e left-0 right-0 mx-auto h-6e rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em ${0.5}em` }">
|
||||
<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>
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
<template>
|
||||
<div cy-id="card" ref="card" :id="`series-card-${index}`" tabindex="0" :style="{ width: cardWidth + 'px' }" class="absolute rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div cy-id="card" ref="card" :id="`series-card-${index}`" tabindex="0" :style="{ width: cardWidth + 'px' }" class="absolute rounded-xs z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div cy-id="covers-area" class="relative" :style="{ height: coverHeight + 'px' }">
|
||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
||||
<div class="w-full h-full bg-primary relative rounded-sm overflow-hidden z-0">
|
||||
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="cardWidth" :height="coverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</div>
|
||||
|
||||
<div cy-id="seriesLengthMarker" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #cd9d49dd">
|
||||
<div cy-id="seriesLengthMarker" class="absolute rounded-lg bg-black/90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #cd9d49dd">
|
||||
<p :style="{ fontSize: 0.8 + 'em' }" role="status" :aria-label="$strings.LabelNumberOfBooks">{{ books.length }}</p>
|
||||
</div>
|
||||
|
||||
<div cy-id="seriesProgressBar" v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1e shadow-sm max-w-full z-10 rounded-b w-full box-shadow-progressbar" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
|
||||
<div cy-id="seriesProgressBar" v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1e shadow-xs max-w-full z-10 rounded-b w-full box-shadow-progressbar" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
|
||||
|
||||
<div cy-id="hoveringDisplayTitle" v-if="hasValidCovers" aria-hidden="true" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: '1em' }">
|
||||
<div cy-id="hoveringDisplayTitle" v-if="hasValidCovers" aria-hidden="true" class="bg-black/60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: '1em' }">
|
||||
<p :style="{ fontSize: 1.2 + 'em' }">{{ displayTitle }}</p>
|
||||
</div>
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
|||
</div>
|
||||
|
||||
<div cy-id="standardBottomText" v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6e h-6e rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
|
||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em 0.5em` }">
|
||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-xs border" :style="{ padding: `0em 0.5em` }">
|
||||
<p cy-id="standardBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ displayTitle }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -71,7 +71,7 @@ export default {
|
|||
return this.height * this.sizeMultiplier
|
||||
},
|
||||
dateFormat() {
|
||||
return this.store.state.serverSettings.dateFormat
|
||||
return this.store.getters['getServerSetting']('dateFormat')
|
||||
},
|
||||
labelFontSize() {
|
||||
if (this.width < 160) return 0.75
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Narrator name & num books overlay -->
|
||||
<div class="absolute bottom-0 left-0 w-full py-1e bg-black bg-opacity-60 px-2e">
|
||||
<div class="absolute bottom-0 left-0 w-full py-1e bg-black/60 px-2e">
|
||||
<p cy-id="name" class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: 0.75 + 'em' }">{{ name }}</p>
|
||||
<p cy-id="numBooks" class="text-center text-gray-200" :style="{ fontSize: 0.65 + 'em' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="w-10 h-10 flex items-center justify-center">
|
||||
<span class="material-symbols text-2xl text-gray-200"></span>
|
||||
</div>
|
||||
<div class="flex-grow px-2 narratorSearchCardContent h-full">
|
||||
<div class="grow px-2 narratorSearchCardContent h-full">
|
||||
<p class="truncate text-sm">{{ narrator }}</p>
|
||||
<p class="text-xs text-gray-400">{{ $getString('LabelXBooks', [numBooks]) }}</p>
|
||||
</div>
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
<template>
|
||||
<div class="w-full border border-white border-opacity-10 rounded-xl p-4 my-2" :class="notification.enabled ? 'bg-primary bg-opacity-25' : 'bg-error bg-opacity-5'">
|
||||
<div class="w-full border border-white/10 rounded-xl p-4 my-2" :class="notification.enabled ? 'bg-primary/25' : 'bg-error/5'">
|
||||
<div class="flex flex-wrap items-center">
|
||||
<p class="text-base md:text-lg font-semibold pr-4">{{ eventName }}</p>
|
||||
<div class="flex-grow" />
|
||||
<div class="grow" />
|
||||
|
||||
<ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" @click.stop="fireTestEventAndSucceed">{{ this.$strings.ButtonFireOnTest }}</ui-btn>
|
||||
<ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" color="red-600" @click.stop="fireTestEventAndFail">{{ this.$strings.ButtonFireAndFail }}</ui-btn>
|
||||
<ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" color="bg-red-600" @click.stop="fireTestEventAndFail">{{ this.$strings.ButtonFireAndFail }}</ui-btn>
|
||||
<!-- <ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" @click.stop="rapidFireTestEvents">Rapid Fire</ui-btn> -->
|
||||
<ui-btn v-else-if="notification.enabled" :loading="sendingTest" small class="mr-2" @click.stop="sendTestClick">{{ this.$strings.ButtonTest }}</ui-btn>
|
||||
<ui-btn v-else :loading="enabling" small color="success" class="mr-2" @click="enableNotification">{{ this.$strings.ButtonEnable }}</ui-btn>
|
||||
<ui-btn v-else :loading="enabling" small color="bg-success" class="mr-2" @click="enableNotification">{{ this.$strings.ButtonEnable }}</ui-btn>
|
||||
|
||||
<ui-icon-btn :size="7" icon-font-size="1.1rem" icon="edit" class="mr-2" @click="editNotification" />
|
||||
<ui-icon-btn bg-color="error" :size="7" icon-font-size="1.2rem" icon="delete" @click="deleteNotificationClick" />
|
||||
<ui-icon-btn bg-color="bg-error" :size="7" icon-font-size="1.2rem" icon="delete" @click="deleteNotificationClick" />
|
||||
</div>
|
||||
<div class="pt-4">
|
||||
<p class="text-gray-300 text-xs md:text-sm mb-2">{{ notification.urls.join(', ') }}</p>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="wrapper" class="w-full p-2 border border-white border-opacity-10 rounded">
|
||||
<div ref="wrapper" class="w-full p-2 border border-white/10 rounded-sm">
|
||||
<div class="flex">
|
||||
<div class="w-16 min-w-16">
|
||||
<div class="w-full h-16 bg-primary">
|
||||
|
@ -7,7 +7,7 @@
|
|||
</div>
|
||||
<p class="text-gray-400 text-xxs pt-1 text-center">{{ numEpisodes }} {{ $strings.HeaderEpisodes }}</p>
|
||||
</div>
|
||||
<div class="flex-grow pl-2" :style="{ maxWidth: detailsWidth + 'px' }">
|
||||
<div class="grow pl-2" :style="{ maxWidth: detailsWidth + 'px' }">
|
||||
<p class="mb-1">{{ title }}</p>
|
||||
<p class="text-xs mb-1 text-gray-300">{{ author }}</p>
|
||||
<p class="text-xs mb-2 text-gray-200">{{ description }}</p>
|
||||
|
@ -68,4 +68,4 @@ export default {
|
|||
this.width = this.$refs.wrapper.clientWidth
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="flex h-full px-1 overflow-hidden">
|
||||
<covers-group-cover :name="name" :book-items="bookItems" :width="60" :height="60" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<div class="flex-grow px-2 seriesSearchCardContent h-full">
|
||||
<div class="grow px-2 seriesSearchCardContent h-full">
|
||||
<p class="truncate text-sm">{{ name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="w-10 h-10 flex items-center justify-center">
|
||||
<span class="material-symbols text-2xl text-gray-200">local_offer</span>
|
||||
</div>
|
||||
<div class="flex-grow px-2 tagSearchCardContent h-full">
|
||||
<div class="grow px-2 tagSearchCardContent h-full">
|
||||
<p class="truncate text-sm">{{ tag }}</p>
|
||||
<p class="text-xs text-gray-400">{{ $getString('LabelXItems', [numItems]) }}</p>
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="narrators?.length" class="flex py-0.5 mt-4">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNarrators }}</span>
|
||||
<div class="w-34 min-w-34 sm:w-34 sm:min-w-34 break-words">
|
||||
<span class="text-white/60 uppercase text-sm">{{ $strings.LabelNarrators }}</span>
|
||||
</div>
|
||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden text-ellipsis">
|
||||
<template v-for="(narrator, index) in narrators">
|
||||
<nuxt-link :key="narrator" :to="`/library/${libraryId}/bookshelf?filter=narrators.${$encode(narrator)}`" class="hover:underline">{{ narrator }}</nuxt-link
|
||||
><span :key="index" v-if="index < narrators.length - 1">, </span>
|
||||
|
@ -12,34 +12,34 @@
|
|||
</div>
|
||||
</div>
|
||||
<div v-if="publishedYear" role="paragraph" class="flex py-0.5">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublishYear }}</span>
|
||||
<div class="w-34 min-w-34 sm:w-34 sm:min-w-34 break-words">
|
||||
<span class="text-white/60 uppercase text-sm">{{ $strings.LabelPublishYear }}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ publishedYear }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="publisher" role="paragraph" class="flex py-0.5">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublisher }}</span>
|
||||
<div class="w-34 min-w-34 sm:w-34 sm:min-w-34 break-words">
|
||||
<span class="text-white/60 uppercase text-sm">{{ $strings.LabelPublisher }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=publishers.${$encode(publisher)}`" class="hover:underline">{{ publisher }}</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="podcastType" role="paragraph" class="flex py-0.5">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span>
|
||||
<div class="w-34 min-w-34 sm:w-34 sm:min-w-34 break-words">
|
||||
<span class="text-white/60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span>
|
||||
</div>
|
||||
<div class="capitalize">
|
||||
{{ podcastType }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex py-0.5" v-if="genres.length">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
|
||||
<div class="w-34 min-w-34 sm:w-34 sm:min-w-34 break-words">
|
||||
<span class="text-white/60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
|
||||
</div>
|
||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden text-ellipsis">
|
||||
<template v-for="(genre, index) in genres">
|
||||
<nuxt-link :key="genre" :to="`/library/${libraryId}/bookshelf?filter=genres.${$encode(genre)}`" class="hover:underline">{{ genre }}</nuxt-link
|
||||
><span :key="index" v-if="index < genres.length - 1">, </span>
|
||||
|
@ -47,10 +47,10 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="flex py-0.5" v-if="tags.length">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelTags }}</span>
|
||||
<div class="w-34 min-w-34 sm:w-34 sm:min-w-34 break-words">
|
||||
<span class="text-white/60 uppercase text-sm">{{ $strings.LabelTags }}</span>
|
||||
</div>
|
||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden text-ellipsis">
|
||||
<template v-for="(tag, index) in tags">
|
||||
<nuxt-link :key="tag" :to="`/library/${libraryId}/bookshelf?filter=tags.${$encode(tag)}`" class="hover:underline">{{ tag }}</nuxt-link
|
||||
><span :key="index" v-if="index < tags.length - 1">, </span>
|
||||
|
@ -58,24 +58,24 @@
|
|||
</div>
|
||||
</div>
|
||||
<div v-if="language" class="flex py-0.5">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelLanguage }}</span>
|
||||
<div class="w-34 min-w-34 sm:w-34 sm:min-w-34 break-words">
|
||||
<span class="text-white/60 uppercase text-sm">{{ $strings.LabelLanguage }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=languages.${$encode(language)}`" class="hover:underline">{{ language }}</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tracks.length || (isPodcast && totalPodcastDuration)" role="paragraph" class="flex py-0.5">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
||||
<div class="w-34 min-w-34 sm:w-34 sm:min-w-34 break-words">
|
||||
<span class="text-white/60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ durationPretty }}
|
||||
</div>
|
||||
</div>
|
||||
<div role="paragraph" class="flex py-0.5">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelSize }}</span>
|
||||
<div class="w-34 min-w-34 sm:w-34 sm:min-w-34 break-words">
|
||||
<span class="text-white/60 uppercase text-sm">{{ $strings.LabelSize }}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ sizePretty }}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
||||
<div class="relative h-9">
|
||||
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none cursor-pointer" aria-haspopup="menu" :aria-expanded="showMenu" @click.prevent="showMenu = !showMenu">
|
||||
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded-sm shadow-xs pl-3 pr-3 py-0 text-left focus:outline-hidden cursor-pointer" aria-haspopup="menu" :aria-expanded="showMenu" @click.prevent="showMenu = !showMenu">
|
||||
<span class="flex items-center justify-between">
|
||||
<span class="block truncate text-xs">{{ selectedText }}</span>
|
||||
</span>
|
||||
|
@ -16,7 +16,7 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div 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 text-sm ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<div 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 text-sm ring-1 ring-black/5 overflow-auto focus:outline-hidden">
|
||||
<ul class="h-full w-full" role="menu">
|
||||
<template v-for="item in items">
|
||||
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" @click="clickedOption(item)">
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<span v-else class="material-symbols" style="font-size: 1.2rem">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full max-w-64 sm:max-w-80 sm:w-80 bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu" @mousedown.stop.prevent>
|
||||
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full max-w-64 sm:max-w-80 sm:w-80 bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black/5 overflow-auto focus:outline-hidden sm:text-sm globalSearchMenu" @mousedown.stop.prevent>
|
||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<li v-if="isTyping" class="py-2 px-2">
|
||||
<p>{{ $strings.MessageThinking }}</p>
|
||||
|
@ -39,6 +39,15 @@
|
|||
</li>
|
||||
</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>
|
||||
<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">
|
||||
|
@ -100,6 +109,7 @@ export default {
|
|||
isFetching: false,
|
||||
search: null,
|
||||
podcastResults: [],
|
||||
episodeResults: [],
|
||||
bookResults: [],
|
||||
authorResults: [],
|
||||
seriesResults: [],
|
||||
|
@ -115,7 +125,7 @@ export default {
|
|||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
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: {
|
||||
|
@ -132,6 +142,7 @@ export default {
|
|||
this.search = null
|
||||
this.lastSearch = null
|
||||
this.podcastResults = []
|
||||
this.episodeResults = []
|
||||
this.bookResults = []
|
||||
this.authorResults = []
|
||||
this.seriesResults = []
|
||||
|
@ -175,6 +186,7 @@ export default {
|
|||
if (!this.isFetching) return
|
||||
|
||||
this.podcastResults = searchResults.podcast || []
|
||||
this.episodeResults = searchResults.episodes || []
|
||||
this.bookResults = searchResults.book || []
|
||||
this.authorResults = searchResults.authors || []
|
||||
this.seriesResults = searchResults.series || []
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
||||
<div class="relative h-7">
|
||||
<button type="button" class="relative w-full h-full bg-bg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="menu" :aria-expanded="showMenu" @click.prevent="showMenu = !showMenu">
|
||||
<button type="button" class="relative w-full h-full bg-bg border border-gray-500 hover:border-gray-400 rounded-sm shadow-xs pl-3 pr-3 py-0 text-left focus:outline-hidden sm:text-sm cursor-pointer" aria-haspopup="menu" :aria-expanded="showMenu" @click.prevent="showMenu = !showMenu">
|
||||
<span class="flex items-center justify-between">
|
||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||
</span>
|
||||
|
@ -16,7 +16,7 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm libraryFilterMenu">
|
||||
<div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black/5 overflow-auto focus:outline-hidden text-sm libraryFilterMenu">
|
||||
<ul v-show="!sublist" class="h-full w-full" role="menu">
|
||||
<template v-for="item in selectItems">
|
||||
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" :aria-haspopup="item.sublist ? '' : 'menu'" @click="clickedOption(item)">
|
||||
|
@ -94,6 +94,9 @@ export default {
|
|||
userIsAdminOrUp() {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
userCanAccessExplicitContent() {
|
||||
return this.$store.getters['user/getUserCanAccessExplicitContent']
|
||||
},
|
||||
libraryMediaType() {
|
||||
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||
},
|
||||
|
@ -239,6 +242,15 @@ export default {
|
|||
sublist: false
|
||||
}
|
||||
]
|
||||
|
||||
if (this.userCanAccessExplicitContent) {
|
||||
items.push({
|
||||
text: this.$strings.LabelExplicit,
|
||||
value: 'explicit',
|
||||
sublist: false
|
||||
})
|
||||
}
|
||||
|
||||
if (this.userIsAdminOrUp) {
|
||||
items.push({
|
||||
text: this.$strings.LabelShareOpen,
|
||||
|
@ -249,7 +261,7 @@ export default {
|
|||
return items
|
||||
},
|
||||
podcastItems() {
|
||||
return [
|
||||
const items = [
|
||||
{
|
||||
text: this.$strings.LabelAll,
|
||||
value: 'all'
|
||||
|
@ -276,8 +288,23 @@ export default {
|
|||
text: this.$strings.ButtonIssues,
|
||||
value: 'issues',
|
||||
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() {
|
||||
if (this.isSeries) return this.seriesItems
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
||||
<button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="menu" :aria-expanded="showMenu" @click.prevent="showMenu = !showMenu">
|
||||
<button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded-sm shadow-xs pl-3 pr-3 py-0 text-left focus:outline-hidden sm:text-sm cursor-pointer" aria-haspopup="menu" :aria-expanded="showMenu" @click.prevent="showMenu = !showMenu">
|
||||
<span class="flex items-center justify-between">
|
||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||
<span class="material-symbols text-lg text-yellow-400" :aria-label="descending ? $strings.LabelSortDescending : $strings.LabelSortAscending">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm" role="menu">
|
||||
<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">
|
||||
<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)">
|
||||
<div class="flex items-center">
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</div>
|
||||
<div class="flex items-center h-9 relative overflow-hidden rounded-lg" style="width: 220px">
|
||||
<template v-for="rate in rates">
|
||||
<div :key="rate" class="h-full border-black-300 w-11 cursor-pointer border rounded-sm" :class="value === rate ? 'bg-black-100' : 'hover:bg-black hover:bg-opacity-10'" style="min-width: 44px; max-width: 44px" @click="set(rate)">
|
||||
<div :key="rate" class="h-full border-black-300 w-11 cursor-pointer border rounded-xs" :class="value === rate ? 'bg-black-100' : 'hover:bg-black/10'" style="min-width: 44px; max-width: 44px" @click="set(rate)">
|
||||
<div class="w-full h-full flex justify-center items-center">
|
||||
<p class="text-xs text-center">{{ rate }}<span class="text-sm">x</span></p>
|
||||
</div>
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
||||
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none cursor-pointer" aria-haspopup="menu" :aria-expanded="showMenu" @click.prevent="showMenu = !showMenu">
|
||||
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded-sm shadow-xs pl-3 pr-3 py-0 text-left focus:outline-hidden cursor-pointer" aria-haspopup="menu" :aria-expanded="showMenu" @click.prevent="showMenu = !showMenu">
|
||||
<span class="flex items-center justify-between">
|
||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||
<span class="material-symbols text-lg text-yellow-400" :aria-label="descending ? $strings.LabelSortDescending : $strings.LabelSortAscending">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-80 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm" role="menu">
|
||||
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-80 rounded-md py-1 ring-1 ring-black/5 overflow-auto focus:outline-hidden text-sm" role="menu">
|
||||
<template v-for="item in items">
|
||||
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" @click="clickedOption(item.value)">
|
||||
<div class="flex items-center">
|
||||
|
@ -77,4 +77,4 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
<span class="material-symbols text-2xl sm:text-3xl">{{ volumeIcon }}</span>
|
||||
</button>
|
||||
<transition name="menux">
|
||||
<div v-show="isOpen" class="volumeMenu h-28 absolute bottom-2 w-6 py-2 bg-bg shadow-sm rounded-lg" style="top: -116px">
|
||||
<div v-show="isOpen" class="volumeMenu h-28 absolute bottom-2 w-6 py-2 bg-bg shadow-xs rounded-lg" style="top: -116px">
|
||||
<div ref="volumeTrack" class="w-1 h-full bg-gray-500 mx-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack">
|
||||
<div class="bg-gray-100 w-full absolute left-0 bottom-0 pointer-events-none rounded-full" :style="{ height: volume * trackHeight + 'px' }" />
|
||||
<div class="w-2.5 h-2.5 bg-white shadow-sm rounded-full absolute pointer-events-none" :class="isDragging ? 'transform scale-125 origin-center' : ''" :style="{ bottom: cursorBottom + 'px', left: '-3px' }" />
|
||||
<div class="w-2.5 h-2.5 bg-white shadow-xs rounded-full absolute pointer-events-none" :class="isDragging ? 'transform scale-125 origin-center' : ''" :style="{ bottom: cursorBottom + 'px', left: '-3px' }" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
|
|
@ -39,9 +39,6 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
_author() {
|
||||
return this.author || {}
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="relative rounded-sm overflow-hidden" :style="{ height: height + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }">
|
||||
<div class="relative rounded-xs overflow-hidden" :style="{ height: height + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }">
|
||||
<div class="w-full h-full relative bg-bg">
|
||||
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
||||
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-xs bg-primary">
|
||||
<div class="absolute cover-bg" ref="coverBg" />
|
||||
</div>
|
||||
|
||||
|
@ -96,8 +96,8 @@ export default {
|
|||
return this.author
|
||||
},
|
||||
placeholderUrl() {
|
||||
const config = this.$config || this.$nuxt.$config
|
||||
return `${config.routerBasePath}/book_placeholder.jpg`
|
||||
const store = this.$store || this.$nuxt.$store
|
||||
return store.getters['globals/getPlaceholderCoverSrc']
|
||||
},
|
||||
fullCoverUrl() {
|
||||
if (!this.libraryItem) return null
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
<template>
|
||||
<div class="relative rounded-sm overflow-hidden" :style="{ width: width + 'px', height: height + 'px' }">
|
||||
<!-- <div class="absolute top-0 left-0 w-full h-full rounded-sm overflow-hidden z-10">
|
||||
<div class="w-full h-full border border-white border-opacity-10" />
|
||||
<div class="relative rounded-xs overflow-hidden" :style="{ width: width + 'px', height: height + 'px' }">
|
||||
<!-- <div class="absolute top-0 left-0 w-full h-full rounded-xs overflow-hidden z-10">
|
||||
<div class="w-full h-full border border-white/10" />
|
||||
</div> -->
|
||||
|
||||
<div v-if="hasOwnCover" class="w-full h-full relative rounded-sm">
|
||||
<div v-if="hasOwnCover" class="w-full h-full relative rounded-xs">
|
||||
<div v-if="showCoverBg" class="bg-primary absolute top-0 left-0 w-full h-full">
|
||||
<div class="w-full h-full z-0" ref="coverBg" />
|
||||
</div>
|
||||
<img ref="cover" :src="fullCoverUrl" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0" :class="showCoverBg ? 'object-contain' : 'object-cover'" />
|
||||
</div>
|
||||
<div v-else-if="books.length" class="flex justify-center h-full relative bg-primary bg-opacity-95 rounded-sm">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||
<div v-else-if="books.length" class="flex justify-center h-full relative bg-primary/95 rounded-xs">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400/5" />
|
||||
|
||||
<covers-book-cover :library-item="books[0]" :width="width / 2" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<covers-book-cover v-if="books.length > 1" :library-item="books[1]" :width="width / 2" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</div>
|
||||
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-xs">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400/5" />
|
||||
|
||||
<p class="text-white text-opacity-60 text-center" :style="{ fontSize: Math.min(1, sizeMultiplier) + 'rem' }">Empty Collection</p>
|
||||
<p class="text-white/60 text-center" :style="{ fontSize: Math.min(1, sizeMultiplier) + 'rem' }">Empty Collection</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -62,4 +62,4 @@ export default {
|
|||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -109,7 +109,7 @@ export default {
|
|||
|
||||
if (showCoverBg) {
|
||||
var coverbgwrapper = document.createElement('div')
|
||||
coverbgwrapper.className = 'absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary'
|
||||
coverbgwrapper.className = 'absolute top-0 left-0 w-full h-full overflow-hidden rounded-xs bg-primary'
|
||||
|
||||
var coverbg = document.createElement('div')
|
||||
coverbg.className = 'absolute cover-bg'
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div class="relative rounded-sm overflow-hidden" :style="{ width: width + 'px', height: height + 'px' }">
|
||||
<div v-if="items.length" class="flex flex-wrap justify-center h-full relative bg-primary bg-opacity-95 rounded-sm">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||
<div class="relative rounded-xs overflow-hidden" :style="{ width: width + 'px', height: height + 'px' }">
|
||||
<div v-if="items.length" class="flex flex-wrap justify-center h-full relative bg-primary/95 rounded-xs">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400/5" />
|
||||
<covers-book-cover v-for="(li, index) in libraryItemCovers" :key="index" :library-item="li" :width="itemCoverWidth" :book-cover-aspect-ratio="1" />
|
||||
</div>
|
||||
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-xs">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400/5" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -48,4 +48,4 @@ export default {
|
|||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<div class="relative rounded-sm" :style="{ height: width * bookCoverAspectRatio + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }" @mouseover="isHovering = true" @mouseleave="isHovering = false">
|
||||
<div class="relative rounded-xs" :style="{ height: width * bookCoverAspectRatio + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }" @mouseover="isHovering = true" @mouseleave="isHovering = false">
|
||||
<div class="w-full h-full relative overflow-hidden">
|
||||
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
||||
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-xs bg-primary">
|
||||
<div class="absolute cover-bg" ref="coverBg" />
|
||||
</div>
|
||||
<img ref="cover" :src="cover" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0" :class="showCoverBg ? 'object-contain' : 'object-fill'" />
|
||||
|
||||
<a v-if="!imageFailed && showOpenNewTab && isHovering" :href="cover" @click.stop target="_blank" class="absolute bg-primary flex items-center justify-center shadow-sm rounded-full hover:scale-110 transform duration-100" :style="{ top: sizeMultiplier * 0.5 + 'rem', right: sizeMultiplier * 0.5 + 'rem', width: 2.5 * sizeMultiplier + 'rem', height: 2.5 * sizeMultiplier + 'rem' }">
|
||||
<a v-if="!imageFailed && showOpenNewTab && isHovering" :href="cover" @click.stop target="_blank" class="absolute bg-primary flex items-center justify-center shadow-xs rounded-full hover:scale-110 transform duration-100" :style="{ top: sizeMultiplier * 0.5 + 'rem', right: sizeMultiplier * 0.5 + 'rem', width: 2.5 * sizeMultiplier + 'rem', height: 2.5 * sizeMultiplier + 'rem' }">
|
||||
<span class="material-symbols" :style="{ fontSize: sizeMultiplier * 1.75 + 'rem' }">open_in_new</span>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -18,7 +18,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="!imageFailed && showResolution" class="absolute -bottom-5 left-0 right-0 mx-auto text-xs text-gray-300 text-center">{{ resolution }}</p>
|
||||
<p v-if="!imageFailed && showResolution && resolution" class="absolute -bottom-5 left-0 right-0 mx-auto text-xs text-gray-300 text-center">{{ resolution }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -65,11 +65,12 @@ export default {
|
|||
return 0.8 * this.sizeMultiplier
|
||||
},
|
||||
resolution() {
|
||||
if (!this.naturalWidth || !this.naturalHeight) return null
|
||||
return `${this.naturalWidth}×${this.naturalHeight}px`
|
||||
},
|
||||
placeholderUrl() {
|
||||
const config = this.$config || this.$nuxt.$config
|
||||
return `${config.routerBasePath}/book_placeholder.jpg`
|
||||
const store = this.$store || this.$nuxt.$store
|
||||
return store.getters['globals/getPlaceholderCoverSrc']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -120,10 +120,10 @@
|
|||
</div>
|
||||
|
||||
<div class="flex pt-4 px-2">
|
||||
<ui-btn v-if="hasOpenIDLink" small :loading="unlinkingFromOpenID" color="primary" type="button" class="mr-2" @click.stop="unlinkOpenID">{{ $strings.ButtonUnlinkOpenId }}</ui-btn>
|
||||
<ui-btn v-if="hasOpenIDLink" small :loading="unlinkingFromOpenID" color="bg-primary" type="button" class="mr-2" @click.stop="unlinkOpenID">{{ $strings.ButtonUnlinkOpenId }}</ui-btn>
|
||||
<ui-btn v-if="isEditingRoot" small class="flex items-center" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
<div class="grow" />
|
||||
<ui-btn color="bg-success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -309,9 +309,9 @@ export default {
|
|||
} else {
|
||||
console.log('Account updated', data.user)
|
||||
|
||||
if (data.user.id === this.user.id && data.user.token !== this.user.token) {
|
||||
console.log('Current user token was updated')
|
||||
this.$store.commit('user/setUserToken', data.user.token)
|
||||
if (data.user.id === this.user.id && data.user.accessToken !== this.user.accessToken) {
|
||||
console.log('Current user access token was updated')
|
||||
this.$store.commit('user/setAccessToken', data.user.accessToken)
|
||||
}
|
||||
|
||||
this.$toast.success(this.$strings.ToastAccountUpdateSuccess)
|
||||
|
@ -351,9 +351,6 @@ export default {
|
|||
this.$toast.error(errMsg || 'Failed to create account')
|
||||
})
|
||||
},
|
||||
toggleActive() {
|
||||
this.newUser.isActive = !this.newUser.isActive
|
||||
},
|
||||
userTypeUpdated(type) {
|
||||
this.newUser.permissions = {
|
||||
download: type !== 'guest',
|
||||
|
|
|
@ -23,8 +23,8 @@
|
|||
<ui-text-input-with-label v-model="newAuthHeaderValue" :label="$strings.LabelProviderAuthorizationValue" type="password" />
|
||||
</div>
|
||||
<div class="flex px-1 pt-4">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" type="submit">{{ $strings.ButtonAdd }}</ui-btn>
|
||||
<div class="grow" />
|
||||
<ui-btn color="bg-success" type="submit">{{ $strings.ButtonAdd }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
60
client/components/modals/ApiKeyCreatedModal.vue
Normal file
60
client/components/modals/ApiKeyCreatedModal.vue
Normal file
|
@ -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>
|
198
client/components/modals/ApiKeyModal.vue
Normal file
198
client/components/modals/ApiKeyModal.vue
Normal file
|
@ -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>
|
|
@ -7,7 +7,7 @@
|
|||
<ui-btn v-else-if="userIsAdminOrUp" small :loading="probingFile" class="ml-2" @click="getFFProbeData">{{ $strings.ButtonProbeAudioFile }}</ui-btn>
|
||||
</div>
|
||||
|
||||
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
|
||||
<div class="w-full h-px bg-white/10 my-4" />
|
||||
|
||||
<template v-if="!ffprobeData">
|
||||
<ui-text-input-with-label :value="metadata.path" readonly :label="$strings.LabelPath" class="mb-4 text-sm" />
|
||||
|
@ -75,7 +75,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
|
||||
<div class="w-full h-px bg-white/10 my-4" />
|
||||
|
||||
<p class="font-bold mb-2">{{ $strings.LabelMetaTags }}</p>
|
||||
|
||||
|
|
|
@ -32,11 +32,11 @@
|
|||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<div class="mt-4 pt-4 text-white text-opacity-80 border-t border-white border-opacity-5">
|
||||
<div class="mt-4 pt-4 text-white/80 border-t border-white/5">
|
||||
<div class="flex items-center px-4">
|
||||
<ui-btn type="button" @click="show = false">{{ $strings.ButtonCancel }}</ui-btn>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" @click="doBatchQuickMatch">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
<div class="grow" />
|
||||
<ui-btn color="bg-success" @click="doBatchQuickMatch">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -17,16 +17,16 @@
|
|||
|
||||
<div v-if="canCreateBookmark && !hideCreate" class="w-full border-t border-white/10">
|
||||
<form @submit.prevent="submitCreateBookmark">
|
||||
<div class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">
|
||||
<div class="flex px-4 py-2 items-center text-center border-b border-white/10 text-white/80">
|
||||
<div class="w-16 max-w-16 text-center">
|
||||
<p class="text-sm font-mono text-gray-400">
|
||||
{{ this.$secondsToTimestamp(currentTime / playbackRate) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-grow px-2">
|
||||
<div class="grow px-2">
|
||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full h-10" />
|
||||
</div>
|
||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-symbols text-2xl -mt-px">add</span></ui-btn>
|
||||
<ui-btn type="submit" color="bg-success" :padding-x="4" class="h-10"><span class="material-symbols text-2xl -mt-px">add</span></ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -79,10 +79,10 @@ export default {
|
|||
return !this.bookmarks.find((bm) => Math.abs(this.currentTime - bm.time) < 1)
|
||||
},
|
||||
dateFormat() {
|
||||
return this.$store.state.serverSettings.dateFormat
|
||||
return this.$store.getters['getServerSetting']('dateFormat')
|
||||
},
|
||||
timeFormat() {
|
||||
return this.$store.state.serverSettings.timeFormat
|
||||
return this.$store.getters['getServerSetting']('timeFormat')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
{{ chap.title }}
|
||||
</p>
|
||||
<span class="font-mono text-xxs sm:text-xs text-gray-400 pl-2 whitespace-nowrap">{{ $elapsedPrettyExtended((chap.end - chap.start) / _playbackRate) }}</span>
|
||||
<span class="flex-grow" />
|
||||
<span class="grow" />
|
||||
<span class="font-mono text-xs sm:text-sm text-gray-300">{{ $secondsToTimestamp(chap.start / _playbackRate) }}</span>
|
||||
|
||||
<div v-show="chap.id === currentChapterId" class="w-0.5 h-full absolute top-0 left-0 bg-yellow-400" />
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
</template>
|
||||
|
||||
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
||||
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
|
||||
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white/20" style="max-height: 75%" @click.stop>
|
||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<template v-for="item in items">
|
||||
<li :key="item.value" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" :class="selected === item.value ? 'bg-success bg-opacity-10' : ''" role="option" @click="clickedOption(item.value)">
|
||||
<li :key="item.value" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" :class="selected === item.value ? 'bg-success/10' : ''" role="option" @click="clickedOption(item.value)">
|
||||
<div class="relative flex items-center px-3">
|
||||
<p class="font-normal block truncate text-base text-white text-opacity-80">{{ item.text }}</p>
|
||||
<p class="font-normal block truncate text-base text-white/80">{{ item.text }}</p>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="wrapper" role="dialog" aria-modal="true" class="hidden absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 rounded-lg items-center justify-center" style="z-index: 61" @click="clickClose">
|
||||
<div ref="wrapper" role="dialog" aria-modal="true" class="hidden absolute top-0 left-0 w-full h-full bg-black/50 rounded-lg items-center justify-center" style="z-index: 61" @click="clickClose">
|
||||
<button type="button" class="absolute top-3 right-3 md:top-5 md:right-5 h-8 w-8 md:h-12 md:w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" aria-label="Close modal">
|
||||
<span class="material-symbols text-2xl md:text-4xl">close</span>
|
||||
</button>
|
||||
|
@ -7,13 +7,14 @@
|
|||
<form v-if="selectedSeries" @submit.prevent="submitSeriesForm">
|
||||
<div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop>
|
||||
<div class="flex">
|
||||
<div class="flex-grow p-1 min-w-48 sm:min-w-64 md:min-w-80">
|
||||
<div class="grow p-1 min-w-48 sm:min-w-64 md:min-w-80">
|
||||
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!isNewSeries" :label="$strings.LabelSeriesName" @input="seriesNameInputHandler" />
|
||||
</div>
|
||||
<div class="w-24 sm:w-28 md:w-40 p-1">
|
||||
<ui-text-input-with-label ref="sequenceInput" v-model="selectedSeries.sequence" :label="$strings.LabelSequence" />
|
||||
</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">
|
||||
<ui-btn type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
</div>
|
||||
|
@ -34,12 +35,17 @@ export default {
|
|||
existingSeriesNames: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
originalSeriesSequence: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
el: null,
|
||||
content: null
|
||||
content: null,
|
||||
error: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -85,10 +91,17 @@ export default {
|
|||
}
|
||||
},
|
||||
submitSeriesForm() {
|
||||
this.error = null
|
||||
|
||||
if (this.$refs.newSeriesSelect) {
|
||||
this.$refs.newSeriesSelect.blur()
|
||||
}
|
||||
|
||||
if (this.selectedSeries.sequence !== this.originalSeriesSequence && this.selectedSeries.sequence.includes(' ')) {
|
||||
this.error = this.$strings.MessageSeriesSequenceCannotContainSpaces
|
||||
return
|
||||
}
|
||||
|
||||
this.$emit('submit')
|
||||
},
|
||||
clickClose() {
|
||||
|
@ -100,6 +113,7 @@ export default {
|
|||
}
|
||||
},
|
||||
setShow() {
|
||||
this.error = null
|
||||
if (!this.el || !this.content) {
|
||||
this.init()
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<p v-if="_session.displayAuthor" class="text-xs text-gray-400 px-4">{{ $getString('LabelByAuthor', [_session.displayAuthor]) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
|
||||
<div class="w-full h-px bg-white/10 my-4" />
|
||||
|
||||
<div class="flex flex-wrap mb-4">
|
||||
<div class="w-full md:w-2/3">
|
||||
|
@ -99,8 +99,8 @@
|
|||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<ui-btn v-if="!isOpenSession && !isMediaItemShareSession" small color="error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn>
|
||||
<ui-btn v-else-if="!isMediaItemShareSession" small color="error" @click.stop="closeSessionClick">{{ $strings.ButtonCloseSession }}</ui-btn>
|
||||
<ui-btn v-if="!isOpenSession && !isMediaItemShareSession" small color="bg-error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn>
|
||||
<ui-btn v-else-if="!isMediaItemShareSession" small color="bg-error" @click.stop="closeSessionClick">{{ $strings.ButtonCloseSession }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
|
@ -159,10 +159,10 @@ export default {
|
|||
return 'Unknown'
|
||||
},
|
||||
dateFormat() {
|
||||
return this.$store.state.serverSettings.dateFormat
|
||||
return this.$store.getters['getServerSetting']('dateFormat')
|
||||
},
|
||||
timeFormat() {
|
||||
return this.$store.state.serverSettings.timeFormat
|
||||
return this.$store.getters['getServerSetting']('timeFormat')
|
||||
},
|
||||
isOpenSession() {
|
||||
return !!this._session.open
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<template>
|
||||
<div ref="wrapper" role="dialog" aria-modal="true" class="modal modal-bg w-full h-full fixed top-0 left-0 bg-primary items-center justify-center opacity-0 hidden" :class="`z-${zIndex} bg-opacity-${bgOpacity}`">
|
||||
<div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" />
|
||||
<div ref="wrapper" role="dialog" aria-modal="true" class="modal modal-bg w-full h-full fixed top-0 left-0 bg-primary items-center justify-center opacity-0 hidden" :class="`z-${zIndex} bg-primary/${bgOpacity}`">
|
||||
<div class="absolute top-0 left-0 right-0 w-full h-36 bg-linear-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" />
|
||||
|
||||
<button class="absolute top-4 right-4 landscape:top-4 landscape:right-4 md:portrait:top-5 md:portrait:right-5 lg:top-5 lg:right-5 inline-flex text-gray-200 hover:text-white" aria-label="Close modal" @click="clickClose">
|
||||
<span class="material-symbols text-2xl landscape:text-2xl md:portrait:text-4xl lg:text-4xl">close</span>
|
||||
</button>
|
||||
<slot name="outer" />
|
||||
<div ref="content" tabindex="0" style="min-width: 380px; min-height: 200px; max-width: 100vw" class="relative text-white outline-none" :style="{ height: modalHeight, width: modalWidth, marginTop: contentMarginTop + 'px' }" @mousedown="mousedownModal" @mouseup="mouseupModal" v-click-outside="clickBg">
|
||||
<div ref="content" tabindex="0" style="min-width: 380px; min-height: 200px; max-width: 100vw" class="relative text-white outline-hidden" :style="{ height: modalHeight, width: modalWidth, marginTop: contentMarginTop + 'px' }" @mousedown="mousedownModal" @mouseup="mouseupModal" v-click-outside="clickBg">
|
||||
<slot />
|
||||
<div v-if="processing" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-black bg-opacity-60 rounded-lg flex items-center justify-center">
|
||||
<div v-if="processing" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-black/60 rounded-lg flex items-center justify-center">
|
||||
<ui-loading-indicator />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -23,7 +23,7 @@ export default {
|
|||
processing: Boolean,
|
||||
persistent: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
default: false
|
||||
},
|
||||
width: {
|
||||
type: [String, Number],
|
||||
|
@ -99,7 +99,7 @@ export default {
|
|||
this.preventClickoutside = false
|
||||
return
|
||||
}
|
||||
if (this.processing && this.persistent) return
|
||||
if (this.processing || this.persistent) return
|
||||
if (ev.srcElement && ev.srcElement.classList.contains('modal-bg')) {
|
||||
this.show = false
|
||||
}
|
||||
|
@ -163,4 +163,4 @@ export default {
|
|||
this.$eventBus.$off('showing-prompt', this.showingPrompt)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<label class="px-1 text-sm font-semibold block">{{ $strings.LabelSlug }}</label>
|
||||
<ui-text-input v-model="newShareSlug" class="text-base h-10" />
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div class="grow" />
|
||||
<div class="w-full sm:w-80">
|
||||
<label class="px-1 text-sm font-semibold block">{{ $strings.LabelDuration }}</label>
|
||||
<div class="inline-flex items-center space-x-2">
|
||||
|
@ -60,9 +60,9 @@
|
|||
<p class="text-sm text-gray-300 py-1 px-1" v-html="$getString('MessageShareExpirationWillBe', [expirationDateString])" />
|
||||
</template>
|
||||
<div class="flex items-center pt-6">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn v-if="currentShare" color="error" small @click="deleteShare">{{ $strings.ButtonDelete }}</ui-btn>
|
||||
<ui-btn v-if="!currentShare" color="success" small @click="openShare">{{ $strings.ButtonShare }}</ui-btn>
|
||||
<div class="grow" />
|
||||
<ui-btn v-if="currentShare" color="bg-error" small @click="deleteShare">{{ $strings.ButtonDelete }}</ui-btn>
|
||||
<ui-btn v-if="!currentShare" color="bg-success" small @click="openShare">{{ $strings.ButtonShare }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
|
@ -144,7 +144,7 @@ export default {
|
|||
expirationDateString() {
|
||||
if (!this.expireDurationSeconds) return this.$strings.LabelPermanent
|
||||
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: {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
</template>
|
||||
<form class="flex items-center justify-center px-6 py-3" @submit.prevent="submitCustomTime">
|
||||
<ui-text-input v-model="customTime" type="number" step="any" min="0.1" :placeholder="$strings.LabelTimeInMinutes" class="w-48" />
|
||||
<ui-btn color="success" type="submit" :padding-x="0" class="h-9 w-18 flex items-center justify-center ml-1">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
<ui-btn color="bg-success" type="submit" :padding-x="0" class="h-9 w-18 flex items-center justify-center ml-1">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
</form>
|
||||
</div>
|
||||
<div v-if="timerSet" class="w-full p-4">
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
<div class="w-40 pr-2 pt-4" style="min-width: 160px">
|
||||
<ui-file-input ref="fileInput" @change="fileUploadSelected">Upload Cover</ui-file-input>
|
||||
</div>
|
||||
<form @submit.prevent="submitForm" class="flex flex-grow">
|
||||
<form @submit.prevent="submitForm" class="flex grow">
|
||||
<ui-text-input-with-label v-model="imageUrl" label="Cover Image URL" />
|
||||
<ui-btn color="success" type="submit" :padding-x="4" class="mt-5 ml-3 w-24">Update</ui-btn>
|
||||
<ui-btn color="bg-success" type="submit" :padding-x="4" class="mt-5 ml-3 w-24">Update</ui-btn>
|
||||
</form>
|
||||
</div>
|
||||
<div v-if="previewUpload" class="absolute top-0 left-0 w-full h-full z-10 bg-bg p-8">
|
||||
|
@ -18,7 +18,7 @@
|
|||
</div>
|
||||
<div class="absolute bottom-0 right-0 flex py-4 px-5">
|
||||
<ui-btn :disabled="processingUpload" class="mx-2" @click="resetCoverPreview">Clear</ui-btn>
|
||||
<ui-btn :loading="processingUpload" color="success" @click="submitCoverUpload">Upload</ui-btn>
|
||||
<ui-btn :loading="processingUpload" color="bg-success" @click="submitCoverUpload">Upload</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -15,10 +15,10 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<form @submit.prevent="submitUploadCover" class="flex flex-grow mb-2 p-2">
|
||||
<div class="grow">
|
||||
<form @submit.prevent="submitUploadCover" class="flex grow mb-2 p-2">
|
||||
<ui-text-input v-model="imageUrl" :placeholder="$strings.LabelImageURLFromTheWeb" class="h-9 w-full" />
|
||||
<ui-btn color="success" type="submit" :padding-x="4" :disabled="!imageUrl" class="ml-2 sm:ml-3 w-24 h-9">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
<ui-btn color="bg-success" type="submit" :padding-x="4" :disabled="!imageUrl" class="ml-2 sm:ml-3 w-24 h-9">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
</form>
|
||||
|
||||
<form v-if="author" @submit.prevent="submitForm">
|
||||
|
@ -26,7 +26,7 @@
|
|||
<div class="w-3/4 p-2">
|
||||
<ui-text-input-with-label v-model="authorCopy.name" :disabled="processing" :label="$strings.LabelName" />
|
||||
</div>
|
||||
<div class="flex-grow p-2">
|
||||
<div class="grow p-2">
|
||||
<ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -35,8 +35,8 @@
|
|||
</div>
|
||||
|
||||
<div class="flex pt-2 px-2">
|
||||
<ui-btn v-if="userCanDelete" small color="error" type="button" @click.stop="removeClick">{{ $strings.ButtonRemove }}</ui-btn>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn v-if="userCanDelete" small color="bg-error" type="button" @click.stop="removeClick">{{ $strings.ButtonRemove }}</ui-btn>
|
||||
<div class="grow" />
|
||||
<ui-btn type="button" class="mx-2" @click="searchAuthor">{{ $strings.ButtonQuickMatch }}</ui-btn>
|
||||
|
||||
<ui-btn type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
||||
|
|
|
@ -5,16 +5,16 @@
|
|||
{{ this.$secondsToTimestamp(bookmark.time / playbackRate) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-grow overflow-hidden px-2">
|
||||
<div class="grow overflow-hidden px-2">
|
||||
<template v-if="isEditing">
|
||||
<form @submit.prevent="submitUpdate">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-grow pr-2">
|
||||
<div class="grow pr-2">
|
||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full h-10" />
|
||||
</div>
|
||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-symbols text-2xl -mt-px">forward</span></ui-btn>
|
||||
<ui-btn type="submit" color="bg-success" :padding-x="4" class="h-10"><span class="material-symbols text-2xl -mt-px">forward</span></ui-btn>
|
||||
<div class="pl-2 flex items-center">
|
||||
<span class="material-symbols text-3xl text-white text-opacity-70 hover:text-opacity-95 cursor-pointer" @click.stop.prevent="cancelEditing">close</span>
|
||||
<span class="material-symbols text-3xl text-white/70 hover:text-white/95 cursor-pointer" @click.stop.prevent="cancelEditing">close</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -48,7 +48,7 @@ export default {
|
|||
computed: {
|
||||
wrapperClass() {
|
||||
var classes = []
|
||||
if (this.highlight) classes.push('bg-bg bg-opacity-60')
|
||||
if (this.highlight) classes.push('bg-bg/60')
|
||||
if (!this.isEditing) classes.push('cursor-pointer')
|
||||
return classes.join(' ')
|
||||
}
|
||||
|
@ -99,4 +99,4 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -40,7 +40,7 @@ export default {
|
|||
}
|
||||
},
|
||||
dateFormat() {
|
||||
return this.$store.state.serverSettings.dateFormat
|
||||
return this.$store.getters['getServerSetting']('dateFormat')
|
||||
},
|
||||
releasesToShow() {
|
||||
return this.versionData?.releasesToShow || []
|
||||
|
@ -62,6 +62,8 @@ since we don't have access to the actual elements in this component
|
|||
|
||||
2. v-deep allows these to take effect on the content passed in to the v-html in the div above
|
||||
*/
|
||||
@reference "tailwindcss";
|
||||
|
||||
.custom-text ::v-deep > h2 {
|
||||
@apply text-lg font-bold;
|
||||
}
|
||||
|
|
|
@ -33,13 +33,13 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full h-px bg-white bg-opacity-10" />
|
||||
<div class="w-full h-px bg-white/10" />
|
||||
<form @submit.prevent="submitCreateCollection">
|
||||
<div class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">
|
||||
<div class="flex-grow px-2">
|
||||
<div class="flex px-4 py-2 items-center text-center border-b border-white/10 text-white/80">
|
||||
<div class="grow px-2">
|
||||
<ui-text-input v-model="newCollectionName" :placeholder="$strings.PlaceholderNewCollection" class="w-full" />
|
||||
</div>
|
||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10">{{ $strings.ButtonCreate }}</ui-btn>
|
||||
<ui-btn type="submit" color="bg-success" :padding-x="4" class="h-10">{{ $strings.ButtonCreate }}</ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
<div class="w-20 max-w-20 text-center">
|
||||
<covers-collection-cover :book-items="books" :width="80" :height="40 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</div>
|
||||
<div class="flex-grow overflow-hidden px-2">
|
||||
<div class="grow overflow-hidden px-2">
|
||||
<nuxt-link :to="`/collection/${collection.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ collection.name }}</nuxt-link>
|
||||
</div>
|
||||
<div class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
||||
<ui-btn v-if="!isBookIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-symbols text-2xl pt-px">add</span></ui-btn>
|
||||
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-symbols text-2xl pt-px">remove</span></ui-btn>
|
||||
<ui-btn v-if="!isBookIncluded" color="bg-success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-symbols text-2xl pt-px">add</span></ui-btn>
|
||||
<ui-btn v-else color="bg-error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-symbols text-2xl pt-px">remove</span></ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -55,4 +55,4 @@ export default {
|
|||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -12,32 +12,32 @@
|
|||
<div class="w-full flex justify-center mb-2 md:w-auto md:mb-0 md:block">
|
||||
<covers-collection-cover :book-items="books" :width="200" :height="100 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</div>
|
||||
<div class="flex-grow px-4">
|
||||
<div class="grow px-4">
|
||||
<ui-text-input-with-label v-model="newCollectionName" :label="$strings.LabelName" class="mb-2" />
|
||||
|
||||
<ui-textarea-with-label v-model="newCollectionDescription" :label="$strings.LabelDescription" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 right-0 w-full py-4 px-4 flex">
|
||||
<ui-btn v-if="userCanDelete" small color="error" type="button" @click.stop="removeClick">{{ $strings.ButtonRemove }}</ui-btn>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
||||
<ui-btn v-if="userCanDelete" small color="bg-error" type="button" @click.stop="removeClick">{{ $strings.ButtonRemove }}</ui-btn>
|
||||
<div class="grow" />
|
||||
<ui-btn color="bg-success" type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="hover:bg-white hover:bg-opacity-10 cursor-pointer h-11 w-11 flex items-center justify-center rounded-full" @click="showImageUploader = false">
|
||||
<div class="hover:bg-white/10 cursor-pointer h-11 w-11 flex items-center justify-center rounded-full" @click="showImageUploader = false">
|
||||
<span class="material-symbols text-4xl">arrow_back</span>
|
||||
</div>
|
||||
<p class="ml-2 text-xl mb-1">Collection Cover Image</p>
|
||||
</div>
|
||||
<div class="flex mb-4">
|
||||
<ui-btn small class="mr-2">Upload</ui-btn>
|
||||
<ui-text-input v-model="newCoverImage" class="flex-grow" placeholder="Collection Cover Image" />
|
||||
<ui-text-input v-model="newCoverImage" class="grow" placeholder="Collection Cover Image" />
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<ui-btn color="success">Upload</ui-btn>
|
||||
<ui-btn color="bg-success">Upload</ui-btn>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -26,8 +26,8 @@
|
|||
</div>
|
||||
|
||||
<div class="flex items-center pt-4">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
<div class="grow" />
|
||||
<ui-btn color="bg-success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
</div>
|
||||
|
||||
<div class="flex items-center pt-4">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
<div class="grow" />
|
||||
<ui-btn color="bg-success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -16,10 +16,10 @@
|
|||
</div>
|
||||
|
||||
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||
<button class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" :aria-label="$strings.ButtonNext" @click.stop.prevent="goPrevBook" @mousedown.prevent>arrow_back_ios</button>
|
||||
<button class="material-symbols text-5xl text-white/50 hover:text-white/90 cursor-pointer pointer-events-auto" :aria-label="$strings.ButtonNext" @click.stop.prevent="goPrevBook" @mousedown.prevent>arrow_back_ios</button>
|
||||
</div>
|
||||
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||
<button class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" :aria-label="$strings.ButtonPrevious" @click.stop.prevent="goNextBook" @mousedown.prevent>arrow_forward_ios</button>
|
||||
<button class="material-symbols text-5xl text-white/50 hover:text-white/90 cursor-pointer pointer-events-auto" :aria-label="$strings.ButtonPrevious" @click.stop.prevent="goNextBook" @mousedown.prevent>arrow_forward_ios</button>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
|
|
|
@ -2,37 +2,37 @@
|
|||
<div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
|
||||
<div class="flex flex-col sm:flex-row mb-4">
|
||||
<div class="relative self-center md:self-start">
|
||||
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, libraryItemUpdatedAt, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<covers-preview-cover :src="coverUrl" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
|
||||
<!-- book cover overlay -->
|
||||
<div v-if="media.coverPath" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
|
||||
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
|
||||
<div v-if="userCanDelete" class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-sm" @click="removeCover">
|
||||
<div class="absolute top-0 left-0 w-full h-16 bg-linear-to-b from-black-600 to-transparent" />
|
||||
<div v-if="userCanDelete" class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-xs" @click="removeCover">
|
||||
<ui-tooltip direction="top" :text="$strings.LabelRemoveCover">
|
||||
<span class="material-symbols text-2xl">delete</span>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-6 md:mt-0">
|
||||
<div class="grow sm:pl-2 md:pl-6 sm:pr-2 mt-6 md:mt-0">
|
||||
<div class="flex items-center">
|
||||
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 md:min-w-32">
|
||||
<ui-file-input ref="fileInput" @change="fileUploadSelected">
|
||||
<span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span>
|
||||
<span class="material-symbols text-2xl inline-block md:!hidden">upload</span>
|
||||
<span class="material-symbols text-2xl inline-block md:hidden!">upload</span>
|
||||
</ui-file-input>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submitForm" class="flex flex-grow">
|
||||
<form @submit.prevent="submitForm" class="flex grow">
|
||||
<ui-text-input v-model="imageUrl" :placeholder="$strings.LabelImageURLFromTheWeb" class="h-9 w-full" />
|
||||
<ui-btn color="success" type="submit" :padding-x="4" :disabled="!imageUrl" class="ml-2 sm:ml-3 w-24 h-9">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
<ui-btn color="bg-success" type="submit" :padding-x="4" :disabled="!imageUrl" class="ml-2 sm:ml-3 w-24 h-9">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="localCovers.length" class="mb-4 mt-6 border-t border-b border-white border-opacity-10">
|
||||
<div v-if="localCovers.length" class="mb-4 mt-6 border-t border-b border-white/10">
|
||||
<div class="flex items-center justify-center py-2">
|
||||
<p>{{ localCovers.length }} local image{{ localCovers.length !== 1 ? 's' : '' }}</p>
|
||||
<div class="flex-grow" />
|
||||
<div class="grow" />
|
||||
<ui-btn small @click="showLocalCovers = !showLocalCovers">{{ showLocalCovers ? $strings.ButtonHide : $strings.ButtonShow }}</ui-btn>
|
||||
</div>
|
||||
|
||||
|
@ -50,13 +50,13 @@
|
|||
</div>
|
||||
<form @submit.prevent="submitSearchForm">
|
||||
<div class="flex flex-wrap sm:flex-nowrap items-center justify-start -mx-1">
|
||||
<div class="w-48 flex-grow p-1">
|
||||
<div class="w-48 grow p-1">
|
||||
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small />
|
||||
</div>
|
||||
<div class="w-72 flex-grow p-1">
|
||||
<div class="w-72 grow p-1">
|
||||
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" />
|
||||
</div>
|
||||
<div v-show="provider != 'itunes' && provider != 'audiobookcovers'" class="w-72 flex-grow p-1">
|
||||
<div v-show="provider != 'itunes' && provider != 'audiobookcovers'" class="w-72 grow p-1">
|
||||
<ui-text-input-with-label v-model="searchAuthor" :label="$strings.LabelAuthor" />
|
||||
</div>
|
||||
<ui-btn class="mt-5 ml-1 md:min-w-24" :padding-x="4" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
|
||||
|
@ -79,7 +79,7 @@
|
|||
</div>
|
||||
<div class="absolute bottom-0 right-0 flex py-4 px-5">
|
||||
<ui-btn :disabled="processingUpload" class="mx-2" @click="resetCoverPreview">{{ $strings.ButtonReset }}</ui-btn>
|
||||
<ui-btn :loading="processingUpload" color="success" @click="submitCoverUpload">{{ $strings.ButtonUpload }}</ui-btn>
|
||||
<ui-btn :loading="processingUpload" color="bg-success" @click="submitCoverUpload">{{ $strings.ButtonUpload }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -157,6 +157,12 @@ export default {
|
|||
coverPath() {
|
||||
return this.media.coverPath
|
||||
},
|
||||
coverUrl() {
|
||||
if (!this.coverPath) {
|
||||
return this.$store.getters['globals/getPlaceholderCoverSrc']
|
||||
}
|
||||
return this.$store.getters['globals/getLibraryItemCoverSrcById'](this.libraryItemId, this.libraryItemUpdatedAt, true)
|
||||
},
|
||||
mediaMetadata() {
|
||||
return this.media.metadata || {}
|
||||
},
|
||||
|
|
|
@ -5,17 +5,15 @@
|
|||
<widgets-podcast-details-edit v-else ref="itemDetailsEdit" :library-item="libraryItem" @submit="saveAndClose" />
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 left-0 w-full py-2 md:py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'border-t border-white border-opacity-5'">
|
||||
<div class="absolute bottom-0 left-0 w-full py-2 md:py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'border-t border-white/5'">
|
||||
<div class="flex items-center px-4">
|
||||
<ui-tooltip :disabled="!!quickMatching" :text="$getString('MessageQuickMatchDescription', [libraryProvider])" direction="bottom" class="mr-2 md:mr-4">
|
||||
<ui-btn v-if="userIsAdminOrUp" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">{{ $strings.ButtonQuickMatch }}</ui-btn>
|
||||
<ui-btn v-if="userIsAdminOrUp" :loading="quickMatching" color="bg-bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">{{ $strings.ButtonQuickMatch }}</ui-btn>
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip :disabled="isLibraryScanning" text="Rescan library item including metadata" direction="bottom" class="mr-2 md:mr-4">
|
||||
<ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="isLibraryScanning" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">{{ $strings.ButtonReScan }}</ui-btn>
|
||||
</ui-tooltip>
|
||||
<ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="isLibraryScanning" color="bg-bg" type="button" class="h-full" small @click.stop.prevent="rescan">{{ $strings.ButtonReScan }}</ui-btn>
|
||||
|
||||
<div class="flex-grow" />
|
||||
<div class="grow" />
|
||||
|
||||
<!-- desktop -->
|
||||
<ui-btn @click="save" class="mx-2 hidden md:block">{{ $strings.ButtonSave }}</ui-btn>
|
||||
|
|
|
@ -29,9 +29,6 @@ export default {
|
|||
media() {
|
||||
return this.libraryItem.media || {}
|
||||
},
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
userCanUpdate() {
|
||||
return this.$store.getters['user/getUserCanUpdate']
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="w-36 px-1">
|
||||
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small />
|
||||
</div>
|
||||
<div class="flex-grow md:w-72 px-1">
|
||||
<div class="grow md:w-72 px-1">
|
||||
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" />
|
||||
</div>
|
||||
<div v-show="provider != 'itunes'" class="w-60 md:w-72 px-1">
|
||||
|
@ -27,7 +27,7 @@
|
|||
</div>
|
||||
<div v-if="selectedMatchOrig" class="absolute top-0 left-0 w-full bg-bg h-full px-2 py-6 md:p-8 max-h-full overflow-y-auto overflow-x-hidden">
|
||||
<div class="flex mb-4">
|
||||
<div class="w-8 h-8 rounded-full hover:bg-white hover:bg-opacity-10 flex items-center justify-center cursor-pointer" @click="clearSelectedMatch">
|
||||
<div class="w-8 h-8 rounded-full hover:bg-white/10 flex items-center justify-center cursor-pointer" @click="clearSelectedMatch">
|
||||
<span class="material-symbols text-3xl">arrow_back</span>
|
||||
</div>
|
||||
<p class="text-xl pl-3">{{ $strings.HeaderUpdateDetails }}</p>
|
||||
|
@ -35,9 +35,9 @@
|
|||
<ui-checkbox v-model="selectAll" :label="$strings.LabelSelectAll" checkbox-bg="bg" @input="selectAllToggled" />
|
||||
<form @submit.prevent="submitMatchUpdate">
|
||||
<div v-if="selectedMatchOrig.cover" class="flex flex-wrap md:flex-nowrap items-center justify-center">
|
||||
<div class="flex flex-grow items-center py-2">
|
||||
<div class="flex grow items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.cover" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<ui-text-input-with-label v-model="selectedMatch.cover" :disabled="!selectedMatchUsage.cover" readonly :label="$strings.LabelCover" class="flex-grow mx-4" />
|
||||
<ui-text-input-with-label v-model="selectedMatch.cover" :disabled="!selectedMatchUsage.cover" readonly :label="$strings.LabelCover" class="grow mx-4" />
|
||||
</div>
|
||||
|
||||
<div class="flex py-2">
|
||||
|
@ -57,63 +57,63 @@
|
|||
</div>
|
||||
<div v-if="selectedMatchOrig.title" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.title" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<div class="grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" :label="$strings.LabelTitle" />
|
||||
<p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60">
|
||||
<p v-if="mediaMetadata.title" class="text-xs ml-1 text-white/60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('title', mediaMetadata.title)">{{ mediaMetadata.title || '' }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.subtitle" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.subtitle" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<div class="grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" :label="$strings.LabelSubtitle" />
|
||||
<p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60">
|
||||
<p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white/60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('subtitle', mediaMetadata.subtitle)">{{ mediaMetadata.subtitle }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.author" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.author" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<div class="grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" :label="$strings.LabelAuthor" />
|
||||
<p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60">
|
||||
<p v-if="mediaMetadata.authorName" 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>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.narrator" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.narrator" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<div class="grow ml-4">
|
||||
<ui-multi-select v-model="selectedMatch.narrator" :items="narrators" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" />
|
||||
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">
|
||||
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white/60">
|
||||
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('narrator', mediaMetadata.narrators)">{{ mediaMetadata.narratorName }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.description" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.description" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<div class="grow ml-4">
|
||||
<ui-rich-text-editor v-model="selectedMatch.description" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" />
|
||||
<p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60">
|
||||
<p v-if="mediaMetadata.description" class="text-xs ml-1 text-white/60">
|
||||
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.descriptionPlain.substr(0, 100) + (mediaMetadata.descriptionPlain.length > 100 ? '...' : '') }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.publisher" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.publisher" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<div class="grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" :label="$strings.LabelPublisher" />
|
||||
<p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60">
|
||||
<p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white/60">
|
||||
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publisher', mediaMetadata.publisher)">{{ mediaMetadata.publisher }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.publishedYear" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.publishedYear" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<div class="grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" :label="$strings.LabelPublishYear" />
|
||||
<p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60">
|
||||
<p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white/60">
|
||||
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publishedYear', mediaMetadata.publishedYear)">{{ mediaMetadata.publishedYear }}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -121,54 +121,54 @@
|
|||
|
||||
<div v-if="selectedMatchOrig.series" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.series" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<div class="grow ml-4">
|
||||
<widgets-series-input-widget v-model="selectedMatch.series" :disabled="!selectedMatchUsage.series" />
|
||||
<p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">
|
||||
<p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white/60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('series', mediaMetadata.series)">{{ mediaMetadata.seriesName }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.genres?.length" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.genres" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<div class="grow ml-4">
|
||||
<ui-multi-select v-model="selectedMatch.genres" :items="genres" :disabled="!selectedMatchUsage.genres" :label="$strings.LabelGenres" />
|
||||
<p v-if="mediaMetadata.genres?.length" class="text-xs ml-1 text-white text-opacity-60">
|
||||
<p v-if="mediaMetadata.genres?.length" class="text-xs ml-1 text-white/60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('genres', mediaMetadata.genres)">{{ mediaMetadata.genres.join(', ') }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.tags" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.tags" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<div class="grow ml-4">
|
||||
<ui-multi-select v-model="selectedMatch.tags" :items="tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" />
|
||||
<p v-if="media.tags?.length" class="text-xs ml-1 text-white text-opacity-60">
|
||||
<p v-if="media.tags?.length" class="text-xs ml-1 text-white/60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('tags', media.tags)">{{ media.tags.join(', ') }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.language" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.language" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<div class="grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" :label="$strings.LabelLanguage" />
|
||||
<p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60">
|
||||
<p v-if="mediaMetadata.language" class="text-xs ml-1 text-white/60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('language', mediaMetadata.language)">{{ mediaMetadata.language }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.isbn" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.isbn" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<div class="grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" />
|
||||
<p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60">
|
||||
<p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white/60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('isbn', mediaMetadata.isbn)">{{ mediaMetadata.isbn }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.asin" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.asin" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<div class="grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" />
|
||||
<p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60">
|
||||
<p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white/60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('asin', mediaMetadata.asin)">{{ mediaMetadata.asin }}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -176,57 +176,57 @@
|
|||
|
||||
<div v-if="selectedMatchOrig.itunesId" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.itunesId" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<div class="grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.itunesId" type="number" :disabled="!selectedMatchUsage.itunesId" label="iTunes ID" />
|
||||
<p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60">
|
||||
<p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white/60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesId', mediaMetadata.itunesId)">{{ mediaMetadata.itunesId }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.feedUrl" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.feedUrl" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<div class="grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.feedUrl" :disabled="!selectedMatchUsage.feedUrl" label="RSS Feed URL" />
|
||||
<p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60">
|
||||
<p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white/60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('feedUrl', mediaMetadata.feedUrl)">{{ mediaMetadata.feedUrl }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.itunesPageUrl" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.itunesPageUrl" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<div class="grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.itunesPageUrl" :disabled="!selectedMatchUsage.itunesPageUrl" label="iTunes Page URL" />
|
||||
<p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60">
|
||||
<p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white/60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesPageUrl', mediaMetadata.itunesPageUrl)">{{ mediaMetadata.itunesPageUrl }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.releaseDate" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.releaseDate" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4">
|
||||
<div class="grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" :label="$strings.LabelReleaseDate" />
|
||||
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">
|
||||
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white/60">
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('releaseDate', mediaMetadata.releaseDate)">{{ mediaMetadata.releaseDate }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.explicit != null" class="flex items-center pb-2" :class="{ 'pt-2': mediaMetadata.explicit == null }">
|
||||
<ui-checkbox v-model="selectedMatchUsage.explicit" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4" :class="{ 'pt-4': mediaMetadata.explicit != null }">
|
||||
<div class="grow ml-4" :class="{ 'pt-4': mediaMetadata.explicit != null }">
|
||||
<ui-checkbox v-model="selectedMatch.explicit" :label="$strings.LabelExplicit" :disabled="!selectedMatchUsage.explicit" :checkbox-bg="!selectedMatchUsage.explicit ? 'bg' : 'primary'" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||
<p v-if="mediaMetadata.explicit != null" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.explicit ? $strings.LabelExplicitChecked : $strings.LabelExplicitUnchecked }}</p>
|
||||
<p v-if="mediaMetadata.explicit != null" class="text-xs ml-1 text-white/60">{{ $strings.LabelCurrently }} {{ mediaMetadata.explicit ? $strings.LabelExplicitChecked : $strings.LabelExplicitUnchecked }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedMatchOrig.abridged != null" class="flex items-center pb-2" :class="{ 'pt-2': mediaMetadata.abridged == null }">
|
||||
<ui-checkbox v-model="selectedMatchUsage.abridged" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="flex-grow ml-4" :class="{ 'pt-4': mediaMetadata.abridged != null }">
|
||||
<div class="grow ml-4" :class="{ 'pt-4': mediaMetadata.abridged != null }">
|
||||
<ui-checkbox v-model="selectedMatch.abridged" :label="$strings.LabelAbridged" :disabled="!selectedMatchUsage.abridged" :checkbox-bg="!selectedMatchUsage.abridged ? 'bg' : 'primary'" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||
<p v-if="mediaMetadata.abridged != null" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.abridged ? $strings.LabelAbridgedChecked : $strings.LabelAbridgedUnchecked }}</p>
|
||||
<p v-if="mediaMetadata.abridged != null" class="text-xs ml-1 text-white/60">{{ $strings.LabelCurrently }} {{ mediaMetadata.abridged ? $strings.LabelAbridgedChecked : $strings.LabelAbridgedUnchecked }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end py-2">
|
||||
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
<ui-btn color="bg-success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -33,10 +33,10 @@
|
|||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="feedUrl || autoDownloadEpisodes" class="absolute bottom-0 left-0 w-full py-2 md:py-4 bg-bg border-t border-white border-opacity-5">
|
||||
<div v-if="feedUrl || autoDownloadEpisodes" class="absolute bottom-0 left-0 w-full py-2 md:py-4 bg-bg border-t border-white/5">
|
||||
<div class="flex items-center px-2 md:px-4">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn @click="save" :disabled="!isUpdated" :color="isUpdated ? 'success' : 'primary'" class="mx-2">{{ isUpdated ? $strings.ButtonSave : $strings.MessageNoUpdatesWereNecessary }}</ui-btn>
|
||||
<div class="grow" />
|
||||
<ui-btn @click="save" :disabled="!isUpdated" :color="isUpdated ? 'bg-success' : 'bg-primary'" class="mx-2">{{ isUpdated ? $strings.ButtonSave : $strings.MessageNoUpdatesWereNecessary }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<p class="text-lg">{{ $strings.LabelToolsMakeM4b }}</p>
|
||||
<p class="max-w-sm text-sm pt-2 text-gray-300">{{ $strings.LabelToolsMakeM4bDescription }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div class="grow" />
|
||||
<div>
|
||||
<ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=m4b`" class="flex items-center"
|
||||
>{{ $strings.ButtonOpenManager }}
|
||||
|
@ -26,7 +26,7 @@
|
|||
<p class="text-lg">{{ $strings.LabelToolsEmbedMetadata }}</p>
|
||||
<p class="max-w-sm text-sm pt-2 text-gray-300">{{ $strings.LabelToolsEmbedMetadataDescription }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div class="grow" />
|
||||
<div>
|
||||
<ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=embed`" class="flex items-center"
|
||||
>{{ $strings.ButtonOpenManager }}
|
||||
|
@ -74,19 +74,12 @@ export default {
|
|||
mediaTracks() {
|
||||
return this.media.tracks || []
|
||||
},
|
||||
isSingleM4b() {
|
||||
return this.mediaTracks.length === 1 && this.mediaTracks[0].metadata.ext.toLowerCase() === '.m4b'
|
||||
},
|
||||
chapters() {
|
||||
return this.media.chapters || []
|
||||
},
|
||||
showM4bDownload() {
|
||||
if (!this.mediaTracks.length) return false
|
||||
return !this.isSingleM4b
|
||||
},
|
||||
showMp3Split() {
|
||||
if (!this.mediaTracks.length) return false
|
||||
return this.isSingleM4b && this.chapters.length
|
||||
return true
|
||||
},
|
||||
queuedEmbedLIds() {
|
||||
return this.$store.state.tasks.queuedEmbedLIds || []
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="w-2/5 md:w-72 px-1 py-1 md:py-0">
|
||||
<ui-dropdown v-model="mediaType" :items="mediaTypes" :label="$strings.LabelMediaType" :disabled="!isNew" small @input="changedMediaType" />
|
||||
</div>
|
||||
<div class="w-full md:flex-grow px-1 py-1 md:py-0">
|
||||
<div class="w-full md:grow px-1 py-1 md:py-0">
|
||||
<ui-text-input-with-label ref="nameInput" v-model="name" :label="$strings.LabelLibraryName" @blur="nameBlurred" />
|
||||
</div>
|
||||
<div class="w-1/5 md:w-18 px-1 py-1 md:py-0">
|
||||
|
@ -19,16 +19,16 @@
|
|||
<div class="folders-container overflow-y-auto w-full py-2 mb-2">
|
||||
<p class="px-1 text-sm font-semibold">{{ $strings.LabelFolders }}</p>
|
||||
<div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2">
|
||||
<span class="material-symbols fill bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<span class="material-symbols fill mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<ui-editable-text ref="folderInput" v-model="folder.fullPath" :readonly="!!folder.id" type="text" class="w-full" @blur="existingFolderInputBlurred(folder)" />
|
||||
<span v-show="folders.length > 1" class="material-symbols text-2xl ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span>
|
||||
</div>
|
||||
<div class="flex py-1 px-2 items-center w-full">
|
||||
<span class="material-symbols fill bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<span class="material-symbols fill mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<ui-editable-text ref="newFolderInput" v-model="newFolderPath" :placeholder="$strings.PlaceholderNewFolderPath" type="text" class="w-full" @blur="newFolderInputBlurred" />
|
||||
</div>
|
||||
|
||||
<ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn>
|
||||
<ui-btn class="w-full mt-2" color="bg-primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
<modals-libraries-lazy-folder-chooser v-else :paths="folderPaths" @back="showDirectoryPicker = false" @select="selectFolder" />
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<div class="px-2 md:px-4 w-full text-sm pt-2 md:pt-6 pb-20 rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
||||
<component v-if="libraryCopy && show" ref="tabComponent" :is="tabName" :is-new="!library" :library="libraryCopy" :library-id="libraryId" :processing.sync="processing" @update="updateLibrary" @close="show = false" />
|
||||
|
||||
<div v-show="selectedTab !== 'tools'" class="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-white border-opacity-10">
|
||||
<div v-show="selectedTab !== 'tools'" class="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-white/10">
|
||||
<div class="flex justify-end">
|
||||
<ui-btn @click="submit">{{ buttonText }}</ui-btn>
|
||||
</div>
|
||||
|
|
|
@ -4,24 +4,24 @@
|
|||
<span class="material-symbols text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span>
|
||||
<p class="px-4 text-xl">{{ $strings.HeaderChooseAFolder }}</p>
|
||||
</div>
|
||||
<div v-if="rootDirs.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2">
|
||||
<div v-if="rootDirs.length" class="w-full bg-primary/70 py-1 px-4 mb-2">
|
||||
<p class="font-mono truncate">{{ selectedPath || '/' }}</p>
|
||||
</div>
|
||||
<div v-if="rootDirs.length" class="relative flex bg-primary bg-opacity-50 p-4 folder-container">
|
||||
<div v-if="rootDirs.length" class="relative flex bg-primary/50 p-4 folder-container">
|
||||
<div class="w-1/2 border-r border-bg h-full overflow-y-auto">
|
||||
<div v-if="level > 0" class="w-full p-1 cursor-pointer flex items-center hover:bg-white/10" @click="goBack">
|
||||
<span class="material-symbols fill bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<span class="material-symbols fill text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<p class="text-base font-mono px-2">..</p>
|
||||
</div>
|
||||
<div v-for="dir in _directories" :key="dir.path" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" :class="dir.className" @click="selectDir(dir)">
|
||||
<span class="material-symbols fill bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<span class="material-symbols fill text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
|
||||
<span v-if="dir.path === selectedPath" class="material-symbols" style="font-size: 1.1rem">arrow_right</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/2 h-full overflow-y-auto">
|
||||
<div v-for="dir in _subdirs" :key="dir.path" :class="dir.className" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" @click="selectSubDir(dir)">
|
||||
<span class="material-symbols fill bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<span class="material-symbols fill text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -38,7 +38,7 @@
|
|||
</div>
|
||||
|
||||
<div class="w-full py-2">
|
||||
<ui-btn :disabled="!selectedPath" color="primary" class="w-full mt-2" @click="selectFolder">{{ $strings.ButtonSelectFolderPath }}</ui-btn>
|
||||
<ui-btn :disabled="!selectedPath" color="bg-primary" class="w-full mt-2" @click="selectFolder">{{ $strings.ButtonSelectFolderPath }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<div class="text-center py-1 w-8 min-w-8">
|
||||
{{ source.include ? getSourceIndex(source.id) : '' }}
|
||||
</div>
|
||||
<div class="flex-grow inline-flex justify-between px-4 py-3">
|
||||
<div class="grow inline-flex justify-between px-4 py-3">
|
||||
{{ source.name }} <span v-if="source.include && (index === firstActiveSourceIndex || index === lastActiveSourceIndex)" class="px-2 italic font-semibold text-xs text-gray-400">{{ index === firstActiveSourceIndex ? $strings.LabelHighestPriority : $strings.LabelLowestPriority }}</span>
|
||||
</div>
|
||||
<div class="px-2 opacity-100">
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<p class="text-lg">{{ $strings.LabelRemoveMetadataFile }}</p>
|
||||
<p class="max-w-sm text-sm pt-2 text-gray-300">{{ $getString('LabelRemoveMetadataFileHelp', [mediaType]) }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div class="grow" />
|
||||
<div>
|
||||
<ui-btn class="mb-4 block" @click.stop="removeAllMetadataClick('json')">{{ $strings.LabelRemoveAllMetadataJson }}</ui-btn>
|
||||
<ui-btn @click.stop="removeAllMetadataClick('abs')">{{ $strings.LabelRemoveAllMetadataAbs }}</ui-btn>
|
||||
|
|
|
@ -25,8 +25,8 @@
|
|||
<ui-toggle-switch v-model="newNotification.enabled" />
|
||||
<p class="text-lg pl-2">{{ $strings.LabelEnable }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
<div class="grow" />
|
||||
<ui-btn color="bg-success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div v-if="item" class="w-full flex items-center px-4 py-2" :class="wrapperClass" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<covers-preview-cover :src="coverUrl" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" />
|
||||
<div class="flex-grow px-2 py-1 queue-item-row-content truncate">
|
||||
<div class="grow px-2 py-1 queue-item-row-content truncate">
|
||||
<p class="text-gray-200 text-sm truncate">{{ title }}</p>
|
||||
<p class="text-gray-300 text-sm">{{ subtitle }}</p>
|
||||
<p v-if="caption" class="text-gray-400 text-xs">{{ caption }}</p>
|
||||
|
@ -9,10 +9,10 @@
|
|||
<div class="w-28">
|
||||
<p v-if="isOpenInPlayer" class="text-sm text-right text-gray-400">{{ $strings.ButtonPlaying }}</p>
|
||||
<div v-else-if="isHovering" class="flex items-center justify-end -mx-1">
|
||||
<button class="outline-none mx-1 flex items-center" @click.stop="playClick">
|
||||
<button class="outline-hidden mx-1 flex items-center" @click.stop="playClick">
|
||||
<span class="material-symbols fill text-2xl text-success">play_arrow</span>
|
||||
</button>
|
||||
<button class="outline-none mx-1 flex items-center" @click.stop="removeClick">
|
||||
<button class="outline-hidden mx-1 flex items-center" @click.stop="removeClick">
|
||||
<span class="material-symbols text-2xl text-error">close</span>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -55,7 +55,7 @@ export default {
|
|||
return this.item.coverPath
|
||||
},
|
||||
coverUrl() {
|
||||
if (!this.coverPath) return `${this.$config.routerBasePath}/book_placeholder.jpg`
|
||||
if (!this.coverPath) return this.$store.getters['globals/getPlaceholderCoverSrc']
|
||||
return this.$store.getters['globals/getLibraryItemCoverSrcById'](this.libraryItemId)
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
|
@ -72,9 +72,9 @@ export default {
|
|||
return this.$store.getters['getIsMediaStreaming'](this.libraryItemId, this.episodeId)
|
||||
},
|
||||
wrapperClass() {
|
||||
if (this.isOpenInPlayer) return 'bg-yellow-400 bg-opacity-10'
|
||||
if (this.index % 2 === 0) return 'bg-gray-300 bg-opacity-5 hover:bg-opacity-10'
|
||||
return 'bg-bg hover:bg-gray-300 hover:bg-opacity-10'
|
||||
if (this.isOpenInPlayer) return 'bg-yellow-400/10'
|
||||
if (this.index % 2 === 0) return 'bg-gray-300/5 hover:bg-gray-300/10'
|
||||
return 'bg-bg hover:bg-gray-300/10'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -99,4 +99,4 @@ export default {
|
|||
.queue-item-row-content {
|
||||
max-width: calc(100% - 48px - 128px);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<div class="pb-4 px-4 flex items-center">
|
||||
<p class="text-base text-gray-200">{{ $strings.HeaderPlayerQueue }}</p>
|
||||
<p class="text-base text-gray-400 px-4">{{ playerQueueItems.length }} Items</p>
|
||||
<div class="flex-grow" />
|
||||
<div class="grow" />
|
||||
<ui-checkbox v-model="playerQueueAutoPlay" label="Auto Play" medium checkbox-bg="primary" border-color="gray-600" label-class="pl-2 mb-px" />
|
||||
</div>
|
||||
<modals-player-queue-item-row v-for="(item, index) in playerQueueItems" :key="index" :item="item" :index="index" @play="playItem(index)" @remove="removeItem" />
|
||||
|
|
|
@ -32,13 +32,13 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full h-px bg-white bg-opacity-10" />
|
||||
<div class="w-full h-px bg-white/10" />
|
||||
<form @submit.prevent="submitCreatePlaylist">
|
||||
<div class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">
|
||||
<div class="flex-grow px-2">
|
||||
<div class="flex px-4 py-2 items-center text-center border-b border-white/10 text-white/80">
|
||||
<div class="grow px-2">
|
||||
<ui-text-input v-model="newPlaylistName" :placeholder="$strings.PlaceholderNewPlaylist" class="w-full" />
|
||||
</div>
|
||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10">{{ $strings.ButtonCreate }}</ui-btn>
|
||||
<ui-btn type="submit" color="bg-success" :padding-x="4" class="h-10">{{ $strings.ButtonCreate }}</ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -11,16 +11,16 @@
|
|||
<div>
|
||||
<covers-playlist-cover :items="items" :width="200" :height="200" />
|
||||
</div>
|
||||
<div class="flex-grow px-4">
|
||||
<div class="grow px-4">
|
||||
<ui-text-input-with-label v-model="newPlaylistName" :label="$strings.LabelName" class="mb-2" />
|
||||
|
||||
<ui-textarea-with-label v-model="newPlaylistDescription" :label="$strings.LabelDescription" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 right-0 w-full py-2 px-4 flex">
|
||||
<ui-btn v-if="userCanDelete" small color="error" type="button" @click.stop="removeClick">{{ $strings.ButtonRemove }}</ui-btn>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
||||
<ui-btn v-if="userCanDelete" small color="bg-error" type="button" @click.stop="removeClick">{{ $strings.ButtonRemove }}</ui-btn>
|
||||
<div class="grow" />
|
||||
<ui-btn color="bg-success" type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
<div class="w-16 max-w-16 text-center">
|
||||
<covers-playlist-cover :items="items" :width="64" :height="64" />
|
||||
</div>
|
||||
<div class="flex-grow overflow-hidden px-2">
|
||||
<div class="grow overflow-hidden px-2">
|
||||
<nuxt-link :to="`/playlist/${playlist.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ playlist.name }}</nuxt-link>
|
||||
</div>
|
||||
<div class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
||||
<ui-btn v-if="!isItemIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-symbols text-2xl pt-px">add</span></ui-btn>
|
||||
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-symbols text-2xl pt-px">remove</span></ui-btn>
|
||||
<ui-btn v-if="!isItemIncluded" color="bg-success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-symbols text-2xl pt-px">add</span></ui-btn>
|
||||
<ui-btn v-else color="bg-error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-symbols text-2xl pt-px">remove</span></ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -54,4 +54,4 @@ export default {
|
|||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -12,10 +12,10 @@
|
|||
</div>
|
||||
|
||||
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||
<div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevEpisode" @mousedown.prevent>arrow_back_ios</div>
|
||||
<div class="material-symbols text-5xl text-white/50 hover:text-white/90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevEpisode" @mousedown.prevent>arrow_back_ios</div>
|
||||
</div>
|
||||
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||
<div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextEpisode" @mousedown.prevent>arrow_forward_ios</div>
|
||||
<div class="material-symbols text-5xl text-white/50 hover:text-white/90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextEpisode" @mousedown.prevent>arrow_forward_ios</div>
|
||||
</div>
|
||||
|
||||
<div ref="wrapper" class="p-4 w-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
|
||||
|
|
|
@ -7,18 +7,18 @@
|
|||
</template>
|
||||
<div ref="wrapper" id="podcast-wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
||||
<div v-if="episodesCleaned.length" class="w-full py-3 mx-auto flex">
|
||||
<form @submit.prevent="submit" class="flex flex-grow">
|
||||
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
|
||||
<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" />
|
||||
</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 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 bg-opacity-40' : selectedEpisodes[episode.cleanUrl] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-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)">
|
||||
<div class="absolute top-0 left-0 h-full flex items-center p-2">
|
||||
<span v-if="episode.isDownloaded" class="material-symbols text-success text-xl">download_done</span>
|
||||
<span v-else-if="episode.isDownloading" class="material-symbols text-warning text-xl">download</span>
|
||||
|
@ -35,14 +35,21 @@
|
|||
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
||||
</div>
|
||||
<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 class="flex justify-end pt-4">
|
||||
<ui-checkbox v-if="!allDownloaded" v-model="selectAll" @input="toggleSelectAll" :label="selectAllLabel" small checkbox-bg="primary" border-color="gray-600" class="mx-8" />
|
||||
<ui-btn v-if="!allDownloaded" :disabled="!episodesSelected.length" @click="submit">{{ buttonText }}</ui-btn>
|
||||
<p v-else class="text-success text-base px-2 py-4">All episodes are downloaded</p>
|
||||
<p v-else class="text-success text-base px-2 py-4">{{ $strings.LabelAllEpisodesDownloaded }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
|
@ -79,7 +86,8 @@ export default {
|
|||
searchTimeout: null,
|
||||
searchText: null,
|
||||
downloadedEpisodeGuidMap: {},
|
||||
downloadedEpisodeUrlMap: {}
|
||||
downloadedEpisodeUrlMap: {},
|
||||
sortDescending: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -147,6 +155,17 @@ export default {
|
|||
}
|
||||
},
|
||||
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) {
|
||||
if (episode.guid && !!this.downloadedEpisodeGuidMap[episode.guid]) {
|
||||
return true
|
||||
|
@ -232,8 +251,8 @@ export default {
|
|||
const sizeInMb = payloadSize / 1024 / 1024
|
||||
const sizeInMbPretty = sizeInMb.toFixed(2) + 'MB'
|
||||
console.log('Request size', sizeInMb)
|
||||
if (sizeInMb > 4.99) {
|
||||
return this.$toast.error(`Request is too large (${sizeInMbPretty}) should be < 5Mb`)
|
||||
if (sizeInMb > 9.99) {
|
||||
return this.$toast.error(`Request is too large (${sizeInMbPretty}) should be < 10Mb`)
|
||||
}
|
||||
|
||||
this.processing = true
|
||||
|
|
|
@ -52,11 +52,11 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="flex items-center py-4 px-2">
|
||||
<div class="flex-grow" />
|
||||
<div class="grow" />
|
||||
<div class="px-4">
|
||||
<ui-checkbox v-model="podcast.autoDownloadEpisodes" :label="$strings.LabelAutoDownloadEpisodes" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-sm md:text-base font-semibold" />
|
||||
</div>
|
||||
<ui-btn color="success" @click="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
<ui-btn color="bg-success" @click="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
|
|
|
@ -32,8 +32,8 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="flex items-center py-4">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" @click="submit">{{ $strings.ButtonAddPodcasts }}</ui-btn>
|
||||
<div class="grow" />
|
||||
<ui-btn color="bg-success" @click="submit">{{ $strings.ButtonAddPodcasts }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
{{ $getString('MessageConfirmRemoveEpisode', [episodeTitle]) }}
|
||||
</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 text-opacity-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 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" />
|
||||
|
|
|
@ -10,30 +10,36 @@
|
|||
<div class="w-12 h-12">
|
||||
<covers-book-cover :library-item="libraryItem" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</div>
|
||||
<div class="flex-grow px-2">
|
||||
<div class="grow px-2">
|
||||
<p class="text-base mb-1">{{ podcastTitle }}</p>
|
||||
<p class="text-xs text-gray-300">{{ podcastAuthor }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div class="w-full h-px bg-white/5 my-4" />
|
||||
|
||||
<div class="flex items-center">
|
||||
<div class="flex-grow">
|
||||
<div class="grow">
|
||||
<p class="font-semibold text-xs mb-1">{{ $strings.LabelFilename }}</p>
|
||||
<p class="mb-2 text-xs">
|
||||
{{ audioFileFilename }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<div class="grow">
|
||||
<p class="font-semibold text-xs mb-1">{{ $strings.LabelSize }}</p>
|
||||
<p class="mb-2 text-xs">
|
||||
{{ audioFileSize }}
|
||||
</p>
|
||||
</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>
|
||||
</modals-modal>
|
||||
|
@ -68,7 +74,7 @@ export default {
|
|||
return this.episode.title || 'No Episode Title'
|
||||
},
|
||||
description() {
|
||||
return this.episode.description || ''
|
||||
return this.parseDescription(this.episode.description || '')
|
||||
},
|
||||
media() {
|
||||
return this.libraryItem?.media || {}
|
||||
|
@ -90,11 +96,49 @@ export default {
|
|||
|
||||
return this.$bytesPretty(size)
|
||||
},
|
||||
audioFileDuration() {
|
||||
const duration = this.episode.duration || 0
|
||||
return this.$elapsedPretty(duration)
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
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() {}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<div v-if="!isProcessing && searchedTitle && !episodesFound.length" class="w-full py-8">
|
||||
<p class="text-center text-lg">{{ $strings.MessageNoEpisodeMatchesFound }}</p>
|
||||
</div>
|
||||
<div v-for="(episode, index) in episodesFound" :key="index" class="w-full py-4 border-b border-white border-opacity-5 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer px-2" @click.stop="selectEpisode(episode)">
|
||||
<div v-for="(episode, index) in episodesFound" :key="index" class="w-full py-4 border-b border-white/5 hover:bg-gray-300/10 cursor-pointer px-2" @click.stop="selectEpisode(episode)">
|
||||
<p v-if="episode.episode" class="font-semibold text-gray-200">#{{ episode.episode }}</p>
|
||||
<p class="break-words mb-1">{{ episode.title }}</p>
|
||||
<p v-if="episode.subtitle" class="mb-1 text-sm text-gray-300 line-clamp-2">{{ episode.subtitle }}</p>
|
||||
|
|
|
@ -16,19 +16,19 @@
|
|||
<div v-if="currentFeed.meta" class="mt-5">
|
||||
<div class="flex py-0.5">
|
||||
<div class="w-48">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedPreventIndexing }}</span>
|
||||
<span class="text-white/60 uppercase text-sm">{{ $strings.LabelRSSFeedPreventIndexing }}</span>
|
||||
</div>
|
||||
<div>{{ currentFeed.meta.preventIndexing ? 'Yes' : 'No' }}</div>
|
||||
</div>
|
||||
<div v-if="currentFeed.meta.ownerName" class="flex py-0.5">
|
||||
<div class="w-48">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerName }}</span>
|
||||
<span class="text-white/60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerName }}</span>
|
||||
</div>
|
||||
<div>{{ currentFeed.meta.ownerName }}</div>
|
||||
</div>
|
||||
<div v-if="currentFeed.meta.ownerEmail" class="flex py-0.5">
|
||||
<div class="w-48">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerEmail }}</span>
|
||||
<span class="text-white/60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerEmail }}</span>
|
||||
</div>
|
||||
<div>{{ currentFeed.meta.ownerEmail }}</div>
|
||||
</div>
|
||||
|
@ -47,9 +47,9 @@
|
|||
<p v-if="hasEpisodesWithoutPubDate" class="w-full pt-2 text-warning text-xs">{{ $strings.NoteRSSFeedPodcastAppsPubDate }}</p>
|
||||
</div>
|
||||
<div v-show="userIsAdminOrUp" class="flex items-center pt-6">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn v-if="currentFeed" color="error" small @click="closeFeed">{{ $strings.ButtonCloseFeed }}</ui-btn>
|
||||
<ui-btn v-else color="success" small @click="openFeed">{{ $strings.ButtonOpenFeed }}</ui-btn>
|
||||
<div class="grow" />
|
||||
<ui-btn v-if="currentFeed" color="bg-error" small @click="closeFeed">{{ $strings.ButtonCloseFeed }}</ui-btn>
|
||||
<ui-btn v-else color="bg-success" small @click="openFeed">{{ $strings.ButtonOpenFeed }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
|
|
|
@ -11,26 +11,26 @@
|
|||
<div v-if="feed.meta" class="mt-5">
|
||||
<div class="flex py-0.5">
|
||||
<div class="w-48">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedPreventIndexing }}</span>
|
||||
<span class="text-white/60 uppercase text-sm">{{ $strings.LabelRSSFeedPreventIndexing }}</span>
|
||||
</div>
|
||||
<div>{{ feed.meta.preventIndexing ? 'Yes' : 'No' }}</div>
|
||||
</div>
|
||||
<div v-if="feed.meta.ownerName" class="flex py-0.5">
|
||||
<div class="w-48">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerName }}</span>
|
||||
<span class="text-white/60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerName }}</span>
|
||||
</div>
|
||||
<div>{{ feed.meta.ownerName }}</div>
|
||||
</div>
|
||||
<div v-if="feed.meta.ownerEmail" class="flex py-0.5">
|
||||
<div class="w-48">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerEmail }}</span>
|
||||
<span class="text-white/60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerEmail }}</span>
|
||||
</div>
|
||||
<div>{{ feed.meta.ownerEmail }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- -->
|
||||
<div class="episodesTable mt-2">
|
||||
<div class="bg-primary bg-opacity-40 h-12 header">
|
||||
<div class="bg-primary/40 h-12 header">
|
||||
{{ $strings.LabelEpisodeTitle }}
|
||||
</div>
|
||||
<div class="scroller">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="flex justify-center pt-4 pb-2 lg:pt-0 lg:pb-2">
|
||||
<div class="flex items-center justify-center flex-grow">
|
||||
<div class="flex items-center justify-center grow">
|
||||
<template v-if="!loading">
|
||||
<ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 lg:mr-8">
|
||||
<button :aria-label="$strings.ButtonPreviousChapter" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
|
||||
|
@ -12,7 +12,7 @@
|
|||
<span class="material-symbols text-2xl sm:text-3xl">replay</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
||||
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-xs bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
||||
<span class="material-symbols fill text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
||||
</button>
|
||||
<ui-tooltip direction="top" :text="jumpForwardText">
|
||||
|
@ -27,7 +27,7 @@
|
|||
</ui-tooltip>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
|
||||
<div class="cursor-pointer p-2 shadow-xs bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
|
||||
<span class="material-symbols text-2xl">autorenew</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
<div ref="bufferTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none" />
|
||||
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
||||
<div ref="trackCursor" class="h-full w-0.5 bg-gray-50 absolute top-0 left-0 opacity-0 pointer-events-none" />
|
||||
<div v-if="loading" class="h-full w-1/4 absolute left-0 top-0 loadingTrack pointer-events-none bg-white bg-opacity-25" />
|
||||
<div v-if="loading" class="h-full w-1/4 absolute left-0 top-0 loadingTrack pointer-events-none bg-white/25" />
|
||||
</div>
|
||||
<div class="w-full h-2 relative overflow-hidden" :class="useChapterTrack ? 'opacity-0' : ''">
|
||||
<template v-for="(tick, index) in chapterTicks">
|
||||
<div :key="index" :style="{ left: tick.left + 'px' }" class="absolute top-0 w-px bg-white bg-opacity-30 h-1 pointer-events-none" />
|
||||
<div :key="index" :style="{ left: tick.left + 'px' }" class="absolute top-0 w-px bg-white/30 h-1 pointer-events-none" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
@ -74,6 +74,9 @@ export default {
|
|||
currentChapterStart() {
|
||||
if (!this.currentChapter) return 0
|
||||
return this.currentChapter.start
|
||||
},
|
||||
isMobile() {
|
||||
return this.$store.state.globals.isMobile
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -145,6 +148,9 @@ export default {
|
|||
})
|
||||
},
|
||||
mousemoveTrack(e) {
|
||||
if (this.isMobile) {
|
||||
return
|
||||
}
|
||||
const offsetX = e.offsetX
|
||||
|
||||
const baseTime = this.useChapterTrack ? this.currentChapterStart : 0
|
||||
|
@ -198,6 +204,7 @@ export default {
|
|||
setTrackWidth() {
|
||||
if (this.$refs.track) {
|
||||
this.trackWidth = this.$refs.track.clientWidth
|
||||
this.trackOffsetLeft = this.$refs.track.getBoundingClientRect().left
|
||||
} else {
|
||||
console.error('Track not loaded', this.$refs)
|
||||
}
|
||||
|
@ -218,4 +225,4 @@ export default {
|
|||
window.removeEventListener('resize', this.windowResize)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue