mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-01-06 06:31:19 -05:00
Compare commits
270 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf5598aeb9 | ||
|
|
8cf3d648ea | ||
|
|
212311a980 | ||
|
|
c9522dc25d | ||
|
|
37af753402 | ||
|
|
d8c5627cf8 | ||
|
|
4f926b37db | ||
|
|
fefc16bd13 | ||
|
|
1b1b71a9b6 | ||
|
|
086532652e | ||
|
|
4e8b4720a1 | ||
|
|
4a7ada28fb | ||
|
|
1710285674 | ||
|
|
a6bb61d998 | ||
|
|
5ec05dfa84 | ||
|
|
83e854aa13 | ||
|
|
634f809159 | ||
|
|
e5cf141834 | ||
|
|
8610b68d3f | ||
|
|
f3e3bddc94 | ||
|
|
7ef3284cc5 | ||
|
|
3494586f77 | ||
|
|
faaf99e6bb | ||
|
|
1078ba2111 | ||
|
|
2ad69300f5 | ||
|
|
d2f3fa7fdf | ||
|
|
64fcb6270b | ||
|
|
562c30cff4 | ||
|
|
7108501d24 | ||
|
|
37eae3406c | ||
|
|
501dc938e6 | ||
|
|
c5ecd35fe9 | ||
|
|
7cd8d7f44d | ||
|
|
567a9a4e58 | ||
|
|
58f4a0cfbb | ||
|
|
e6c0b697aa | ||
|
|
35f60d699d | ||
|
|
c219be0970 | ||
|
|
c72ce843fa | ||
|
|
c606059a3a | ||
|
|
049a8bdc6d | ||
|
|
9752f744ca | ||
|
|
4be6fb789c | ||
|
|
afc56e5259 | ||
|
|
d47f8521d5 | ||
|
|
7f853d426a | ||
|
|
e9008c615d | ||
|
|
01f081ef5a | ||
|
|
7ee174e0d5 | ||
|
|
24439f86e0 | ||
|
|
fbd3ce3b72 | ||
|
|
96f8b54b51 | ||
|
|
9c94a78e29 | ||
|
|
a14e3dd137 | ||
|
|
e37673bd67 | ||
|
|
6aa10d20a1 | ||
|
|
68a92acb7a | ||
|
|
8aa7cc9ca5 | ||
|
|
e6c087c3bb | ||
|
|
39a2097152 | ||
|
|
6a8003917e | ||
|
|
d5a17ddc8c | ||
|
|
48bbf0d649 | ||
|
|
0bc58c254f | ||
|
|
b2d41f0583 | ||
|
|
0d31d20f0f | ||
|
|
bb7938f66d | ||
|
|
5b22e945da | ||
|
|
decde230aa | ||
|
|
1dec8ae122 | ||
|
|
8512d5e693 | ||
|
|
bb481ccfb4 | ||
|
|
12bce48ef5 | ||
|
|
013c7c776e | ||
|
|
8f96d20a23 | ||
|
|
1a8811b69a | ||
|
|
d796849d74 | ||
|
|
942bd0859f | ||
|
|
072028c740 | ||
|
|
0d08aecd56 | ||
|
|
22ad16e11b | ||
|
|
2f49a08c7d | ||
|
|
fcacda74cb | ||
|
|
fa0c90de70 | ||
|
|
c1197314ac | ||
|
|
0b31792660 | ||
|
|
b35fabbe55 | ||
|
|
8cd8a157a6 | ||
|
|
86aece6828 | ||
|
|
f9edadbafd | ||
|
|
6a388cd4fe | ||
|
|
9d17e9ff48 | ||
|
|
662b7d01b8 | ||
|
|
a19bc4b4e4 | ||
|
|
a545aa5c39 | ||
|
|
8493e56b11 | ||
|
|
21c77dccce | ||
|
|
55164803b0 | ||
|
|
5c49a8ce6a | ||
|
|
854f308eae | ||
|
|
16ba6b53ba | ||
|
|
0af29a378a | ||
|
|
def34a860b | ||
|
|
f8034e1b78 | ||
|
|
01fbea02f1 | ||
|
|
3d9af89e24 | ||
|
|
d430d9f3ed | ||
|
|
0c24a1e626 | ||
|
|
1099dbe642 | ||
|
|
2df3277dcd | ||
|
|
6ae14213f5 | ||
|
|
61bd029303 | ||
|
|
5b09bd8242 | ||
|
|
703477b157 | ||
|
|
03ff5d8ae1 | ||
|
|
220f7ef7cd | ||
|
|
682a99dd43 | ||
|
|
fac5de582d | ||
|
|
7cbf9de8ca | ||
|
|
ce213c3d89 | ||
|
|
32cd0360e6 | ||
|
|
1ec23a5699 | ||
|
|
48330f6432 | ||
|
|
28358debbc | ||
|
|
54b7ed6117 | ||
|
|
0cfd2ee63b | ||
|
|
37a0990741 | ||
|
|
7a0cd1eb34 | ||
|
|
ac3277da09 | ||
|
|
65d1e7be56 | ||
|
|
80685afa7e | ||
|
|
f892453892 | ||
|
|
422bb8c31c | ||
|
|
6fb1202c1c | ||
|
|
4ddd2788f0 | ||
|
|
8a28029809 | ||
|
|
423a2129d1 | ||
|
|
a338097514 | ||
|
|
84b67abb03 | ||
|
|
5ec8406653 | ||
|
|
b3ce300d32 | ||
|
|
3f93b93d9e | ||
|
|
e32c83db63 | ||
|
|
0344a63b48 | ||
|
|
24923c0009 | ||
|
|
a9036c9738 | ||
|
|
f9f7fbed33 | ||
|
|
53b5bee736 | ||
|
|
d0b3726905 | ||
|
|
7a6864507e | ||
|
|
e20563f2e1 | ||
|
|
fea5f8f3d4 | ||
|
|
f9bb529b85 | ||
|
|
60e348fcc1 | ||
|
|
f194c5be0e | ||
|
|
47712e63f1 | ||
|
|
790c1fb34a | ||
|
|
9cca731acc | ||
|
|
48f232790a | ||
|
|
3c55aa5f43 | ||
|
|
8c1edb30a6 | ||
|
|
5e64af4448 | ||
|
|
9f60017cfe | ||
|
|
b6a86d11d2 | ||
|
|
db86bfd63d | ||
|
|
7ff72a8920 | ||
|
|
2c4f86d148 | ||
|
|
1a9f26e804 | ||
|
|
42f8194bde | ||
|
|
8634b7058c | ||
|
|
fc276b330a | ||
|
|
5b22d7430a | ||
|
|
8883debc74 | ||
|
|
c92cb08f6f | ||
|
|
1254b668de | ||
|
|
48b703bf9f | ||
|
|
064679c057 | ||
|
|
ba23d258e7 | ||
|
|
98cd19d440 | ||
|
|
4c8b91e9d9 | ||
|
|
ba742563c2 | ||
|
|
f0e70ed27b | ||
|
|
acc4bdbc5e | ||
|
|
c45c82306e | ||
|
|
fd827b2214 | ||
|
|
df1c157994 | ||
|
|
a92e417581 | ||
|
|
6ad0719880 | ||
|
|
5383d0b5f7 | ||
|
|
b3cefc075d | ||
|
|
ac62d18007 | ||
|
|
fe14c26782 | ||
|
|
b33a3cabf9 | ||
|
|
6224163ecd | ||
|
|
05aabb2843 | ||
|
|
7d2d5f6bf4 | ||
|
|
c938685679 | ||
|
|
e6ecc28001 | ||
|
|
93fa6ba466 | ||
|
|
a8f459e4fa | ||
|
|
2441bb1cec | ||
|
|
25cc24fca5 | ||
|
|
ff4cbc6d5f | ||
|
|
f79bfae95d | ||
|
|
2f99efcc60 | ||
|
|
45b13571a5 | ||
|
|
04da8812df | ||
|
|
840304ee04 | ||
|
|
41bd9a9358 | ||
|
|
1e0a9918fd | ||
|
|
799acf5db8 | ||
|
|
1326d29fad | ||
|
|
9b35530956 | ||
|
|
0ae054c5d7 | ||
|
|
c72eac9987 | ||
|
|
159ccd807f | ||
|
|
5d13faef33 | ||
|
|
e0de59a4b6 | ||
|
|
519a1b0eaf | ||
|
|
4d8e1b7cef | ||
|
|
6d3e096e08 | ||
|
|
38edcdca4b | ||
|
|
8774e6be71 | ||
|
|
ec197b2e13 | ||
|
|
1c0d6e9c67 | ||
|
|
7d711da381 | ||
|
|
f66cea9829 | ||
|
|
5f572face5 | ||
|
|
88a4cf9f12 | ||
|
|
0b860e0d40 | ||
|
|
149bb3e5b2 | ||
|
|
7a7a779824 | ||
|
|
20a3657063 | ||
|
|
9c87c3a095 | ||
|
|
4de65b4369 | ||
|
|
996c78d760 | ||
|
|
ccdc3d60c4 | ||
|
|
8be08882d8 | ||
|
|
26d2c5a8f0 | ||
|
|
bae39e3a2d | ||
|
|
bb1a72269a | ||
|
|
9674cfd258 | ||
|
|
627ddd2f70 | ||
|
|
27b3a44147 | ||
|
|
5308fd8b46 | ||
|
|
1b914d5d4f | ||
|
|
9e0f17f7c6 | ||
|
|
1320b6d785 | ||
|
|
f1ddbeadaf | ||
|
|
f9f89e1e51 | ||
|
|
bbf214fa4c | ||
|
|
f1582177e1 | ||
|
|
d5712a564c | ||
|
|
1c274862d8 | ||
|
|
663c9e0fa9 | ||
|
|
bcb0bc75c9 | ||
|
|
603823d6ea | ||
|
|
20c04d3ed3 | ||
|
|
02e5d608d0 | ||
|
|
e53ac6566b | ||
|
|
2472b86284 | ||
|
|
29a15858f4 | ||
|
|
afc16358ca | ||
|
|
9facf77ff1 | ||
|
|
1923854202 | ||
|
|
9cd92c7b7f | ||
|
|
8e0b723207 | ||
|
|
68ef3a07a7 | ||
|
|
202ceb02b5 | ||
|
|
59370cae81 |
55
.github/workflows/apply_comments.yaml
vendored
Normal file
55
.github/workflows/apply_comments.yaml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: Add issue comments by label
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- labeled
|
||||
jobs:
|
||||
help-wanted:
|
||||
if: github.event.label.name == 'help wanted'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Help wanted comment
|
||||
run: gh issue comment "$NUMBER" --body "$BODY"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
NUMBER: ${{ github.event.issue.number }}
|
||||
BODY: >
|
||||
This issue is not able to be completed due to limited bandwidth or access to the required test hardware.
|
||||
|
||||
This issue is available for anyone to work on.
|
||||
|
||||
|
||||
config-issue:
|
||||
if: github.event.label.name == 'config-issue'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Config issue comment
|
||||
run: gh issue close "$NUMBER" --reason "not planned" --comment "$BODY"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
NUMBER: ${{ github.event.issue.number }}
|
||||
BODY: >
|
||||
After reviewing this issue, this appears to be a problem with your setup and not Audiobookshelf. This issue is being closed to keep the issue tracker focused on Audiobookshelf itself. Please reach out on the Audiobookshelf Discord for community support.
|
||||
|
||||
Some common search terms to help you find the solution to your problem:
|
||||
- Reverse proxy
|
||||
- Enabling websockets
|
||||
- SSL (https vs http)
|
||||
- Configuring a static IP
|
||||
- `localhost` versus IP address
|
||||
- hairpin NAT
|
||||
- VPN
|
||||
- firewall ports
|
||||
- public versus private network
|
||||
- bridge versus host mode
|
||||
- Docker networking
|
||||
- DNS (such as EAI_AGAIN errors)
|
||||
|
||||
After you have followed these steps, please post the solution or steps you followed to fix the problem to help others in the future, or show that it is a problem with Audiobookshelf so we can reopen the issue.
|
||||
|
||||
1
.github/workflows/docker-build.yml
vendored
1
.github/workflows/docker-build.yml
vendored
@@ -70,6 +70,7 @@ jobs:
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
tags: ${{ github.event.inputs.tags || steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
|
||||
12
.github/workflows/lint-openapi.yml
vendored
12
.github/workflows/lint-openapi.yml
vendored
@@ -1,13 +1,15 @@
|
||||
name: API linting
|
||||
|
||||
# Run on pull requests or pushes when there is a change to the OpenAPI file
|
||||
# Run on pull requests or pushes when there is a change to any OpenAPI files in docs/
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
paths:
|
||||
- docs/
|
||||
pull_request:
|
||||
paths:
|
||||
- docs/
|
||||
- 'docs/**'
|
||||
|
||||
# This action only needs read permissions
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,6 +16,7 @@
|
||||
/ffmpeg*
|
||||
/ffprobe*
|
||||
/unicode*
|
||||
/libnusqlite3*
|
||||
|
||||
sw.*
|
||||
.DS_STORE
|
||||
|
||||
34
Dockerfile
34
Dockerfile
@@ -11,20 +11,36 @@ FROM node:20-alpine
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache --update \
|
||||
curl \
|
||||
tzdata \
|
||||
ffmpeg \
|
||||
make \
|
||||
gcompat \
|
||||
python3 \
|
||||
g++ \
|
||||
tini
|
||||
apk add --no-cache --update \
|
||||
curl \
|
||||
tzdata \
|
||||
ffmpeg \
|
||||
make \
|
||||
gcompat \
|
||||
python3 \
|
||||
g++ \
|
||||
tini \
|
||||
unzip
|
||||
|
||||
COPY --from=build /client/dist /client/dist
|
||||
COPY index.js package* /
|
||||
COPY server server
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
ENV NUSQLITE3_DIR="/usr/local/lib/nusqlite3"
|
||||
ENV NUSQLITE3_PATH="${NUSQLITE3_DIR}/libnusqlite3.so"
|
||||
|
||||
RUN case "$TARGETPLATFORM" in \
|
||||
"linux/amd64") \
|
||||
curl -L -o /tmp/library.zip "https://github.com/mikiher/nunicode-sqlite/releases/download/v1.1/libnusqlite3-linux-x64.zip" ;; \
|
||||
"linux/arm64") \
|
||||
curl -L -o /tmp/library.zip "https://github.com/mikiher/nunicode-sqlite/releases/download/v1.1/libnusqlite3-linux-arm64.zip" ;; \
|
||||
*) echo "Unsupported platform: $TARGETPLATFORM" && exit 1 ;; \
|
||||
esac && \
|
||||
unzip /tmp/library.zip -d $NUSQLITE3_DIR && \
|
||||
rm /tmp/library.zip
|
||||
|
||||
RUN npm ci --only=production
|
||||
|
||||
RUN apk del make python3 g++
|
||||
|
||||
@@ -2,14 +2,7 @@
|
||||
font-family: 'Material Symbols Rounded';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(~static/fonts/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].woff2) format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(~static/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2) format('woff2');
|
||||
src: url(~static/fonts/MaterialSymbolsRounded.woff2) format('woff2');
|
||||
}
|
||||
|
||||
.material-symbols {
|
||||
@@ -32,26 +25,6 @@
|
||||
'FILL' 1
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.material-symbols-outlined.fill {
|
||||
font-variation-settings:
|
||||
'FILL' 1
|
||||
}
|
||||
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="flex-grow" />
|
||||
|
||||
<ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
|
||||
<span class="material-symbols-outlined text-2xl text-warning text-opacity-50"> cast </span>
|
||||
<span class="material-symbols text-2xl text-warning text-opacity-50"> cast </span>
|
||||
</ui-tooltip>
|
||||
<div v-if="isChromecastInitialized" class="w-6 min-w-6 h-6 ml-2 mr-1 sm:mx-2 cursor-pointer">
|
||||
<google-cast-launcher></google-cast-launcher>
|
||||
@@ -26,19 +26,19 @@
|
||||
|
||||
<nuxt-link v-if="currentLibrary" to="/config/stats" class="hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
|
||||
<ui-tooltip :text="$strings.HeaderYourStats" direction="bottom" class="flex items-center">
|
||||
<span class="material-symbols text-2xl" aria-label="User Stats" role="button">equalizer</span>
|
||||
<span class="material-symbols text-2xl" aria-label="User Stats" role="button"></span>
|
||||
</ui-tooltip>
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="userCanUpload && currentLibrary" to="/upload" class="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
||||
<ui-tooltip :text="$strings.ButtonUpload" direction="bottom" class="flex items-center">
|
||||
<span class="material-symbols text-2xl" aria-label="Upload Media" role="button">upload</span>
|
||||
<span class="material-symbols text-2xl" aria-label="Upload Media" role="button"></span>
|
||||
</ui-tooltip>
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="userIsAdminOrUp" to="/config" class="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
||||
<ui-tooltip :text="$strings.HeaderSettings" direction="bottom" class="flex items-center">
|
||||
<span class="material-symbols text-2xl" aria-label="System Settings" role="button">settings</span>
|
||||
<span class="material-symbols text-2xl" aria-label="System Settings" role="button"></span>
|
||||
</ui-tooltip>
|
||||
</nuxt-link>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<span class="block truncate">{{ username }}</span>
|
||||
</span>
|
||||
<span class="h-full md:ml-3 md:absolute inset-y-0 md:right-0 flex items-center justify-center md:pr-2 pointer-events-none">
|
||||
<span class="material-symbols text-xl text-gray-100">person</span>
|
||||
<span class="material-symbols text-xl text-gray-100"></span>
|
||||
</span>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
@@ -264,7 +264,6 @@ export default {
|
||||
libraryItems.forEach((item) => {
|
||||
let subtitle = ''
|
||||
if (item.mediaType === 'book') subtitle = item.media.metadata.authors.map((au) => au.name).join(', ')
|
||||
else if (item.mediaType === 'music') subtitle = item.media.metadata.artists.join(', ')
|
||||
queueItems.push({
|
||||
libraryItemId: item.id,
|
||||
libraryId: item.libraryId,
|
||||
@@ -332,13 +331,13 @@ export default {
|
||||
libraryItemIds: this.selectedMediaItems.map((i) => i.id)
|
||||
})
|
||||
.then(() => {
|
||||
this.$toast.success('Batch delete success')
|
||||
this.$toast.success(this.$strings.ToastBatchDeleteSuccess)
|
||||
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Batch delete failed', error)
|
||||
this.$toast.error('Batch delete failed')
|
||||
this.$toast.error(this.$strings.ToastBatchDeleteFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.$store.commit('setProcessingBatch', false)
|
||||
|
||||
@@ -347,6 +347,13 @@ export default {
|
||||
libraryItemsAdded(libraryItems) {
|
||||
console.log('libraryItems added', libraryItems)
|
||||
|
||||
// First items added to library
|
||||
const isThisLibrary = libraryItems.some((li) => li.libraryId === this.currentLibraryId)
|
||||
if (!this.shelves.length && !this.search && isThisLibrary) {
|
||||
this.fetchCategories()
|
||||
return
|
||||
}
|
||||
|
||||
const recentlyAddedShelf = this.shelves.find((shelf) => shelf.id === 'recently-added')
|
||||
if (!recentlyAddedShelf) return
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="flex-grow h-full flex justify-center items-center" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<p v-if="isPlaylistsPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonPlaylists }}</p>
|
||||
<span v-else class="material-symbols-outlined text-lg">queue_music</span>
|
||||
<span v-else class="material-symbols text-lg"></span>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
|
||||
<span v-else class="material-symbols-outlined text-lg">collections_bookmark</span>
|
||||
<span v-else class="material-symbols text-lg"></span>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
|
||||
@@ -50,7 +50,7 @@
|
||||
{{ seriesName }}
|
||||
</p>
|
||||
<div class="w-6 h-6 rounded-full bg-black bg-opacity-30 flex items-center justify-center ml-3">
|
||||
<span class="font-mono">{{ numShowing }}</span>
|
||||
<span class="font-mono">{{ $formatNumber(numShowing) }}</span>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
</template>
|
||||
<!-- library & collections page -->
|
||||
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
|
||||
<p class="hidden md:block">{{ numShowing }} {{ entityName }}</p>
|
||||
<p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p>
|
||||
|
||||
<div class="flex-grow hidden sm:inline-block" />
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<controls-sort-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesSortBy" :descending.sync="settings.seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" />
|
||||
|
||||
<!-- issues page remove all button -->
|
||||
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn>
|
||||
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ $formatNumber(numShowing) }} {{ entityName }}</ui-btn>
|
||||
|
||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
|
||||
</template>
|
||||
@@ -159,6 +159,7 @@ export default {
|
||||
}
|
||||
|
||||
this.addSubtitlesMenuItem(items)
|
||||
this.addCollapseSubSeriesMenuItem(items)
|
||||
|
||||
return items
|
||||
},
|
||||
@@ -245,9 +246,6 @@ export default {
|
||||
isPodcastLibrary() {
|
||||
return this.currentLibraryMediaType === 'podcast'
|
||||
},
|
||||
isMusicLibrary() {
|
||||
return this.currentLibraryMediaType === 'music'
|
||||
},
|
||||
isLibraryPage() {
|
||||
return this.page === ''
|
||||
},
|
||||
@@ -280,7 +278,6 @@ export default {
|
||||
},
|
||||
entityName() {
|
||||
if (this.isAlbumsPage) return 'Albums'
|
||||
if (this.isMusicLibrary) return 'Tracks'
|
||||
|
||||
if (this.isPodcastLibrary) return this.$strings.LabelPodcasts
|
||||
if (!this.page) return this.$strings.LabelBooks
|
||||
@@ -371,6 +368,21 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
addCollapseSubSeriesMenuItem(items) {
|
||||
if (this.selectedSeries && this.isBookLibrary && !this.isBatchSelecting) {
|
||||
if (this.settings.collapseBookSeries) {
|
||||
items.push({
|
||||
text: this.$strings.LabelExpandSubSeries,
|
||||
action: 'expand-sub-series'
|
||||
})
|
||||
} else {
|
||||
items.push({
|
||||
text: this.$strings.LabelCollapseSubSeries,
|
||||
action: 'collapse-sub-series'
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
handleSubtitlesAction(action) {
|
||||
if (action === 'show-subtitles') {
|
||||
this.settings.showSubtitles = true
|
||||
@@ -397,6 +409,19 @@ export default {
|
||||
}
|
||||
return false
|
||||
},
|
||||
handleCollapseSubSeriesAction(action) {
|
||||
if (action === 'collapse-sub-series') {
|
||||
this.settings.collapseBookSeries = true
|
||||
this.updateCollapseSubSeries()
|
||||
return true
|
||||
}
|
||||
if (action === 'expand-sub-series') {
|
||||
this.settings.collapseBookSeries = false
|
||||
this.updateCollapseSubSeries()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
contextMenuAction({ action }) {
|
||||
if (action === 'export-opml') {
|
||||
this.exportOPML()
|
||||
@@ -427,6 +452,8 @@ export default {
|
||||
this.markSeriesFinished()
|
||||
} else if (this.handleSubtitlesAction(action)) {
|
||||
return
|
||||
} else if (this.handleCollapseSubSeriesAction(action)) {
|
||||
return
|
||||
}
|
||||
},
|
||||
showOpenSeriesRSSFeed() {
|
||||
@@ -442,11 +469,11 @@ export default {
|
||||
this.$axios
|
||||
.$get(`/api/me/series/${this.seriesId}/readd-to-continue-listening`)
|
||||
.then(() => {
|
||||
this.$toast.success('Series re-added to continue listening')
|
||||
this.$toast.success(this.$strings.ToastItemUpdateSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to re-add series to continue listening', error)
|
||||
this.$toast.error('Failed to re-add series to continue listening')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processingSeries = false
|
||||
@@ -473,7 +500,7 @@ export default {
|
||||
})
|
||||
if (!response) {
|
||||
console.error(`Author ${author.name} not found`)
|
||||
this.$toast.error(`Author ${author.name} not found`)
|
||||
this.$toast.error(this.$getString('ToastAuthorNotFound', [author.name]))
|
||||
} else if (response.updated) {
|
||||
if (response.author.imagePath) console.log(`Author ${response.author.name} was updated`)
|
||||
else console.log(`Author ${response.author.name} was updated (no image found)`)
|
||||
@@ -491,13 +518,13 @@ export default {
|
||||
this.$axios
|
||||
.$delete(`/api/libraries/${this.currentLibraryId}/issues`)
|
||||
.then(() => {
|
||||
this.$toast.success('Removed library items with issues')
|
||||
this.$toast.success(this.$strings.ToastRemoveItemsWithIssuesSuccess)
|
||||
this.$router.push(`/library/${this.currentLibraryId}/bookshelf`)
|
||||
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove library items with issues', error)
|
||||
this.$toast.error('Failed to remove library items with issues')
|
||||
this.$toast.error(this.$strings.ToastRemoveItemsWithIssuesFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processingIssues = false
|
||||
@@ -553,7 +580,7 @@ export default {
|
||||
updateCollapseSeries() {
|
||||
this.saveSettings()
|
||||
},
|
||||
updateCollapseBookSeries() {
|
||||
updateCollapseSubSeries() {
|
||||
this.saveSettings()
|
||||
},
|
||||
updateShowSubtitles() {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<template>
|
||||
<div v-if="streamLibraryItem" id="mediaPlayerContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 lg:h-40 z-50 bg-primary px-2 lg:px-4 pb-1 lg:pb-4 pt-2">
|
||||
<div id="videoDock" />
|
||||
<div class="absolute left-2 top-2 lg:left-4 cursor-pointer">
|
||||
<covers-book-cover expand-on-click :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
||||
</div>
|
||||
<div class="flex items-start mb-6 lg:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
|
||||
<div class="flex items-start mb-6 lg:mb-0" :class="isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
|
||||
<div class="min-w-0 w-full">
|
||||
<div class="flex items-center">
|
||||
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
|
||||
@@ -12,10 +11,9 @@
|
||||
</nuxt-link>
|
||||
<widgets-explicit-indicator v-if="isExplicit" />
|
||||
</div>
|
||||
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5">
|
||||
<div class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5">
|
||||
<span class="material-symbols text-sm">person</span>
|
||||
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
|
||||
<div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div>
|
||||
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate">
|
||||
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
||||
</div>
|
||||
@@ -43,12 +41,14 @@
|
||||
:sleep-timer-remaining="sleepTimerRemaining"
|
||||
:sleep-timer-type="sleepTimerType"
|
||||
:is-podcast="isPodcast"
|
||||
:hasNextItemInQueue="hasNextItemInQueue"
|
||||
@playPause="playPause"
|
||||
@jumpForward="jumpForward"
|
||||
@jumpBackward="jumpBackward"
|
||||
@setVolume="setVolume"
|
||||
@setPlaybackRate="setPlaybackRate"
|
||||
@seek="seek"
|
||||
@nextItemInQueue="playNextItemInQueue"
|
||||
@close="closePlayer"
|
||||
@showBookmarks="showBookmarks"
|
||||
@showSleepTimer="showSleepTimerModal = true"
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-type="sleepTimerType" :remaining="sleepTimerRemaining" :has-chapters="!!chapters.length" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
|
||||
|
||||
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" :library-item-id="libraryItemId" />
|
||||
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" />
|
||||
|
||||
<modals-player-settings-modal v-model="showPlayerSettingsModal" />
|
||||
</div>
|
||||
@@ -138,9 +138,6 @@ export default {
|
||||
isPodcast() {
|
||||
return this.streamLibraryItem?.mediaType === 'podcast'
|
||||
},
|
||||
isMusic() {
|
||||
return this.streamLibraryItem?.mediaType === 'music'
|
||||
},
|
||||
isExplicit() {
|
||||
return !!this.mediaMetadata.explicit
|
||||
},
|
||||
@@ -172,9 +169,15 @@ export default {
|
||||
if (!this.isPodcast) return null
|
||||
return this.mediaMetadata.author || 'Unknown'
|
||||
},
|
||||
musicArtists() {
|
||||
if (!this.isMusic) return null
|
||||
return this.mediaMetadata.artists.join(', ')
|
||||
hasNextItemInQueue() {
|
||||
return this.currentPlayerQueueIndex < this.playerQueueItems.length - 1
|
||||
},
|
||||
currentPlayerQueueIndex() {
|
||||
if (!this.libraryItemId) return -1
|
||||
return this.playerQueueItems.findIndex((i) => {
|
||||
if (this.streamEpisode?.id) return i.episodeId === this.streamEpisode.id
|
||||
return i.libraryItemId === this.libraryItemId
|
||||
})
|
||||
},
|
||||
playerQueueItems() {
|
||||
return this.$store.state.playerQueueItems || []
|
||||
@@ -460,6 +463,30 @@ export default {
|
||||
this.playerHandler.switchPlayer()
|
||||
}
|
||||
},
|
||||
playNextItemInQueue() {
|
||||
if (this.hasNextItemInQueue) {
|
||||
this.playQueueItem({ index: this.currentPlayerQueueIndex + 1 })
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {{ index: number }} payload
|
||||
*/
|
||||
playQueueItem(payload) {
|
||||
if (payload?.index === undefined) {
|
||||
console.error('playQueueItem: No index provided')
|
||||
return
|
||||
}
|
||||
if (!this.playerQueueItems[payload.index]) {
|
||||
console.error('playQueueItem: No item found at index', payload.index)
|
||||
return
|
||||
}
|
||||
const item = this.playerQueueItems[payload.index]
|
||||
this.playLibraryItem({
|
||||
libraryItemId: item.libraryItemId,
|
||||
episodeId: item.episodeId || null,
|
||||
queueItems: this.playerQueueItems
|
||||
})
|
||||
},
|
||||
async playLibraryItem(payload) {
|
||||
const libraryItemId = payload.libraryItemId
|
||||
const episodeId = payload.episodeId || null
|
||||
@@ -512,6 +539,7 @@ export default {
|
||||
this.$eventBus.$on('cast-session-active', this.castSessionActive)
|
||||
this.$eventBus.$on('playback-seek', this.seek)
|
||||
this.$eventBus.$on('playback-time-update', this.playbackTimeUpdate)
|
||||
this.$eventBus.$on('play-queue-item', this.playQueueItem)
|
||||
this.$eventBus.$on('play-item', this.playLibraryItem)
|
||||
this.$eventBus.$on('pause-item', this.pauseItem)
|
||||
},
|
||||
@@ -519,6 +547,7 @@ export default {
|
||||
this.$eventBus.$off('cast-session-active', this.castSessionActive)
|
||||
this.$eventBus.$off('playback-seek', this.seek)
|
||||
this.$eventBus.$off('playback-time-update', this.playbackTimeUpdate)
|
||||
this.$eventBus.$off('play-queue-item', this.playQueueItem)
|
||||
this.$eventBus.$off('play-item', this.playLibraryItem)
|
||||
this.$eventBus.$off('pause-item', this.pauseItem)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-symbols text-2xl">format_list_bulleted</span>
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
|
||||
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-symbols-outlined text-2xl">collections_bookmark</span>
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
|
||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-symbols text-2.5xl">queue_music</span>
|
||||
<span class="material-symbols text-2.5xl"></span>
|
||||
|
||||
<p class="pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/narrators`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isNarratorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-symbols text-2xl">record_voice_over</span>
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
|
||||
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.LabelNarrators }}</p>
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/stats`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isStatsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-symbols text-2xl">monitoring</span>
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
|
||||
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonStats }}</p>
|
||||
|
||||
@@ -95,16 +95,8 @@
|
||||
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isMusicLibrary" :to="`/library/${currentLibraryId}/bookshelf/albums`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isMusicAlbumsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-symbols-outlined text-xl">album</span>
|
||||
|
||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">Albums</p>
|
||||
|
||||
<div v-show="isMusicAlbumsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-symbols text-2xl">file_download</span>
|
||||
<span class="material-symbols text-2xl"></span>
|
||||
|
||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonDownloadQueue }}</p>
|
||||
|
||||
@@ -172,9 +164,6 @@ export default {
|
||||
isPodcastLibrary() {
|
||||
return this.currentLibraryMediaType === 'podcast'
|
||||
},
|
||||
isMusicLibrary() {
|
||||
return this.currentLibraryMediaType === 'music'
|
||||
},
|
||||
isPodcastDownloadQueuePage() {
|
||||
return this.$route.name === 'library-library-podcast-download-queue'
|
||||
},
|
||||
@@ -184,9 +173,6 @@ export default {
|
||||
isPodcastLatestPage() {
|
||||
return this.$route.name === 'library-library-podcast-latest'
|
||||
},
|
||||
isMusicAlbumsPage() {
|
||||
return this.paramId === 'albums'
|
||||
},
|
||||
homePage() {
|
||||
return this.$route.name === 'library-library'
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<p class="truncate text-sm">{{ title }}</p>
|
||||
|
||||
<p class="truncate text-xs text-gray-300">{{ description }}</p>
|
||||
<p v-if="specialMessage" class="truncate text-xs text-gray-300">{{ specialMessage }}</p>
|
||||
|
||||
<p v-if="isFailed && failedMessage" class="text-xs truncate text-red-500">{{ failedMessage }}</p>
|
||||
<p v-else-if="!isFinished && cancelingScan" class="text-xs truncate">Canceling...</p>
|
||||
@@ -26,7 +27,16 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
cancelingScan: false
|
||||
cancelingScan: false,
|
||||
specialMessage: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
task: {
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.initTask()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -34,14 +44,17 @@ export default {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
title() {
|
||||
if (this.task.titleKey && this.$strings[this.task.titleKey]) {
|
||||
return this.$getString(this.task.titleKey, this.task.titleSubs)
|
||||
}
|
||||
return this.task.title || 'No Title'
|
||||
},
|
||||
description() {
|
||||
if (this.task.descriptionKey && this.$strings[this.task.descriptionKey]) {
|
||||
return this.$getString(this.task.descriptionKey, this.task.descriptionSubs)
|
||||
}
|
||||
return this.task.description || ''
|
||||
},
|
||||
details() {
|
||||
return this.task.details || 'Unknown'
|
||||
},
|
||||
isFinished() {
|
||||
return !!this.task.isFinished
|
||||
},
|
||||
@@ -52,6 +65,9 @@ export default {
|
||||
return this.isFinished && !this.isFailed
|
||||
},
|
||||
failedMessage() {
|
||||
if (this.task.errorKey && this.$strings[this.task.errorKey]) {
|
||||
return this.$getString(this.task.errorKey, this.task.errorSubs)
|
||||
}
|
||||
return this.task.error || ''
|
||||
},
|
||||
action() {
|
||||
@@ -87,6 +103,21 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initTask() {
|
||||
// special message for library scan tasks
|
||||
if (this.task?.data?.scanResults) {
|
||||
const scanResults = this.task.data.scanResults
|
||||
const strs = []
|
||||
if (scanResults.added) strs.push(this.$getString('MessageTaskScanItemsAdded', [scanResults.added]))
|
||||
if (scanResults.updated) strs.push(this.$getString('MessageTaskScanItemsUpdated', [scanResults.updated]))
|
||||
if (scanResults.missing) strs.push(this.$getString('MessageTaskScanItemsMissing', [scanResults.missing]))
|
||||
const changesDetected = strs.length > 0 ? strs.join(', ') : this.$strings.MessageTaskScanNoChangesNeeded
|
||||
const timeElapsed = scanResults.elapsed ? ` (${this.$elapsedPretty(scanResults.elapsed / 1000, false, true)})` : ''
|
||||
this.specialMessage = `${changesDetected}${timeElapsed}`
|
||||
} else {
|
||||
this.specialMessage = ''
|
||||
}
|
||||
},
|
||||
cancelScan() {
|
||||
const libraryId = this.task?.data?.libraryId
|
||||
if (!libraryId) {
|
||||
|
||||
@@ -201,23 +201,6 @@ export default {
|
||||
// This method returns immediately without waiting for the DOM to update
|
||||
return this.coverWidth
|
||||
},
|
||||
/*
|
||||
cardHeight() {
|
||||
// This method returns immediately without waiting for the DOM to update
|
||||
return this.coverHeight + this.detailsHeight
|
||||
},
|
||||
detailsHeight() {
|
||||
if (!this.isAlternativeBookshelfView) return 0
|
||||
const lineHeight = 1.5
|
||||
const remSize = 16
|
||||
const baseHeight = this.sizeMultiplier * lineHeight * remSize
|
||||
const titleHeight = 0.9 * baseHeight
|
||||
const line2Height = 0.8 * baseHeight
|
||||
const line3Height = this.displaySortLine ? 0.8 * baseHeight : 0
|
||||
const marginHeight = 8 * 2 * this.sizeMultiplier // py-2
|
||||
return titleHeight + line2Height + line3Height + marginHeight
|
||||
},
|
||||
*/
|
||||
sizeMultiplier() {
|
||||
return this.store.getters['user/getSizeMultiplier']
|
||||
},
|
||||
@@ -243,9 +226,6 @@ export default {
|
||||
isPodcast() {
|
||||
return this.mediaType === 'podcast' || this.store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast'
|
||||
},
|
||||
isMusic() {
|
||||
return this.mediaType === 'music'
|
||||
},
|
||||
isExplicit() {
|
||||
return this.mediaMetadata.explicit || false
|
||||
},
|
||||
@@ -353,7 +333,6 @@ export default {
|
||||
displayLineTwo() {
|
||||
if (this.recentEpisode) return this.title
|
||||
if (this.isPodcast) return this.author
|
||||
if (this.isMusic) return this.artist
|
||||
if (this.collapsedSeries) return ''
|
||||
if (this.isAuthorBookshelfView) {
|
||||
return this.mediaMetadata.publishedYear || ''
|
||||
@@ -363,14 +342,14 @@ export default {
|
||||
},
|
||||
displaySortLine() {
|
||||
if (this.collapsedSeries) return null
|
||||
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)
|
||||
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)
|
||||
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt, this.dateFormat)
|
||||
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
||||
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
||||
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
|
||||
if (this.orderBy === 'mtimeMs') return this.$getString('LabelFileModifiedDate', [this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)])
|
||||
if (this.orderBy === 'birthtimeMs') return this.$getString('LabelFileBornDate', [this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)])
|
||||
if (this.orderBy === 'addedAt') return this.$getString('LabelAddedDate', [this.$formatDate(this._libraryItem.addedAt, this.dateFormat)])
|
||||
if (this.orderBy === 'media.duration') return this.$strings.LabelDuration + ': ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
||||
if (this.orderBy === 'size') return this.$strings.LabelSize + ': ' + this.$bytesPretty(this._libraryItem.size)
|
||||
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} ` + this.$strings.LabelEpisodes
|
||||
if (this.orderBy === 'media.metadata.publishedYear') {
|
||||
if (this.mediaMetadata.publishedYear) return 'Published ' + this.mediaMetadata.publishedYear
|
||||
if (this.mediaMetadata.publishedYear) return this.$getString('LabelPublishedDate', [this.mediaMetadata.publishedYear])
|
||||
return '\u00A0'
|
||||
}
|
||||
return null
|
||||
@@ -381,7 +360,6 @@ export default {
|
||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
|
||||
},
|
||||
userProgress() {
|
||||
if (this.isMusic) return null
|
||||
if (this.episodeProgress) return this.episodeProgress
|
||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||
},
|
||||
@@ -437,7 +415,7 @@ export default {
|
||||
return !this.isSelectionMode && !this.showPlayButton && this.ebookFormat
|
||||
},
|
||||
showPlayButton() {
|
||||
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode || this.isMusic)
|
||||
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode)
|
||||
},
|
||||
showSmallEBookIcon() {
|
||||
return !this.isSelectionMode && this.ebookFormat
|
||||
@@ -481,8 +459,6 @@ export default {
|
||||
return this.store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
moreMenuItems() {
|
||||
if (this.isMusic) return []
|
||||
|
||||
if (this.recentEpisode) {
|
||||
const items = [
|
||||
{
|
||||
@@ -727,7 +703,7 @@ export default {
|
||||
toggleFinished(confirmed = false) {
|
||||
if (!this.itemIsFinished && this.userProgressPercent > 0 && !confirmed) {
|
||||
const payload = {
|
||||
message: `Are you sure you want to mark "${this.displayTitle}" as finished?`,
|
||||
message: this.$getString('MessageConfirmMarkItemFinished', [this.displayTitle]),
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.toggleFinished(true)
|
||||
@@ -772,18 +748,18 @@ export default {
|
||||
.then((data) => {
|
||||
var result = data.result
|
||||
if (!result) {
|
||||
this.$toast.error(`Re-Scan Failed for "${this.title}"`)
|
||||
this.$toast.error(this.$getString('ToastRescanFailed', [this.displayTitle]))
|
||||
} else if (result === 'UPDATED') {
|
||||
this.$toast.success(`Re-Scan complete item was updated`)
|
||||
this.$toast.success(this.$strings.ToastRescanUpdated)
|
||||
} else if (result === 'UPTODATE') {
|
||||
this.$toast.success(`Re-Scan complete item was up to date`)
|
||||
this.$toast.success(this.$strings.ToastRescanUpToDate)
|
||||
} else if (result === 'REMOVED') {
|
||||
this.$toast.error(`Re-Scan complete item was removed`)
|
||||
this.$toast.error(this.$strings.ToastRescanRemoved)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to scan library item', error)
|
||||
this.$toast.error('Failed to scan library item')
|
||||
this.$toast.error(this.$strings.ToastScanFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
@@ -840,7 +816,7 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove series from home', error)
|
||||
this.$toast.error('Failed to update user')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
@@ -858,7 +834,7 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to hide item from home', error)
|
||||
this.$toast.error('Failed to update user')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
@@ -873,7 +849,7 @@ export default {
|
||||
episodeId: this.recentEpisode.id,
|
||||
title: this.recentEpisode.title,
|
||||
subtitle: this.mediaMetadata.title,
|
||||
caption: this.recentEpisode.publishedAt ? `Published ${this.$formatDate(this.recentEpisode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||
caption: this.recentEpisode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(this.recentEpisode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
|
||||
duration: this.recentEpisode.audioFile.duration || null,
|
||||
coverPath: this.media.coverPath || null
|
||||
}
|
||||
@@ -923,11 +899,11 @@ export default {
|
||||
axios
|
||||
.$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`)
|
||||
.then(() => {
|
||||
this.$toast.success('Item deleted')
|
||||
this.$toast.success(this.$strings.ToastItemDeletedSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to delete item', error)
|
||||
this.$toast.error('Failed to delete item')
|
||||
this.$toast.error(this.$strings.ToastItemDeletedFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
@@ -1033,7 +1009,7 @@ export default {
|
||||
episodeId: episode.id,
|
||||
title: episode.title,
|
||||
subtitle: this.mediaMetadata.title,
|
||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||
caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
|
||||
duration: episode.audioFile.duration || null,
|
||||
coverPath: this.media.coverPath || null
|
||||
})
|
||||
|
||||
@@ -57,23 +57,11 @@ export default {
|
||||
return this.store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
cardWidth() {
|
||||
return this.width || this.coverHeight * 2
|
||||
return this.width || (this.coverHeight / this.bookCoverAspectRatio) * 2
|
||||
},
|
||||
coverHeight() {
|
||||
return this.height * this.sizeMultiplier
|
||||
},
|
||||
cardHeight() {
|
||||
return this.coverHeight + this.bottomTextHeight
|
||||
},
|
||||
bottomTextHeight() {
|
||||
if (!this.isAlternativeBookshelfView) return 0 // bottom text appears on top of the divider
|
||||
const lineHeight = 1.5
|
||||
const remSize = 16
|
||||
const baseHeight = this.sizeMultiplier * lineHeight * remSize
|
||||
const titleHeight = this.labelFontSize * baseHeight
|
||||
const paddingHeight = 4 * 2 * this.sizeMultiplier // py-1
|
||||
return titleHeight + paddingHeight
|
||||
},
|
||||
labelFontSize() {
|
||||
if (this.width < 160) return 0.75
|
||||
return 0.9
|
||||
|
||||
@@ -65,7 +65,7 @@ export default {
|
||||
return this.store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
cardWidth() {
|
||||
return this.width || this.coverHeight * 2
|
||||
return this.width || (this.coverHeight / this.bookCoverAspectRatio) * 2
|
||||
},
|
||||
coverHeight() {
|
||||
return this.height * this.sizeMultiplier
|
||||
@@ -96,7 +96,7 @@ export default {
|
||||
displaySortLine() {
|
||||
switch (this.orderBy) {
|
||||
case 'addedAt':
|
||||
return `${this.$strings.LabelAdded} ${this.$formatDate(this.addedAt, this.dateFormat)}`
|
||||
return this.$getString('LabelAddedDate', [this.$formatDate(this.addedAt, this.dateFormat)])
|
||||
case 'totalDuration':
|
||||
return `${this.$strings.LabelDuration} ${this.$elapsedPrettyExtended(this.totalDuration, false)}`
|
||||
case 'lastBookUpdated':
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(name)}`">
|
||||
<div cy-id="card" :style="{ width: cardWidth + 'px', height: cardHeight + 'px', fontSize: sizeMultiplier + 'rem' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
||||
<div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-40">
|
||||
<span class="material-symbols-outlined text-[10em]">record_voice_over</span>
|
||||
<span class="material-symbols text-[10em]"></span>
|
||||
</div>
|
||||
|
||||
<!-- Narrator name & num books overlay -->
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex h-full px-1 overflow-hidden">
|
||||
<div class="w-10 h-10 flex items-center justify-center">
|
||||
<span class="material-symbols text-2xl text-gray-200">record_voice_over</span>
|
||||
<span class="material-symbols text-2xl text-gray-200"></span>
|
||||
</div>
|
||||
<div class="flex-grow px-2 narratorSearchCardContent h-full">
|
||||
<p class="truncate text-sm">{{ narrator }}</p>
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
<p class="text-base md:text-lg font-semibold pr-4">{{ eventName }}</p>
|
||||
<div class="flex-grow" />
|
||||
|
||||
<ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" @click.stop="fireTestEventAndSucceed">Fire onTest Event</ui-btn>
|
||||
<ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" color="red-600" @click.stop="fireTestEventAndFail">Fire & Fail</ui-btn>
|
||||
<ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" @click.stop="fireTestEventAndSucceed">{{ this.$strings.ButtonFireOnTest }}</ui-btn>
|
||||
<ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" color="red-600" @click.stop="fireTestEventAndFail">{{ this.$strings.ButtonFireAndFail }}</ui-btn>
|
||||
<!-- <ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" @click.stop="rapidFireTestEvents">Rapid Fire</ui-btn> -->
|
||||
<ui-btn v-else-if="notification.enabled" :loading="sendingTest" small class="mr-2" @click.stop="sendTestClick">Test</ui-btn>
|
||||
<ui-btn v-else :loading="enabling" small color="success" class="mr-2" @click="enableNotification">Enable</ui-btn>
|
||||
<ui-btn v-else-if="notification.enabled" :loading="sendingTest" small class="mr-2" @click.stop="sendTestClick">{{ this.$strings.ButtonTest }}</ui-btn>
|
||||
<ui-btn v-else :loading="enabling" small color="success" class="mr-2" @click="enableNotification">{{ this.$strings.ButtonEnable }}</ui-btn>
|
||||
|
||||
<ui-icon-btn :size="7" icon-font-size="1.1rem" icon="edit" class="mr-2" @click="editNotification" />
|
||||
<ui-icon-btn bg-color="error" :size="7" icon-font-size="1.2rem" icon="delete" @click="deleteNotificationClick" />
|
||||
@@ -65,12 +65,12 @@ export default {
|
||||
this.$axios
|
||||
.$get(`/api/notifications/test?fail=${intentionallyFail ? 1 : 0}`)
|
||||
.then(() => {
|
||||
this.$toast.success('Triggered onTest Event')
|
||||
this.$toast.success(this.$strings.ToastNotificationTestTriggerSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
const errorMsg = error.response ? error.response.data : null
|
||||
this.$toast.error(`Failed: ${errorMsg}` || 'Failed to trigger onTest event')
|
||||
this.$toast.error(`Failed: ${errorMsg}` || this.$strings.ToastNotificationTestTriggerFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.testing = false
|
||||
@@ -91,7 +91,7 @@ export default {
|
||||
// End testing functions
|
||||
sendTestClick() {
|
||||
const payload = {
|
||||
message: `Trigger this notification with test data?`,
|
||||
message: this.$strings.MessageConfirmNotificationTestTrigger,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.sendTest()
|
||||
@@ -106,12 +106,12 @@ export default {
|
||||
this.$axios
|
||||
.$get(`/api/notifications/${this.notification.id}/test`)
|
||||
.then(() => {
|
||||
this.$toast.success('Triggered test notification')
|
||||
this.$toast.success(this.$strings.ToastNotificationTestTriggerSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
const errorMsg = error.response ? error.response.data : null
|
||||
this.$toast.error(`Failed: ${errorMsg}` || 'Failed to trigger test notification')
|
||||
this.$toast.error(`Failed: ${errorMsg}` || this.$strings.ToastNotificationTestTriggerFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.sendingTest = false
|
||||
@@ -127,11 +127,10 @@ export default {
|
||||
.$patch(`/api/notifications/${this.notification.id}`, payload)
|
||||
.then((updatedSettings) => {
|
||||
this.$emit('update', updatedSettings)
|
||||
this.$toast.success('Notification enabled')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update notification', error)
|
||||
this.$toast.error('Failed to update notification')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.enabling = false
|
||||
@@ -139,7 +138,7 @@ export default {
|
||||
},
|
||||
deleteNotificationClick() {
|
||||
const payload = {
|
||||
message: `Are you sure you want to delete this notification?`,
|
||||
message: this.$strings.MessageConfirmDeleteNotification,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.deleteNotification()
|
||||
@@ -155,11 +154,10 @@ export default {
|
||||
.$delete(`/api/notifications/${this.notification.id}`)
|
||||
.then((updatedSettings) => {
|
||||
this.$emit('update', updatedSettings)
|
||||
this.$toast.success('Deleted notification')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
this.$toast.error('Failed to delete notification')
|
||||
this.$toast.error(this.$strings.ToastNotificationDeleteFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.deleting = false
|
||||
@@ -171,4 +169,4 @@ export default {
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -27,38 +27,6 @@
|
||||
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=publishers.${$encode(publisher)}`" class="hover:underline">{{ publisher }}</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="musicAlbum" class="flex py-0.5">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">Album</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ musicAlbum }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="musicAlbumArtist" class="flex py-0.5">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">Album Artist</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ musicAlbumArtist }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="musicTrackPretty" class="flex py-0.5">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">Track</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ musicTrackPretty }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="musicDiscPretty" class="flex py-0.5">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">Disc</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ musicDiscPretty }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="podcastType" class="flex py-0.5">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span>
|
||||
@@ -97,7 +65,7 @@
|
||||
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=languages.${$encode(language)}`" class="hover:underline">{{ language }}</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
|
||||
<div v-if="tracks.length || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
||||
</div>
|
||||
@@ -134,10 +102,6 @@ export default {
|
||||
isPodcast() {
|
||||
return this.libraryItem.mediaType === 'podcast'
|
||||
},
|
||||
audioFile() {
|
||||
// Music track
|
||||
return this.media.audioFile
|
||||
},
|
||||
media() {
|
||||
return this.libraryItem.media || {}
|
||||
},
|
||||
@@ -168,25 +132,6 @@ export default {
|
||||
publisher() {
|
||||
return this.mediaMetadata.publisher || ''
|
||||
},
|
||||
musicArtists() {
|
||||
return this.mediaMetadata.artists || []
|
||||
},
|
||||
musicAlbum() {
|
||||
return this.mediaMetadata.album || ''
|
||||
},
|
||||
musicAlbumArtist() {
|
||||
return this.mediaMetadata.albumArtist || ''
|
||||
},
|
||||
musicTrackPretty() {
|
||||
if (!this.mediaMetadata.trackNumber) return null
|
||||
if (!this.mediaMetadata.trackTotal) return this.mediaMetadata.trackNumber
|
||||
return `${this.mediaMetadata.trackNumber} / ${this.mediaMetadata.trackTotal}`
|
||||
},
|
||||
musicDiscPretty() {
|
||||
if (!this.mediaMetadata.discNumber) return null
|
||||
if (!this.mediaMetadata.discTotal) return this.mediaMetadata.discNumber
|
||||
return `${this.mediaMetadata.discNumber} / ${this.mediaMetadata.discTotal}`
|
||||
},
|
||||
narrators() {
|
||||
return this.mediaMetadata.narrators || []
|
||||
},
|
||||
@@ -220,4 +165,4 @@ export default {
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
|
||||
</form>
|
||||
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
|
||||
<span v-if="!search" class="material-symbols" style="font-size: 1.2rem">search</span>
|
||||
<span v-if="!search" class="material-symbols" style="font-size: 1.2rem"></span>
|
||||
<span v-else class="material-symbols" style="font-size: 1.2rem">close</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@
|
||||
<p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelAuthors }}</p>
|
||||
<template v-for="item in authorResults">
|
||||
<li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(item.id)}`">
|
||||
<nuxt-link :to="`/author/${item.id}`">
|
||||
<cards-author-search-card :author="item" />
|
||||
</nuxt-link>
|
||||
</li>
|
||||
|
||||
@@ -98,9 +98,6 @@ export default {
|
||||
isPodcast() {
|
||||
return this.libraryMediaType === 'podcast'
|
||||
},
|
||||
isMusic() {
|
||||
return this.libraryMediaType === 'music'
|
||||
},
|
||||
seriesItems() {
|
||||
return [
|
||||
{
|
||||
@@ -274,35 +271,9 @@ export default {
|
||||
}
|
||||
]
|
||||
},
|
||||
musicItems() {
|
||||
return [
|
||||
{
|
||||
text: this.$strings.LabelAll,
|
||||
value: 'all'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelGenre,
|
||||
textPlural: this.$strings.LabelGenres,
|
||||
value: 'genres',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelTag,
|
||||
textPlural: this.$strings.LabelTags,
|
||||
value: 'tags',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.ButtonIssues,
|
||||
value: 'issues',
|
||||
sublist: false
|
||||
}
|
||||
]
|
||||
},
|
||||
selectItems() {
|
||||
if (this.isSeries) return this.seriesItems
|
||||
if (this.isPodcast) return this.podcastItems
|
||||
if (this.isMusic) return this.musicItems
|
||||
return this.bookItems
|
||||
},
|
||||
selectedItemSublist() {
|
||||
|
||||
@@ -56,9 +56,6 @@ export default {
|
||||
isPodcast() {
|
||||
return this.libraryMediaType === 'podcast'
|
||||
},
|
||||
isMusic() {
|
||||
return this.libraryMediaType === 'music'
|
||||
},
|
||||
podcastItems() {
|
||||
return [
|
||||
{
|
||||
@@ -148,40 +145,10 @@ export default {
|
||||
}
|
||||
]
|
||||
},
|
||||
musicItems() {
|
||||
return [
|
||||
{
|
||||
text: this.$strings.LabelTitle,
|
||||
value: 'media.metadata.title'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelAddedAt,
|
||||
value: 'addedAt'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelSize,
|
||||
value: 'size'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelDuration,
|
||||
value: 'media.duration'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelFileBirthtime,
|
||||
value: 'birthtimeMs'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelFileModified,
|
||||
value: 'mtimeMs'
|
||||
}
|
||||
]
|
||||
},
|
||||
selectItems() {
|
||||
let items = null
|
||||
if (this.isPodcast) {
|
||||
items = this.podcastItems
|
||||
} else if (this.isMusic) {
|
||||
items = this.musicItems
|
||||
} else if (this.$store.getters['user/getUserSetting']('filterBy').startsWith('series.')) {
|
||||
items = this.seriesItems
|
||||
} else {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="relative ml-4 sm:ml-8" v-click-outside="clickOutside">
|
||||
<div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="setShowMenu(true)">
|
||||
<span class="font-mono uppercase text-gray-200 text-sm sm:text-base">{{ playbackRate.toFixed(1) }}<span class="text-base">x</span></span>
|
||||
<span class="text-gray-200 text-sm sm:text-base">{{ playbackRate.toFixed(1) }}<span class="text-base">x</span></span>
|
||||
</div>
|
||||
<div v-show="showMenu" class="absolute -top-20 z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }">
|
||||
<div v-show="showMenu" class="absolute -top-[5.5rem] z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }">
|
||||
<div class="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }">
|
||||
<div class="arrow-down" />
|
||||
</div>
|
||||
@@ -11,12 +11,12 @@
|
||||
<template v-for="rate in rates">
|
||||
<div :key="rate" class="h-full border-black-300 w-11 cursor-pointer border rounded-sm" :class="value === rate ? 'bg-black-100' : 'hover:bg-black hover:bg-opacity-10'" style="min-width: 44px; max-width: 44px" @click="set(rate)">
|
||||
<div class="w-full h-full flex justify-center items-center">
|
||||
<p class="text-xs text-center font-mono">{{ rate }}<span class="text-sm">x</span></p>
|
||||
<p class="text-xs text-center">{{ rate }}<span class="text-sm">x</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="w-full py-1 px-4">
|
||||
<div class="w-full py-1 px-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" />
|
||||
<p class="px-2 text-2xl sm:text-3xl">{{ playbackRate }}<span class="text-2xl">x</span></p>
|
||||
@@ -41,7 +41,7 @@ export default {
|
||||
currentPlaybackRate: 0,
|
||||
MIN_SPEED: 0.5,
|
||||
MAX_SPEED: 10,
|
||||
menuLeft: -92,
|
||||
menuLeft: -96,
|
||||
arrowLeft: 0
|
||||
}
|
||||
},
|
||||
@@ -89,9 +89,9 @@ export default {
|
||||
if (boundingBox.left + 110 > window.innerWidth - 10) {
|
||||
this.menuLeft = window.innerWidth - 230 - boundingBox.left
|
||||
|
||||
this.arrowLeft = Math.abs(this.menuLeft) - 92
|
||||
this.arrowLeft = Math.abs(this.menuLeft) - 96
|
||||
} else {
|
||||
this.menuLeft = -92
|
||||
this.menuLeft = -96
|
||||
this.arrowLeft = 0
|
||||
}
|
||||
},
|
||||
@@ -109,4 +109,4 @@ export default {
|
||||
this.currentPlaybackRate = this.playbackRate
|
||||
}
|
||||
}
|
||||
</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-6 absolute bottom-2 w-28 px-2 bg-bg shadow-sm rounded-lg" style="left: -116px">
|
||||
<div ref="volumeTrack" class="h-1 w-full bg-gray-500 my-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack">
|
||||
<div class="bg-gray-100 h-full absolute left-0 top-0 pointer-events-none rounded-full" :style="{ width: volume * trackWidth + 'px' }" />
|
||||
<div class="w-2.5 h-2.5 bg-white shadow-sm rounded-full absolute pointer-events-none" :class="isDragging ? 'transform scale-125 origin-center' : ''" :style="{ left: cursorLeft + 'px', top: '-3px' }" />
|
||||
<div v-show="isOpen" class="volumeMenu h-28 absolute bottom-2 w-6 py-2 bg-bg shadow-sm rounded-lg" style="top: -116px">
|
||||
<div ref="volumeTrack" class="w-1 h-full bg-gray-500 mx-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack">
|
||||
<div class="bg-gray-100 w-full absolute left-0 bottom-0 pointer-events-none rounded-full" :style="{ height: volume * trackHeight + 'px' }" />
|
||||
<div class="w-2.5 h-2.5 bg-white shadow-sm rounded-full absolute pointer-events-none" :class="isDragging ? 'transform scale-125 origin-center' : ''" :style="{ bottom: cursorBottom + 'px', left: '-3px' }" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
@@ -24,10 +24,10 @@ export default {
|
||||
isOpen: false,
|
||||
isDragging: false,
|
||||
isHovering: false,
|
||||
posX: 0,
|
||||
posY: 0,
|
||||
lastValue: 0.5,
|
||||
isMute: false,
|
||||
trackWidth: 112 - 20,
|
||||
trackHeight: 112 - 20,
|
||||
openTimeout: null
|
||||
}
|
||||
},
|
||||
@@ -45,9 +45,9 @@ export default {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
cursorLeft() {
|
||||
var left = this.trackWidth * this.volume
|
||||
return left - 3
|
||||
cursorBottom() {
|
||||
var bottom = this.trackHeight * this.volume
|
||||
return bottom - 3
|
||||
},
|
||||
volumeIcon() {
|
||||
if (this.volume <= 0) return 'volume_mute'
|
||||
@@ -89,17 +89,10 @@ export default {
|
||||
}, 600)
|
||||
},
|
||||
mousemove(e) {
|
||||
var diff = this.posX - e.x
|
||||
this.posX = e.x
|
||||
var volShift = 0
|
||||
if (diff < 0) {
|
||||
// Volume up
|
||||
volShift = diff / this.trackWidth
|
||||
} else {
|
||||
// volume down
|
||||
volShift = diff / this.trackWidth
|
||||
}
|
||||
var newVol = this.volume - volShift
|
||||
var diff = this.posY - e.y
|
||||
this.posY = e.y
|
||||
var volShift = diff / this.trackHeight
|
||||
var newVol = this.volume + volShift
|
||||
newVol = Math.min(Math.max(0, newVol), 1)
|
||||
this.volume = newVol
|
||||
e.preventDefault()
|
||||
@@ -113,8 +106,8 @@ export default {
|
||||
},
|
||||
mousedownTrack(e) {
|
||||
this.isDragging = true
|
||||
this.posX = e.x
|
||||
var vol = e.offsetX / this.trackWidth
|
||||
this.posY = e.y
|
||||
var vol = 1 - e.offsetY / this.trackHeight
|
||||
vol = Math.min(Math.max(vol, 0), 1)
|
||||
this.volume = vol
|
||||
document.body.addEventListener('mousemove', this.mousemove)
|
||||
@@ -137,7 +130,7 @@ export default {
|
||||
this.clickVolumeIcon()
|
||||
},
|
||||
clickVolumeTrack(e) {
|
||||
var vol = e.offsetX / this.trackWidth
|
||||
var vol = 1 - e.offsetY / this.trackHeight
|
||||
vol = Math.min(Math.max(vol, 0), 1)
|
||||
this.volume = vol
|
||||
}
|
||||
@@ -147,7 +140,7 @@ export default {
|
||||
this.isMute = true
|
||||
}
|
||||
const storageVolume = localStorage.getItem('volume')
|
||||
if (storageVolume) {
|
||||
if (storageVolume && !isNaN(storageVolume)) {
|
||||
this.volume = parseFloat(storageVolume)
|
||||
}
|
||||
},
|
||||
@@ -157,4 +150,4 @@ export default {
|
||||
document.body.removeEventListener('mouseup', this.mouseup)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex pt-4 px-2">
|
||||
<ui-btn v-if="hasOpenIDLink" small :loading="unlinkingFromOpenID" color="primary" type="button" class="mr-2" @click.stop="unlinkOpenID">Unlink OpenID</ui-btn>
|
||||
<ui-btn v-if="hasOpenIDLink" small :loading="unlinkingFromOpenID" color="primary" type="button" class="mr-2" @click.stop="unlinkOpenID">{{ $strings.ButtonUnlinkOpenId }}</ui-btn>
|
||||
<ui-btn v-if="isEditingRoot" small class="flex items-center" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
@@ -212,19 +212,19 @@ export default {
|
||||
},
|
||||
unlinkOpenID() {
|
||||
const payload = {
|
||||
message: 'Are you sure you want to unlink this user from OpenID?',
|
||||
message: this.$strings.MessageConfirmUnlinkOpenId,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.unlinkingFromOpenID = true
|
||||
this.$axios
|
||||
.$patch(`/api/users/${this.account.id}/openid-unlink`)
|
||||
.then(() => {
|
||||
this.$toast.success('User unlinked from OpenID')
|
||||
this.$toast.success(this.$strings.ToastUnlinkOpenIdSuccess)
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to unlink user from OpenID', error)
|
||||
this.$toast.error('Failed to unlink user from OpenID')
|
||||
this.$toast.error(this.$strings.ToastUnlinkOpenIdFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.unlinkingFromOpenID = false
|
||||
@@ -265,15 +265,15 @@ export default {
|
||||
},
|
||||
submitForm() {
|
||||
if (!this.newUser.username) {
|
||||
this.$toast.error('Enter a username')
|
||||
this.$toast.error(this.$strings.ToastNewUserUsernameError)
|
||||
return
|
||||
}
|
||||
if (!this.newUser.permissions.accessAllLibraries && !this.newUser.librariesAccessible.length) {
|
||||
this.$toast.error('Must select at least one library')
|
||||
this.$toast.error(this.$strings.ToastNewUserLibraryError)
|
||||
return
|
||||
}
|
||||
if (!this.newUser.permissions.accessAllTags && !this.newUser.itemTagsSelected.length) {
|
||||
this.$toast.error('Must select at least one tag')
|
||||
this.$toast.error(this.$strings.ToastNewUserTagError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ export default {
|
||||
.then((data) => {
|
||||
this.processing = false
|
||||
if (data.error) {
|
||||
this.$toast.error(`${this.$strings.ToastAccountUpdateFailed}: ${data.error}`)
|
||||
this.$toast.error(`${this.$strings.ToastFailedToUpdate}: ${data.error}`)
|
||||
} else {
|
||||
console.log('Account updated', data.user)
|
||||
|
||||
@@ -313,12 +313,12 @@ export default {
|
||||
this.processing = false
|
||||
console.error('Failed to update account', error)
|
||||
var errMsg = error.response ? error.response.data || '' : ''
|
||||
this.$toast.error(errMsg || 'Failed to update account')
|
||||
this.$toast.error(errMsg || this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
},
|
||||
submitCreateAccount() {
|
||||
if (!this.newUser.password) {
|
||||
this.$toast.error('Must have a password, only root user can have an empty password')
|
||||
this.$toast.error(this.$strings.ToastNewUserPasswordError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -329,9 +329,9 @@ export default {
|
||||
.then((data) => {
|
||||
this.processing = false
|
||||
if (data.error) {
|
||||
this.$toast.error(`Failed to create account: ${data.error}`)
|
||||
this.$toast.error(this.$strings.ToastNewUserCreatedFailed + ': ' + data.error)
|
||||
} else {
|
||||
this.$toast.success('New account created')
|
||||
this.$toast.success(this.$strings.ToastNewUserCreatedSuccess)
|
||||
this.show = false
|
||||
}
|
||||
})
|
||||
@@ -351,6 +351,7 @@ export default {
|
||||
update: type === 'admin',
|
||||
delete: type === 'admin',
|
||||
upload: type === 'admin',
|
||||
accessExplicitContent: type === 'admin',
|
||||
accessAllLibraries: true,
|
||||
accessAllTags: true,
|
||||
selectedTagsNotAccessible: false
|
||||
@@ -385,6 +386,7 @@ export default {
|
||||
upload: false,
|
||||
accessAllLibraries: true,
|
||||
accessAllTags: true,
|
||||
accessExplicitContent: false,
|
||||
selectedTagsNotAccessible: false
|
||||
},
|
||||
librariesAccessible: [],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<modals-modal ref="modal" v-model="show" name="custom-metadata-provider" :width="600" :height="'unset'" :processing="processing">
|
||||
<template #outer>
|
||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||
<p class="text-3xl text-white truncate">Add custom metadata provider</p>
|
||||
<p class="text-3xl text-white truncate">{{ $strings.HeaderAddCustomMetadataProvider }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<form @submit.prevent="submitForm">
|
||||
@@ -20,7 +20,7 @@
|
||||
<ui-text-input-with-label v-model="newUrl" label="URL" />
|
||||
</div>
|
||||
<div class="w-full mb-2 p-1">
|
||||
<ui-text-input-with-label v-model="newAuthHeaderValue" :label="'Authorization Header Value'" type="password" />
|
||||
<ui-text-input-with-label v-model="newAuthHeaderValue" :label="$strings.LabelProviderAuthorizationValue" type="password" />
|
||||
</div>
|
||||
<div class="flex px-1 pt-4">
|
||||
<div class="flex-grow" />
|
||||
@@ -67,7 +67,7 @@ export default {
|
||||
methods: {
|
||||
submitForm() {
|
||||
if (!this.newName || !this.newUrl) {
|
||||
this.$toast.error('Must add name and url')
|
||||
this.$toast.error(this.$strings.ToastProviderNameAndUrlRequired)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -81,13 +81,13 @@ export default {
|
||||
})
|
||||
.then((data) => {
|
||||
this.$emit('added', data.provider)
|
||||
this.$toast.success('New provider added')
|
||||
this.$toast.success(this.$strings.ToastProviderCreatedSuccess)
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMsg = error.response?.data || 'Unknown error'
|
||||
console.error('Failed to add provider', error)
|
||||
this.$toast.error('Failed to add provider: ' + errorMsg)
|
||||
this.$toast.error(this.$strings.ToastProviderCreatedFailed + ': ' + errorMsg)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-base text-gray-200 truncate">{{ metadata.filename }}</p>
|
||||
<ui-btn v-if="ffprobeData" small class="ml-2" @click="ffprobeData = null">{{ $strings.ButtonReset }}</ui-btn>
|
||||
<ui-btn v-else-if="userIsAdminOrUp" small :loading="probingFile" class="ml-2" @click="getFFProbeData">Probe Audio File</ui-btn>
|
||||
<ui-btn v-else-if="userIsAdminOrUp" small :loading="probingFile" class="ml-2" @click="getFFProbeData">{{ $strings.ButtonProbeAudioFile }}</ui-btn>
|
||||
</div>
|
||||
|
||||
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
|
||||
@@ -159,7 +159,7 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to get ffprobe data', error)
|
||||
this.$toast.error('FFProbe failed')
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
})
|
||||
.finally(() => {
|
||||
this.probingFile = false
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<widgets-cron-expression-builder ref="expressionBuilder" v-model="newCronExpression" @input="expressionUpdated" />
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<ui-btn :disabled="!isUpdated" @click="submit">{{ isUpdated ? $strings.ButtonSave : $strings.MessageNoUpdateNecessary }}</ui-btn>
|
||||
<ui-btn :disabled="!isUpdated" @click="submit">{{ isUpdated ? $strings.ButtonSave : $strings.MessageNoUpdatesWereNecessary }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
|
||||
@@ -94,7 +94,7 @@ export default {
|
||||
this.$toast.success(this.$strings.ToastBookmarkRemoveSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toast.error(this.$strings.ToastBookmarkRemoveFailed)
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
console.error(error)
|
||||
})
|
||||
this.show = false
|
||||
@@ -110,7 +110,7 @@ export default {
|
||||
this.$toast.success(this.$strings.ToastBookmarkUpdateSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toast.error(this.$strings.ToastBookmarkUpdateFailed)
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
console.error(error)
|
||||
})
|
||||
this.show = false
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop>
|
||||
<div class="flex">
|
||||
<div class="flex-grow p-1 min-w-48 sm:min-w-64 md:min-w-80">
|
||||
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!isNewSeries" :label="$strings.LabelSeriesName" />
|
||||
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!isNewSeries" :label="$strings.LabelSeriesName" @input="seriesNameInputHandler" />
|
||||
</div>
|
||||
<div class="w-24 sm:w-28 md:w-40 p-1">
|
||||
<ui-text-input-with-label ref="sequenceInput" v-model="selectedSeries.sequence" :label="$strings.LabelSequence" />
|
||||
@@ -66,6 +66,11 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
seriesNameInputHandler() {
|
||||
if (this.$refs.sequenceInput) {
|
||||
this.$refs.sequenceInput.setFocus()
|
||||
}
|
||||
},
|
||||
setInputFocus() {
|
||||
if (this.isNewSeries) {
|
||||
// Focus on series input if new series
|
||||
@@ -134,4 +139,4 @@ export default {
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
|
||||
<div class="flex items-center">
|
||||
<ui-btn v-if="!isOpenSession && !isMediaItemShareSession" small color="error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn>
|
||||
<ui-btn v-else-if="!isMediaItemShareSession" small color="error" @click.stop="closeSessionClick">Close Open Session</ui-btn>
|
||||
<ui-btn v-else-if="!isMediaItemShareSession" small color="error" @click.stop="closeSessionClick">{{ $strings.ButtonCloseSession }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
@@ -206,14 +206,13 @@ export default {
|
||||
this.$axios
|
||||
.$post(`/api/session/${this._session.id}/close`)
|
||||
.then(() => {
|
||||
this.$toast.success('Session closed')
|
||||
this.show = false
|
||||
this.$emit('closedSession')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to close session', error)
|
||||
const errMsg = error.response?.data || ''
|
||||
this.$toast.error(errMsg || 'Failed to close open session')
|
||||
this.$toast.error(errMsg || this.$strings.ToastSessionCloseFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
|
||||
@@ -165,7 +165,7 @@ export default {
|
||||
},
|
||||
openShare() {
|
||||
if (!this.newShareSlug) {
|
||||
this.$toast.error('Slug is required')
|
||||
this.$toast.error(this.$strings.ToastSlugRequired)
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
|
||||
@@ -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-12 flex items-center justify-center ml-1">Set</ui-btn>
|
||||
<ui-btn color="success" type="submit" :padding-x="0" class="h-9 w-18 flex items-center justify-center ml-1">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
</form>
|
||||
</div>
|
||||
<div v-if="timerSet" class="w-full p-4">
|
||||
|
||||
@@ -78,14 +78,13 @@ export default {
|
||||
if (data.error) {
|
||||
this.$toast.error(data.error)
|
||||
} else {
|
||||
this.$toast.success('Cover Uploaded')
|
||||
this.resetCoverPreview()
|
||||
}
|
||||
this.processingUpload = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
var errorMsg = error.response && error.response.data ? error.response.data : 'Unknown Error'
|
||||
var errorMsg = error.response && error.response.data ? error.response.data : this.$strings.ToastUnknownError
|
||||
this.$toast.error(errorMsg)
|
||||
this.processingUpload = false
|
||||
})
|
||||
@@ -95,7 +94,7 @@ export default {
|
||||
|
||||
var success = await this.$axios.$post(`/api/${this.entity}/${this.entityId}/cover`, { url: this.imageUrl }).catch((error) => {
|
||||
console.error('Failed to download cover from url', error)
|
||||
var errorMsg = error.response && error.response.data ? error.response.data : 'Unknown Error'
|
||||
var errorMsg = error.response && error.response.data ? error.response.data : this.$strings.ToastUnknownError
|
||||
this.$toast.error(errorMsg)
|
||||
return false
|
||||
})
|
||||
@@ -104,4 +103,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -116,12 +116,12 @@ export default {
|
||||
this.$axios
|
||||
.$delete(`/api/authors/${this.authorId}`)
|
||||
.then(() => {
|
||||
this.$toast.success('Author removed')
|
||||
this.$toast.success(this.$strings.ToastAuthorRemoveSuccess)
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove author', error)
|
||||
this.$toast.error('Failed to remove author')
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
@@ -141,14 +141,14 @@ export default {
|
||||
}
|
||||
})
|
||||
if (!Object.keys(updatePayload).length) {
|
||||
this.$toast.info(this.$strings.MessageNoUpdateNecessary)
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
return
|
||||
}
|
||||
this.processing = true
|
||||
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
const errorMsg = error.response ? error.response.data : null
|
||||
this.$toast.error(errorMsg || this.$strings.ToastAuthorUpdateFailed)
|
||||
this.$toast.error(errorMsg || this.$strings.ToastFailedToUpdate)
|
||||
return null
|
||||
})
|
||||
if (result) {
|
||||
@@ -158,7 +158,7 @@ export default {
|
||||
} else if (result.merged) {
|
||||
this.$toast.success(this.$strings.ToastAuthorUpdateMerged)
|
||||
this.show = false
|
||||
} else this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||
} else this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
}
|
||||
this.processing = false
|
||||
},
|
||||
@@ -174,7 +174,7 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
this.$toast.error(this.$strings.ToastAuthorImageRemoveFailed)
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
@@ -182,7 +182,7 @@ export default {
|
||||
},
|
||||
submitUploadCover() {
|
||||
if (!this.imageUrl?.startsWith('http:') && !this.imageUrl?.startsWith('https:')) {
|
||||
this.$toast.error('Invalid image url')
|
||||
this.$toast.error(this.$strings.ToastInvalidImageUrl)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -194,14 +194,14 @@ export default {
|
||||
.$post(`/api/authors/${this.authorId}/image`, updatePayload)
|
||||
.then((data) => {
|
||||
this.imageUrl = ''
|
||||
this.$toast.success('Author image updated')
|
||||
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||
|
||||
this.authorCopy.updatedAt = data.author.updatedAt
|
||||
this.authorCopy.imagePath = data.author.imagePath
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
this.$toast.error(error.response.data || 'Failed to remove author image')
|
||||
this.$toast.error(error.response.data || this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
@@ -209,7 +209,7 @@ export default {
|
||||
},
|
||||
async searchAuthor() {
|
||||
if (!this.authorCopy.name && !this.authorCopy.asin) {
|
||||
this.$toast.error('Must enter an author name')
|
||||
this.$toast.error(this.$strings.ToastNameRequired)
|
||||
return
|
||||
}
|
||||
this.processing = true
|
||||
@@ -228,17 +228,19 @@ export default {
|
||||
return null
|
||||
})
|
||||
if (!response) {
|
||||
this.$toast.error('Author not found')
|
||||
this.$toast.error(this.$strings.ToastAuthorSearchNotFound)
|
||||
} else if (response.updated) {
|
||||
if (response.author.imagePath) {
|
||||
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||
} else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
||||
} else {
|
||||
this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
||||
}
|
||||
|
||||
this.authorCopy = {
|
||||
...response.author
|
||||
}
|
||||
} else {
|
||||
this.$toast.info('No updates were made for Author')
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
}
|
||||
this.processing = false
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove books from collection', error)
|
||||
this.$toast.error(this.$strings.ToastCollectionItemsRemoveFailed)
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
this.processing = false
|
||||
})
|
||||
} else {
|
||||
@@ -157,7 +157,7 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove book from collection', error)
|
||||
this.$toast.error(this.$strings.ToastCollectionItemsRemoveFailed)
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
@@ -172,12 +172,12 @@ export default {
|
||||
.$post(`/api/collections/${collection.id}/batch/add`, { books: this.selectedBookIds })
|
||||
.then((updatedCollection) => {
|
||||
console.log(`Books added to collection`, updatedCollection)
|
||||
this.$toast.success('Books added to collection')
|
||||
this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
|
||||
this.processing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to add books to collection', error)
|
||||
this.$toast.error('Failed to add books to collection')
|
||||
this.$toast.error(this.$strings.ToastCollectionItemsAddFailed)
|
||||
this.processing = false
|
||||
})
|
||||
} else {
|
||||
@@ -187,12 +187,12 @@ export default {
|
||||
.$post(`/api/collections/${collection.id}/book`, { id: this.selectedLibraryItemId })
|
||||
.then((updatedCollection) => {
|
||||
console.log(`Book added to collection`, updatedCollection)
|
||||
this.$toast.success('Book added to collection')
|
||||
this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
|
||||
this.processing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to add book to collection', error)
|
||||
this.$toast.error('Failed to add book to collection')
|
||||
this.$toast.error(this.$strings.ToastCollectionItemsAddFailed)
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
@@ -221,7 +221,7 @@ export default {
|
||||
.catch((error) => {
|
||||
console.error('Failed to create collection', error)
|
||||
var errMsg = error.response ? error.response.data || '' : ''
|
||||
this.$toast.error(`Failed to create collection: ${errMsg}`)
|
||||
this.$toast.error(this.$strings.ToastCollectionCreateFailed + ': ' + errMsg)
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ export default {
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove collection', error)
|
||||
this.processing = false
|
||||
this.$toast.error(this.$strings.ToastCollectionRemoveFailed)
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -115,7 +115,7 @@ export default {
|
||||
return
|
||||
}
|
||||
if (!this.newCollectionName) {
|
||||
return this.$toast.error('Collection must have a name')
|
||||
return this.$toast.error(this.$strings.ToastNameRequired)
|
||||
}
|
||||
|
||||
this.processing = true
|
||||
@@ -135,7 +135,7 @@ export default {
|
||||
.catch((error) => {
|
||||
console.error('Failed to update collection', error)
|
||||
this.processing = false
|
||||
this.$toast.error(this.$strings.ToastCollectionUpdateFailed)
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -125,12 +125,12 @@ export default {
|
||||
this.$refs.ereaderEmailInput.blur()
|
||||
|
||||
if (!this.newDevice.name?.trim() || !this.newDevice.email?.trim()) {
|
||||
this.$toast.error('Name and email required')
|
||||
this.$toast.error(this.$strings.ToastNameEmailRequired)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.newDevice.availabilityOption === 'specificUsers' && !this.newDevice.users.length) {
|
||||
this.$toast.error('Must select at least one user')
|
||||
this.$toast.error(this.$strings.ToastSelectAtLeastOneUser)
|
||||
return
|
||||
}
|
||||
if (this.newDevice.availabilityOption !== 'specificUsers') {
|
||||
@@ -142,14 +142,14 @@ export default {
|
||||
|
||||
if (!this.ereaderDevice) {
|
||||
if (this.existingDevices.some((d) => d.name === this.newDevice.name)) {
|
||||
this.$toast.error('Ereader device with that name already exists')
|
||||
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
|
||||
return
|
||||
}
|
||||
|
||||
this.submitCreate()
|
||||
} else {
|
||||
if (this.ereaderDevice.name !== this.newDevice.name && this.existingDevices.some((d) => d.name === this.newDevice.name)) {
|
||||
this.$toast.error('Ereader device with that name already exists')
|
||||
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -174,12 +174,11 @@ export default {
|
||||
.$post(`/api/emails/ereader-devices`, payload)
|
||||
.then((data) => {
|
||||
this.$emit('update', data.ereaderDevices)
|
||||
this.$toast.success('Device updated')
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update device', error)
|
||||
this.$toast.error('Failed to update device')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
@@ -201,12 +200,11 @@ export default {
|
||||
.$post('/api/emails/ereader-devices', payload)
|
||||
.then((data) => {
|
||||
this.$emit('update', data.ereaderDevices || [])
|
||||
this.$toast.success('Device added')
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to add device', error)
|
||||
this.$toast.error('Failed to add device')
|
||||
this.$toast.error(this.$strings.ToastDeviceAddFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
|
||||
@@ -194,7 +194,6 @@ export default {
|
||||
if (data.error) {
|
||||
this.$toast.error(data.error)
|
||||
} else {
|
||||
this.$toast.success('Cover Uploaded')
|
||||
this.resetCoverPreview()
|
||||
}
|
||||
this.processingUpload = false
|
||||
@@ -204,7 +203,7 @@ export default {
|
||||
if (error.response && error.response.data) {
|
||||
this.$toast.error(error.response.data)
|
||||
} else {
|
||||
this.$toast.error('Oops, something went wrong...')
|
||||
this.$toast.error(this.$strings.ToastUnknownError)
|
||||
}
|
||||
this.processingUpload = false
|
||||
})
|
||||
@@ -255,7 +254,7 @@ export default {
|
||||
},
|
||||
async updateCover(cover) {
|
||||
if (!cover.startsWith('http:') && !cover.startsWith('https:')) {
|
||||
this.$toast.error('Invalid URL')
|
||||
this.$toast.error(this.$strings.ToastInvalidUrl)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -264,11 +263,10 @@ export default {
|
||||
.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover })
|
||||
.then(() => {
|
||||
this.imageUrl = ''
|
||||
this.$toast.success('Update Successful')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update cover', error)
|
||||
this.$toast.error(error.response?.data || 'Failed to update cover')
|
||||
this.$toast.error(error.response?.data || this.$strings.ToastCoverUpdateFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.isProcessing = false
|
||||
@@ -308,12 +306,9 @@ export default {
|
||||
this.isProcessing = true
|
||||
this.$axios
|
||||
.$patch(`/api/items/${this.libraryItemId}/cover`, { cover: coverFile.metadata.path })
|
||||
.then(() => {
|
||||
this.$toast.success('Update Successful')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to set local cover', error)
|
||||
this.$toast.error(error.response?.data || 'Failed to set cover')
|
||||
this.$toast.error(error.response?.data || this.$strings.ToastCoverUpdateFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.isProcessing = false
|
||||
@@ -321,4 +316,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -92,7 +92,7 @@ export default {
|
||||
|
||||
var { title, author } = this.$refs.itemDetailsEdit.getTitleAndAuthorName()
|
||||
if (!title) {
|
||||
this.$toast.error('Must have a title for quick match')
|
||||
this.$toast.error(this.$strings.ToastTitleRequired)
|
||||
return
|
||||
}
|
||||
this.quickMatching = true
|
||||
@@ -108,9 +108,9 @@ export default {
|
||||
if (res.warning) {
|
||||
this.$toast.warning(res.warning)
|
||||
} else if (res.updated) {
|
||||
this.$toast.success('Item details updated')
|
||||
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||
} else {
|
||||
this.$toast.info('No updates were made')
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -128,18 +128,18 @@ export default {
|
||||
this.rescanning = false
|
||||
var result = data.result
|
||||
if (!result) {
|
||||
this.$toast.error(`Re-Scan Failed for "${this.title}"`)
|
||||
this.$toast.error(this.$getString('ToastRescanFailed', [this.title]))
|
||||
} else if (result === 'UPDATED') {
|
||||
this.$toast.success(`Re-Scan complete item was updated`)
|
||||
this.$toast.success(this.$strings.ToastRescanUpdated)
|
||||
} else if (result === 'UPTODATE') {
|
||||
this.$toast.success(`Re-Scan complete item was up to date`)
|
||||
this.$toast.success(this.$strings.ToastRescanUpToDate)
|
||||
} else if (result === 'REMOVED') {
|
||||
this.$toast.error(`Re-Scan complete item was removed`)
|
||||
this.$toast.error(this.$strings.ToastRescanRemoved)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to scan library item', error)
|
||||
this.$toast.error('Failed to scan library item')
|
||||
this.$toast.error(this.$strings.ToastScanFailed)
|
||||
this.rescanning = false
|
||||
})
|
||||
},
|
||||
@@ -156,7 +156,7 @@ export default {
|
||||
}
|
||||
var updatedDetails = this.$refs.itemDetailsEdit.getDetails()
|
||||
if (!updatedDetails.hasChanges) {
|
||||
this.$toast.info('No changes were made')
|
||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||
return false
|
||||
}
|
||||
return this.updateDetails(updatedDetails)
|
||||
@@ -170,7 +170,7 @@ export default {
|
||||
this.isProcessing = false
|
||||
if (updateResult) {
|
||||
if (updateResult.updated) {
|
||||
this.$toast.success('Item details updated')
|
||||
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||
return true
|
||||
} else {
|
||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||
@@ -217,4 +217,4 @@ export default {
|
||||
height: calc(100% - 80px);
|
||||
max-height: calc(100% - 80px);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -397,7 +397,7 @@ export default {
|
||||
},
|
||||
submitSearch() {
|
||||
if (!this.searchTitle) {
|
||||
this.$toast.warning('Search title is required')
|
||||
this.$toast.warning(this.$strings.ToastTitleRequired)
|
||||
return
|
||||
}
|
||||
this.persistProvider()
|
||||
@@ -618,12 +618,12 @@ export default {
|
||||
if (updateResult.updated) {
|
||||
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||
} else {
|
||||
this.$toast.info(this.$strings.ToastItemDetailsUpdateUnneeded)
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
}
|
||||
this.clearSelectedMatch()
|
||||
this.$emit('selectTab', 'details')
|
||||
} else {
|
||||
this.$toast.error(this.$strings.ToastItemDetailsUpdateFailed)
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
}
|
||||
} else {
|
||||
this.clearSelectedMatch()
|
||||
|
||||
@@ -163,7 +163,7 @@ export default {
|
||||
this.isProcessing = false
|
||||
if (updateResult) {
|
||||
if (updateResult.updated) {
|
||||
this.$toast.success('Item details updated')
|
||||
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||
return true
|
||||
} else {
|
||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||
|
||||
@@ -156,7 +156,7 @@ export default {
|
||||
},
|
||||
validate() {
|
||||
if (!this.libraryCopy.name) {
|
||||
this.$toast.error('Library must have a name')
|
||||
this.$toast.error(this.$strings.ToastNameRequired)
|
||||
return false
|
||||
}
|
||||
if (!this.libraryCopy.folders.length) {
|
||||
@@ -205,7 +205,7 @@ export default {
|
||||
submitUpdateLibrary() {
|
||||
var newLibraryPayload = this.getLibraryUpdatePayload()
|
||||
if (!Object.keys(newLibraryPayload).length) {
|
||||
this.$toast.info('No updates are necessary')
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ export default {
|
||||
if (error.response && error.response.data) {
|
||||
this.$toast.error(error.response.data)
|
||||
} else {
|
||||
this.$toast.error(this.$strings.ToastLibraryUpdateFailed)
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
}
|
||||
this.processing = false
|
||||
})
|
||||
@@ -264,4 +264,4 @@ export default {
|
||||
.tab.tab-selected {
|
||||
height: 41px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -162,7 +162,7 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to get filesystem paths', error)
|
||||
this.$toast.error('Failed to get filesystem paths')
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
return []
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
@@ -78,4 +78,4 @@ export default {
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -86,7 +86,7 @@ export default {
|
||||
return this.selectedEventData && this.selectedEventData.requiresLibrary
|
||||
},
|
||||
title() {
|
||||
return this.isNew ? 'Create Notification' : 'Update Notification'
|
||||
return this.isNew ? this.$strings.HeaderNotificationCreate : this.$strings.HeaderNotificationUpdate
|
||||
},
|
||||
availableVariables() {
|
||||
return this.selectedEventData ? this.selectedEventData.variables || null : null
|
||||
@@ -104,9 +104,9 @@ export default {
|
||||
},
|
||||
submitForm() {
|
||||
this.$refs.urlsInput?.forceBlur()
|
||||
|
||||
|
||||
if (!this.newNotification.urls.length) {
|
||||
this.$toast.error('Must enter an Apprise URL')
|
||||
this.$toast.error(this.$strings.ToastAppriseUrlRequired)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -127,12 +127,12 @@ export default {
|
||||
.$patch(`/api/notifications/${payload.id}`, payload)
|
||||
.then((updatedSettings) => {
|
||||
this.$emit('update', updatedSettings)
|
||||
this.$toast.success('Notification updated')
|
||||
this.$toast.success(this.$strings.ToastNotificationUpdateSuccess)
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update notification', error)
|
||||
this.$toast.error('Failed to update notification')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
@@ -149,12 +149,11 @@ export default {
|
||||
.$post('/api/notifications', payload)
|
||||
.then((updatedSettings) => {
|
||||
this.$emit('update', updatedSettings)
|
||||
this.$toast.success('Notification created')
|
||||
this.show = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to create notification', error)
|
||||
this.$toast.error('Failed to create notification')
|
||||
this.$toast.error(this.$strings.ToastNotificationCreateFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="flex-grow" />
|
||||
<ui-checkbox v-model="playerQueueAutoPlay" label="Auto Play" medium checkbox-bg="primary" border-color="gray-600" label-class="pl-2 mb-px" />
|
||||
</div>
|
||||
<modals-player-queue-item-row v-for="(item, index) in playerQueueItems" :key="index" :item="item" :index="index" @play="playItem" @remove="removeItem" />
|
||||
<modals-player-queue-item-row v-for="(item, index) in playerQueueItems" :key="index" :item="item" :index="index" @play="playItem(index)" @remove="removeItem" />
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
@@ -22,8 +22,7 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: Boolean,
|
||||
libraryItemId: String
|
||||
value: Boolean
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
@@ -50,11 +49,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
playItem(item) {
|
||||
this.$eventBus.$emit('play-item', {
|
||||
libraryItemId: item.libraryItemId,
|
||||
episodeId: item.episodeId || null,
|
||||
queueItems: this.playerQueueItems
|
||||
playItem(index) {
|
||||
this.$eventBus.$emit('play-queue-item', {
|
||||
index
|
||||
})
|
||||
this.show = false
|
||||
},
|
||||
@@ -63,4 +60,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -130,12 +130,12 @@ export default {
|
||||
.$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects })
|
||||
.then((updatedPlaylist) => {
|
||||
console.log(`Items removed from playlist`, updatedPlaylist)
|
||||
this.$toast.success('Playlist item(s) removed')
|
||||
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
|
||||
this.processing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove items from playlist', error)
|
||||
this.$toast.error('Failed to remove playlist item(s)')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
@@ -148,12 +148,12 @@ export default {
|
||||
.$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects })
|
||||
.then((updatedPlaylist) => {
|
||||
console.log(`Items added to playlist`, updatedPlaylist)
|
||||
this.$toast.success('Items added to playlist')
|
||||
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
|
||||
this.processing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to add items to playlist', error)
|
||||
this.$toast.error('Failed to add items to playlist')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
@@ -174,14 +174,14 @@ export default {
|
||||
.$post('/api/playlists', newPlaylist)
|
||||
.then((data) => {
|
||||
console.log('New playlist created', data)
|
||||
this.$toast.success(`Playlist "${data.name}" created`)
|
||||
this.$toast.success(this.$strings.ToastPlaylistCreateSuccess + ': ' + data.name)
|
||||
this.processing = false
|
||||
this.newPlaylistName = ''
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to create playlist', error)
|
||||
var errMsg = error.response ? error.response.data || '' : ''
|
||||
this.$toast.error(`Failed to create playlist: ${errMsg}`)
|
||||
this.$toast.error(this.$strings.ToastPlaylistCreateFailed + ': ' + errMsg)
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export default {
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove playlist', error)
|
||||
this.processing = false
|
||||
this.$toast.error(this.$strings.ToastPlaylistRemoveFailed)
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -95,7 +95,7 @@ export default {
|
||||
return
|
||||
}
|
||||
if (!this.newPlaylistName) {
|
||||
return this.$toast.error('Playlist must have a name')
|
||||
return this.$toast.error(this.$strings.ToastNameRequired)
|
||||
}
|
||||
|
||||
this.processing = true
|
||||
@@ -115,7 +115,7 @@ export default {
|
||||
.catch((error) => {
|
||||
console.error('Failed to update playlist', error)
|
||||
this.processing = false
|
||||
this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -142,7 +142,7 @@ export default {
|
||||
|
||||
const updatedDetails = this.getUpdatePayload()
|
||||
if (!Object.keys(updatedDetails).length) {
|
||||
this.$toast.info('No changes were made')
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
return false
|
||||
}
|
||||
return this.updateDetails(updatedDetails)
|
||||
|
||||
@@ -105,7 +105,7 @@ export default {
|
||||
}
|
||||
const updatePayload = this.getUpdatePayload(episodeData)
|
||||
if (!Object.keys(updatePayload).length) {
|
||||
return this.$toast.info('No updates are necessary')
|
||||
return this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
}
|
||||
console.log('Episode update payload', updatePayload)
|
||||
|
||||
@@ -126,7 +126,7 @@ export default {
|
||||
},
|
||||
submitForm() {
|
||||
if (!this.episodeTitle || !this.episodeTitle.length) {
|
||||
this.$toast.error('Must enter an episode title')
|
||||
this.$toast.error(this.$strings.ToastTitleRequired)
|
||||
return
|
||||
}
|
||||
this.searchedTitle = this.episodeTitle
|
||||
|
||||
@@ -121,14 +121,14 @@ export default {
|
||||
methods: {
|
||||
openFeed() {
|
||||
if (!this.newFeedSlug) {
|
||||
this.$toast.error('Must set a feed slug')
|
||||
this.$toast.error(this.$strings.ToastSlugRequired)
|
||||
return
|
||||
}
|
||||
|
||||
const sanitized = this.$sanitizeSlug(this.newFeedSlug)
|
||||
if (this.newFeedSlug !== sanitized) {
|
||||
this.newFeedSlug = sanitized
|
||||
this.$toast.warning('Slug had to be modified - Run again')
|
||||
this.$toast.warning(this.$strings.ToastSlugMustChange)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +1,37 @@
|
||||
<template>
|
||||
<div class="flex items-center pt-4 pb-2 lg:pt-0 lg:pb-2">
|
||||
<div class="flex-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">
|
||||
<span class="material-symbols text-2xl sm:text-3xl">first_page</span>
|
||||
<div class="flex justify-center pt-4 pb-2 lg:pt-0 lg:pb-2">
|
||||
<div class="flex items-center justify-center flex-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">
|
||||
<span class="material-symbols text-2xl sm:text-3xl">first_page</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
<ui-tooltip direction="top" :text="jumpBackwardText">
|
||||
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
||||
<span class="material-symbols text-2xl sm:text-3xl">replay</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
||||
<span class="material-symbols fill text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
<ui-tooltip direction="top" :text="jumpBackwardText">
|
||||
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
||||
<span class="material-symbols text-2xl sm:text-3xl">replay</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
||||
<span class="material-symbols fill text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
||||
</button>
|
||||
<ui-tooltip direction="top" :text="jumpForwardText">
|
||||
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
||||
<span class="material-symbols text-2xl sm:text-3xl">forward_media</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
<ui-tooltip direction="top" :text="$strings.ButtonNextChapter" class="ml-4 lg:ml-8">
|
||||
<button :aria-label="$strings.ButtonNextChapter" :disabled="!hasNextChapter" :class="hasNextChapter ? 'text-gray-300' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter">
|
||||
<span class="material-symbols text-2xl sm:text-3xl">last_page</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
<controls-playback-speed-control v-model="playbackRateInput" @input="playbackRateUpdated" @change="playbackRateChanged" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
|
||||
<span class="material-symbols text-2xl">autorenew</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex-grow" />
|
||||
<ui-tooltip direction="top" :text="jumpForwardText">
|
||||
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
||||
<span class="material-symbols text-2xl sm:text-3xl">forward_media</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
<ui-tooltip direction="top" :text="hasNextLabel" class="ml-4 lg:ml-8">
|
||||
<button :aria-label="hasNextLabel" :disabled="!hasNext" class="text-gray-300 disabled:text-gray-500" @mousedown.prevent @mouseup.prevent @click.stop="next">
|
||||
<span class="material-symbols text-2xl sm:text-3xl">last_page</span>
|
||||
</button>
|
||||
</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">
|
||||
<span class="material-symbols text-2xl">autorenew</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -41,27 +40,26 @@ export default {
|
||||
props: {
|
||||
loading: Boolean,
|
||||
seekLoading: Boolean,
|
||||
playbackRate: Number,
|
||||
paused: Boolean,
|
||||
hasNextChapter: Boolean
|
||||
hasNextChapter: Boolean,
|
||||
hasNextItemInQueue: Boolean
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
playbackRateInput: {
|
||||
get() {
|
||||
return this.playbackRate
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:playbackRate', val)
|
||||
}
|
||||
},
|
||||
jumpForwardText() {
|
||||
return this.getJumpText('jumpForwardAmount', this.$strings.ButtonJumpForward)
|
||||
},
|
||||
jumpBackwardText() {
|
||||
return this.getJumpText('jumpBackwardAmount', this.$strings.ButtonJumpBackward)
|
||||
},
|
||||
hasNextLabel() {
|
||||
if (this.hasNextItemInQueue && !this.hasNextChapter) return this.$strings.ButtonNextItemInQueue
|
||||
return this.$strings.ButtonNextChapter
|
||||
},
|
||||
hasNext() {
|
||||
return this.hasNextItemInQueue || this.hasNextChapter
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -71,9 +69,9 @@ export default {
|
||||
prevChapter() {
|
||||
this.$emit('prevChapter')
|
||||
},
|
||||
nextChapter() {
|
||||
if (!this.hasNextChapter) return
|
||||
this.$emit('nextChapter')
|
||||
next() {
|
||||
if (!this.hasNext) return
|
||||
this.$emit('next')
|
||||
},
|
||||
jumpBackward() {
|
||||
this.$emit('jumpBackward')
|
||||
@@ -81,15 +79,6 @@ export default {
|
||||
jumpForward() {
|
||||
this.$emit('jumpForward')
|
||||
},
|
||||
playbackRateUpdated(playbackRate) {
|
||||
this.$emit('setPlaybackRate', playbackRate)
|
||||
},
|
||||
playbackRateChanged(playbackRate) {
|
||||
this.$emit('setPlaybackRate', playbackRate)
|
||||
this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => {
|
||||
console.error('Failed to update settings', err)
|
||||
})
|
||||
},
|
||||
getJumpText(setting, prefix) {
|
||||
const amount = this.$store.getters['user/getUserSetting'](setting)
|
||||
if (!amount) return prefix
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<div class="w-full -mt-6">
|
||||
<div class="w-full relative mb-1">
|
||||
<div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full">
|
||||
<!-- <span class="material-symbols text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
|
||||
<controls-playback-speed-control v-model="playbackRate" @input="setPlaybackRate" @change="playbackRateChanged" class="mx-2 block" />
|
||||
|
||||
<ui-tooltip direction="top" :text="$strings.LabelVolume">
|
||||
<ui-tooltip direction="left" :text="$strings.LabelVolume">
|
||||
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden sm:block" />
|
||||
</ui-tooltip>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<span v-if="!sleepTimerSet" class="material-symbols text-2xl">snooze</span>
|
||||
<div v-else class="flex items-center">
|
||||
<span class="material-symbols text-lg text-warning">snooze</span>
|
||||
<p class="text-sm sm:text-lg text-warning font-mono font-semibold text-center px-0.5 sm:pb-0.5 sm:min-w-8">{{ sleepTimerRemainingString }}</p>
|
||||
<p class="text-sm sm:text-lg text-warning font-semibold text-center px-0.5 sm:pb-0.5 sm:min-w-8">{{ sleepTimerRemainingString }}</p>
|
||||
</div>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
@@ -43,20 +43,24 @@
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<player-playback-controls :loading="loading" :seek-loading="seekLoading" :playback-rate.sync="playbackRate" :paused="paused" :has-next-chapter="hasNextChapter" @prevChapter="prevChapter" @nextChapter="nextChapter" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setPlaybackRate="setPlaybackRate" @playPause="playPause" />
|
||||
<player-playback-controls :loading="loading" :seek-loading="seekLoading" :playback-rate.sync="playbackRate" :paused="paused" :hasNextChapter="hasNextChapter" :hasNextItemInQueue="hasNextItemInQueue" @prevChapter="prevChapter" @next="goToNext" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setPlaybackRate="setPlaybackRate" @playPause="playPause" />
|
||||
</div>
|
||||
|
||||
<player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" :current-chapter="currentChapter" :playback-rate="playbackRate" @seek="seek" />
|
||||
|
||||
<div class="flex">
|
||||
<p ref="currentTimestamp" class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">00:00:00</p>
|
||||
<p class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto"> / {{ progressPercent }}%</p>
|
||||
<div class="flex-grow" />
|
||||
<p class="text-xs sm:text-sm text-gray-300 pt-0.5 px-2 truncate">
|
||||
{{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400"> ({{ $getString('LabelPlayerChapterNumberMarker', [currentChapterIndex + 1, chapters.length]) }})</span>
|
||||
</p>
|
||||
<div class="flex-grow" />
|
||||
<p class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
|
||||
<div class="relative flex items-center justify-between">
|
||||
<div class="flex-grow flex items-center">
|
||||
<p ref="currentTimestamp" class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">00:00:00</p>
|
||||
<p class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto"> / {{ progressPercent }}%</p>
|
||||
</div>
|
||||
<div class="absolute left-1/2 transform -translate-x-1/2">
|
||||
<p class="text-xs sm:text-sm text-gray-300 pt-0.5 px-2 truncate">
|
||||
{{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400"> ({{ $getString('LabelPlayerChapterNumberMarker', [currentChapterIndex + 1, chapters.length]) }})</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-grow flex items-center justify-end">
|
||||
<p class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :playback-rate="playbackRate" :chapters="chapters" @select="selectChapter" />
|
||||
@@ -82,7 +86,8 @@ export default {
|
||||
sleepTimerType: String,
|
||||
isPodcast: Boolean,
|
||||
hideBookmarks: Boolean,
|
||||
hideSleepTimer: Boolean
|
||||
hideSleepTimer: Boolean,
|
||||
hasNextItemInQueue: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -145,7 +150,7 @@ export default {
|
||||
return Math.round((100 * time) / duration)
|
||||
},
|
||||
currentChapterName() {
|
||||
return this.currentChapter ? this.currentChapter.title : ''
|
||||
return this.currentChapter?.title || ''
|
||||
},
|
||||
currentChapterDuration() {
|
||||
if (!this.currentChapter) return 0
|
||||
@@ -177,22 +182,6 @@ export default {
|
||||
methods: {
|
||||
toggleFullscreen(isFullscreen) {
|
||||
this.$store.commit('setPlayerIsFullscreen', isFullscreen)
|
||||
|
||||
var videoPlayerEl = document.getElementById('video-player')
|
||||
if (videoPlayerEl) {
|
||||
if (isFullscreen) {
|
||||
videoPlayerEl.style.width = '100vw'
|
||||
videoPlayerEl.style.height = '100vh'
|
||||
videoPlayerEl.style.top = '0px'
|
||||
videoPlayerEl.style.left = '0px'
|
||||
} else {
|
||||
videoPlayerEl.style.width = '384px'
|
||||
videoPlayerEl.style.height = '216px'
|
||||
videoPlayerEl.style.top = 'unset'
|
||||
videoPlayerEl.style.bottom = '80px'
|
||||
videoPlayerEl.style.left = '16px'
|
||||
}
|
||||
}
|
||||
},
|
||||
setDuration(duration) {
|
||||
this.duration = duration
|
||||
@@ -239,6 +228,12 @@ export default {
|
||||
this.playbackRate = Number((this.playbackRate - 0.1).toFixed(1))
|
||||
this.setPlaybackRate(this.playbackRate)
|
||||
},
|
||||
playbackRateChanged(playbackRate) {
|
||||
this.setPlaybackRate(playbackRate)
|
||||
this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => {
|
||||
console.error('Failed to update settings', err)
|
||||
})
|
||||
},
|
||||
setPlaybackRate(playbackRate) {
|
||||
this.$emit('setPlaybackRate', playbackRate)
|
||||
},
|
||||
@@ -278,10 +273,13 @@ export default {
|
||||
this.seek(this.currentChapter.start)
|
||||
}
|
||||
},
|
||||
nextChapter() {
|
||||
if (!this.currentChapter || !this.hasNextChapter) return
|
||||
var nextChapter = this.chapters[this.currentChapterIndex + 1]
|
||||
this.seek(nextChapter.start)
|
||||
goToNext() {
|
||||
if (this.hasNextChapter) {
|
||||
const nextChapter = this.chapters[this.currentChapterIndex + 1]
|
||||
this.seek(nextChapter.start)
|
||||
} else if (this.hasNextItemInQueue) {
|
||||
this.$emit('nextItemInQueue')
|
||||
}
|
||||
},
|
||||
setStreamReady() {
|
||||
if (this.$refs.trackbar) this.$refs.trackbar.setPercentageReady(1)
|
||||
|
||||
@@ -35,22 +35,22 @@
|
||||
<div class="flex justify-between pt-12">
|
||||
<div>
|
||||
<p class="text-sm text-center">{{ $strings.LabelStatsWeekListening }}</p>
|
||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ totalMinutesListeningThisWeek }}</p>
|
||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ $formatNumber(totalMinutesListeningThisWeek) }}</p>
|
||||
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-center">{{ $strings.LabelStatsDailyAverage }}</p>
|
||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ averageMinutesPerDay }}</p>
|
||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ $formatNumber(averageMinutesPerDay) }}</p>
|
||||
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-center">{{ $strings.LabelStatsBestDay }}</p>
|
||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ mostListenedDay }}</p>
|
||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ $formatNumber(mostListenedDay) }}</p>
|
||||
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-center">{{ $strings.LabelStatsDays }}</p>
|
||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ daysInARow }}</p>
|
||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ $formatNumber(daysInARow) }}</p>
|
||||
<p class="text-sm text-center">{{ $strings.LabelStatsInARow }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex p-2">
|
||||
<span class="material-symbols-outlined text-5xl pt-1">insert_drive_file</span>
|
||||
<span class="material-symbols text-5xl pt-1">insert_drive_file</span>
|
||||
<div class="px-1">
|
||||
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalSizeNum) }}</p>
|
||||
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelSize }} ({{ totalSizeMod }})</p>
|
||||
@@ -37,7 +37,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex p-2">
|
||||
<span class="material-symbols-outlined text-5xl pt-1">audio_file</span>
|
||||
<span class="material-symbols text-5xl pt-1">audio_file</span>
|
||||
<div class="px-1">
|
||||
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(numAudioTracks) }}</p>
|
||||
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAudioTracks }}</p>
|
||||
@@ -103,4 +103,4 @@ export default {
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -73,7 +73,7 @@ export default {
|
||||
|
||||
const addIcon = (icon, color, fontSize, x, y) => {
|
||||
ctx.fillStyle = color
|
||||
ctx.font = `${fontSize} Material Symbols Outlined`
|
||||
ctx.font = `${fontSize} Material Symbols Rounded`
|
||||
ctx.fillText(icon, x, y)
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ export default {
|
||||
|
||||
// Top text
|
||||
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
|
||||
addText(`${this.year} ${this.$strings.StatsYearInReview}`, '18px', 'bold', 'white', '1px', 65, 51,)
|
||||
addText(`${this.year} ${this.$strings.StatsYearInReview}`, '18px', 'bold', 'white', '1px', 65, 51)
|
||||
|
||||
// Top left box
|
||||
createRoundedRect(50, 100, 340, 160)
|
||||
@@ -261,7 +261,7 @@ export default {
|
||||
.catch((error) => {
|
||||
console.error('Failed to share', error)
|
||||
if (error.name !== 'AbortError') {
|
||||
this.$toast.error('Failed to share: ' + error.message)
|
||||
this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-1 sm:p-4 mb-4">
|
||||
<!-- hack to get icon fonts loaded on init -->
|
||||
<div class="h-0 w-0 overflow-hidden opacity-0">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
<span class="material-symbols">close</span>
|
||||
<span class="abs-icons icon-audiobookshelf" />
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<!-- next button -->
|
||||
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
|
||||
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
|
||||
<span class="material-symbols-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
||||
<span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
||||
</ui-btn>
|
||||
</div>
|
||||
<stats-year-in-review ref="yearInReview" :variant="yearInReviewVariant" :year="yearInReviewYear" :processing.sync="processingYearInReview" />
|
||||
@@ -74,7 +74,7 @@
|
||||
<!-- next button -->
|
||||
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
|
||||
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
|
||||
<span class="material-symbols-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
||||
<span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
||||
</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,4 +138,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -237,7 +237,7 @@ export default {
|
||||
.catch((error) => {
|
||||
console.error('Failed to share', error)
|
||||
if (error.name !== 'AbortError') {
|
||||
this.$toast.error('Failed to share: ' + error.message)
|
||||
this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -64,7 +64,7 @@ export default {
|
||||
|
||||
const addIcon = (icon, color, fontSize, x, y) => {
|
||||
ctx.fillStyle = color
|
||||
ctx.font = `${fontSize} Material Symbols Outlined`
|
||||
ctx.font = `${fontSize} Material Symbols Rounded`
|
||||
ctx.fillText(icon, x, y)
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ export default {
|
||||
.catch((error) => {
|
||||
console.error('Failed to share', error)
|
||||
if (error.name !== 'AbortError') {
|
||||
this.$toast.error('Failed to share: ' + error.message)
|
||||
this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<div class="w-full flex flex-row items-center justify-center">
|
||||
<ui-btn v-if="backup.serverVersion && backup.key" small color="primary" @click="applyBackup(backup)">{{ $strings.ButtonRestore }}</ui-btn>
|
||||
<ui-tooltip v-else text="This backup was created with an old version of audiobookshelf no longer supported" direction="bottom" class="mx-2 flex items-center">
|
||||
<span class="material-symbols-outlined text-2xl text-error">error_outline</span>
|
||||
<span class="material-symbols text-2xl text-error">error_outline</span>
|
||||
</ui-tooltip>
|
||||
|
||||
<button aria-label="Download Backup" class="inline-flex material-symbols text-xl mx-1 mt-1 text-white/70 hover:text-white/100" @click.stop="downloadBackup(backup)">download</button>
|
||||
@@ -186,7 +186,7 @@ export default {
|
||||
mounted() {
|
||||
this.loadBackups()
|
||||
if (this.$route.query.backup) {
|
||||
this.$toast.success('Backup applied successfully')
|
||||
this.$toast.success(this.$strings.ToastBackupAppliedSuccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="flex-grow" />
|
||||
<ui-btn v-if="userCanUpdate" small :to="`/audiobook/${libraryItemId}/chapters`" color="primary" class="mr-2" @click="clickEditChapters">{{ $strings.ButtonEditChapters }}</ui-btn>
|
||||
<div v-if="!keepOpen" class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="expanded ? 'transform rotate-180' : ''">
|
||||
<span class="material-symbols text-4xl">expand_more</span>
|
||||
<span class="material-symbols text-4xl"></span>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="slide">
|
||||
|
||||
@@ -78,7 +78,7 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update collection', error)
|
||||
this.$toast.error('Failed to save collection books order')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
},
|
||||
editBook(book) {
|
||||
@@ -110,4 +110,4 @@ export default {
|
||||
.collection-book-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -45,7 +45,7 @@ export default {
|
||||
methods: {
|
||||
removeProvider(provider) {
|
||||
const payload = {
|
||||
message: `Are you sure you want remove custom metadata provider "${provider.name}"?`,
|
||||
message: this.$getString('MessageConfirmDeleteMetadataProvider', [provider.name]),
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.$emit('update:processing', true)
|
||||
@@ -53,12 +53,12 @@ export default {
|
||||
this.$axios
|
||||
.$delete(`/api/custom-metadata-providers/${provider.id}`)
|
||||
.then(() => {
|
||||
this.$toast.success('Provider removed')
|
||||
this.$toast.success(this.$strings.ToastProviderRemoveSuccess)
|
||||
this.$emit('removed', provider.id)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove provider', error)
|
||||
this.$toast.error('Failed to remove provider')
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.$emit('update:processing', false)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="flex-grow" />
|
||||
<ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="toggleFullPath">{{ $strings.ButtonFullPath }}</ui-btn>
|
||||
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''">
|
||||
<span class="material-symbols text-4xl">expand_more</span>
|
||||
<span class="material-symbols text-4xl"></span>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="slide">
|
||||
@@ -18,7 +18,7 @@
|
||||
<th class="text-left px-4">{{ $strings.LabelPath }}</th>
|
||||
<th class="text-left w-24 min-w-24">{{ $strings.LabelSize }}</th>
|
||||
<th class="text-left px-4 w-24">
|
||||
{{ $strings.LabelRead }} <ui-tooltip :text="$strings.LabelReadEbookWithoutProgress" direction="top" class="inline-block"><span class="material-symbols-outlined text-sm align-middle">info</span></ui-tooltip>
|
||||
{{ $strings.LabelRead }} <ui-tooltip :text="$strings.LabelReadEbookWithoutProgress" direction="top" class="inline-block"><span class="material-symbols text-sm align-middle">info</span></ui-tooltip>
|
||||
</th>
|
||||
<th v-if="showMoreColumn" class="text-center w-16"></th>
|
||||
</tr>
|
||||
@@ -92,4 +92,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<tr>
|
||||
<td class="px-4">
|
||||
{{ showFullPath ? file.metadata.path : file.metadata.relPath }} <ui-tooltip :text="$strings.LabelPrimaryEbook" class="inline-block"><span v-if="isPrimary" class="material-symbols-outlined text-success align-text-bottom">check_circle</span></ui-tooltip>
|
||||
{{ showFullPath ? file.metadata.path : file.metadata.relPath }} <ui-tooltip :text="$strings.LabelPrimaryEbook" class="inline-block"><span v-if="isPrimary" class="material-symbols text-success align-text-bottom">check_circle</span></ui-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
{{ $bytesPretty(file.metadata.size) }}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="flex-grow" />
|
||||
<ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="toggleFullPath">{{ $strings.ButtonFullPath }}</ui-btn>
|
||||
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''">
|
||||
<span class="material-symbols text-4xl">expand_more</span>
|
||||
<span class="material-symbols text-4xl"></span>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="slide">
|
||||
@@ -103,4 +103,4 @@ export default {
|
||||
this.showFiles = this.expanded
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -92,7 +92,7 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update playlist', error)
|
||||
this.$toast.error('Failed to save playlist items order')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
},
|
||||
init() {
|
||||
@@ -119,4 +119,4 @@ export default {
|
||||
.playlist-item-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<ui-btn small color="primary">{{ $strings.ButtonManageTracks }}</ui-btn>
|
||||
</nuxt-link>
|
||||
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''">
|
||||
<span class="material-symbols text-4xl">expand_more</span>
|
||||
<span class="material-symbols text-4xl"></span>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="slide">
|
||||
@@ -92,4 +92,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<div v-if="user.type !== 'root' || userIsRoot" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-opacity-100 cursor-pointer" @click.stop="editUser(user)">
|
||||
<button type="button" :aria-label="$getString('ButtonUserEdit', [user.username])" class="material-symbols text-base">edit</button>
|
||||
</div>
|
||||
<div v-show="user.type !== 'root'" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="deleteUserClick(user)">
|
||||
<div v-show="user.type !== 'root' && user.id !== currentUserId" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="deleteUserClick(user)">
|
||||
<button type="button" :aria-label="$getString('ButtonUserDelete', [user.username])" class="material-symbols text-base">delete</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,10 +157,6 @@ export default {
|
||||
this.init()
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.$refs.accountModal) {
|
||||
this.$refs.accountModal.close()
|
||||
}
|
||||
|
||||
if (this.$root.socket) {
|
||||
this.$root.socket.off('user_added', this.addUpdateUser)
|
||||
this.$root.socket.off('user_updated', this.addUpdateUser)
|
||||
|
||||
@@ -76,8 +76,7 @@ export default {
|
||||
var newOrder = libraryOrderData.map((lib) => lib.id).join(',')
|
||||
if (currOrder !== newOrder) {
|
||||
this.$axios.$post('/api/libraries/order', libraryOrderData).then((response) => {
|
||||
if (response.libraries && response.libraries.length) {
|
||||
this.$toast.success('Library order saved', { timeout: 1500 })
|
||||
if (response.libraries?.length) {
|
||||
this.$store.commit('libraries/set', response.libraries)
|
||||
}
|
||||
})
|
||||
@@ -110,4 +109,4 @@ export default {
|
||||
this.$store.commit('libraries/removeListener', 'libraries-table')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -218,12 +218,12 @@ export default {
|
||||
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
|
||||
} else {
|
||||
console.log(`Item removed from playlist`, updatedPlaylist)
|
||||
this.$toast.success('Item removed from playlist')
|
||||
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove item from playlist', error)
|
||||
this.$toast.error('Failed to remove item from playlist')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processingRemove = false
|
||||
|
||||
@@ -182,7 +182,7 @@ export default {
|
||||
toggleFinished(confirmed = false) {
|
||||
if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) {
|
||||
const payload = {
|
||||
message: `Are you sure you want to mark "${this.title}" as finished?`,
|
||||
message: `Are you sure you want to mark "${this.episodeTitle}" as finished?`,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.toggleFinished(true)
|
||||
@@ -233,4 +233,4 @@ export default {
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -246,7 +246,7 @@ export default {
|
||||
message: newIsFinished ? this.$strings.MessageConfirmMarkAllEpisodesFinished : this.$strings.MessageConfirmMarkAllEpisodesNotFinished,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.batchUpdateEpisodesFinished(this.episodesSorted, newIsFinished)
|
||||
this.batchUpdateEpisodesFinished(this.episodesCopy, newIsFinished)
|
||||
}
|
||||
},
|
||||
type: 'yesNo'
|
||||
@@ -270,7 +270,7 @@ export default {
|
||||
if (data.numEpisodesUpdated) {
|
||||
this.$toast.success(`${data.numEpisodesUpdated} episodes updated`)
|
||||
} else {
|
||||
this.$toast.info('No changes were made')
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -295,7 +295,7 @@ export default {
|
||||
episodeId: episode.id,
|
||||
title: episode.title,
|
||||
subtitle: this.mediaMetadata.title,
|
||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||
caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
|
||||
duration: episode.audioFile.duration || null,
|
||||
coverPath: this.media.coverPath || null
|
||||
}
|
||||
@@ -305,6 +305,7 @@ export default {
|
||||
this.batchUpdateEpisodesFinished(this.selectedEpisodes, !this.selectedIsFinished)
|
||||
},
|
||||
batchUpdateEpisodesFinished(episodes, newIsFinished) {
|
||||
if (!episodes.length) return
|
||||
this.processing = true
|
||||
|
||||
const updateProgressPayloads = episodes.map((episode) => {
|
||||
@@ -371,7 +372,7 @@ export default {
|
||||
episodeId: episode.id,
|
||||
title: episode.title,
|
||||
subtitle: this.mediaMetadata.title,
|
||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||
caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
|
||||
duration: episode.audioFile.duration || null,
|
||||
coverPath: this.media.coverPath || null
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
|
||||
<slot :disabled="disabled" :showMenu="showMenu" :clickShowMenu="clickShowMenu" :processing="processing">
|
||||
<button v-if="!processing" type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||
<span class="material-symbols text-2xl" :class="iconClass">more_vert</span>
|
||||
<span class="material-symbols text-2xl" :class="iconClass"></span>
|
||||
</button>
|
||||
<div v-else class="h-full w-full flex items-center justify-center">
|
||||
<widgets-loading-spinner />
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span v-else :class="outlined ? 'material-symbols-outlined' : 'material-symbols'" :style="{ fontSize }">{{ icon }}</span>
|
||||
<span v-else :class="outlined ? 'material-symbols' : 'material-symbols fill'" :style="{ fontSize }" v-html="icon" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -86,4 +86,4 @@ button.icon-btn:disabled::before {
|
||||
button.icon-btn:disabled span {
|
||||
color: #777;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
<div ref="wrapper" class="relative">
|
||||
<form @submit.prevent="submitForm">
|
||||
<div ref="inputWrapper" class="input-wrapper flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-2" :class="disabled ? 'pointer-events-none bg-black-300 text-gray-400' : 'bg-primary'">
|
||||
<input ref="input" v-model="textInput" :disabled="disabled" :readonly="!editable" class="h-full w-full bg-transparent focus:outline-none px-1" @focus="inputFocus" @blur="inputBlur" />
|
||||
<input ref="input" v-model="textInput" :disabled="disabled" :readonly="!editable" class="h-full w-full bg-transparent focus:outline-none px-1" @focus="inputFocus" @blur="inputBlur" @keydown="keydownHandler" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ul ref="menu" v-show="isFocused && itemsToShow.length" class="absolute z-50 mt-0 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
|
||||
<template v-for="item in itemsToShow">
|
||||
<li :key="item" class="text-gray-50 select-none relative py-2 pr-3 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
|
||||
<li :key="item" class="text-gray-50 select-none relative py-2 pr-3 cursor-pointer hover:bg-black-400" :class="isMenuItemSelected(item) ? 'text-yellow-300' : ''" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
|
||||
<div class="flex items-center">
|
||||
<span class="font-normal ml-3 block truncate">{{ item }}</span>
|
||||
</div>
|
||||
@@ -30,7 +30,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import menuKeyboardNavigationMixin from '@/mixins/menuKeyboardNavigation'
|
||||
|
||||
export default {
|
||||
mixins: [menuKeyboardNavigationMixin],
|
||||
props: {
|
||||
value: [String, Number],
|
||||
disabled: Boolean,
|
||||
@@ -81,6 +84,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
keydownHandler(e) {
|
||||
this.menuNavigationHandler(e)
|
||||
},
|
||||
setFocus() {
|
||||
if (this.$refs.input && this.editable) this.$refs.input.focus()
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
|
||||
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
|
||||
</div>
|
||||
<div class="text-gray-200 text-xs font-light mt-2 text-center">{{ text }}</div>
|
||||
<div class="text-gray-200 text-xs font-light mt-2 text-center">{{ message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -17,7 +17,12 @@ export default {
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
default: 'Please Wait...'
|
||||
default: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
message() {
|
||||
return this.text || this.$strings.MessagePleaseWait
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,4 +72,4 @@ export default {
|
||||
transform: translate(24px, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -37,7 +37,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import menuKeyboardNavigationMixin from '@/mixins/menuKeyboardNavigation'
|
||||
|
||||
export default {
|
||||
mixins: [menuKeyboardNavigationMixin],
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
@@ -63,8 +66,7 @@ export default {
|
||||
typingTimeout: null,
|
||||
isFocused: false,
|
||||
menu: null,
|
||||
filteredItems: null,
|
||||
selectedMenuItemIndex: null
|
||||
filteredItems: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -119,34 +121,8 @@ export default {
|
||||
this.filteredItems = results || []
|
||||
},
|
||||
keydownInput(event) {
|
||||
let items = this.itemsToShow
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
if (!items.length) return
|
||||
if (event.key === 'ArrowDown') {
|
||||
if (this.selectedMenuItemIndex === null) {
|
||||
this.selectedMenuItemIndex = 0
|
||||
} else {
|
||||
this.selectedMenuItemIndex = Math.min(this.selectedMenuItemIndex + 1, items.length - 1)
|
||||
}
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
if (this.selectedMenuItemIndex === null) {
|
||||
this.selectedMenuItemIndex = items.length - 1
|
||||
} else {
|
||||
this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0)
|
||||
}
|
||||
}
|
||||
this.recalcScroll()
|
||||
return
|
||||
} else if (event.key === 'Enter') {
|
||||
if (this.selectedMenuItemIndex !== null) {
|
||||
this.clickedOption(event, items[this.selectedMenuItemIndex])
|
||||
} else {
|
||||
this.submitForm()
|
||||
}
|
||||
return
|
||||
}
|
||||
this.selectedMenuItemIndex = null
|
||||
this.menuNavigationHandler(event)
|
||||
|
||||
clearTimeout(this.typingTimeout)
|
||||
this.typingTimeout = setTimeout(() => {
|
||||
this.search()
|
||||
@@ -161,24 +137,6 @@ export default {
|
||||
this.recalcMenuPos()
|
||||
}, 50)
|
||||
},
|
||||
recalcScroll() {
|
||||
if (!this.menu) return
|
||||
var menuItems = this.menu.querySelectorAll('li')
|
||||
if (!menuItems.length) return
|
||||
var selectedItem = menuItems[this.selectedMenuItemIndex]
|
||||
if (!selectedItem) return
|
||||
var menuHeight = this.menu.offsetHeight
|
||||
var itemHeight = selectedItem.offsetHeight
|
||||
var itemTop = selectedItem.offsetTop
|
||||
var itemBottom = itemTop + itemHeight
|
||||
if (itemBottom > this.menu.scrollTop + menuHeight) {
|
||||
let menuPaddingBottom = parseFloat(window.getComputedStyle(this.menu).paddingBottom)
|
||||
this.menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom
|
||||
} else if (itemTop < this.menu.scrollTop) {
|
||||
let menuPaddingTop = parseFloat(window.getComputedStyle(this.menu).paddingTop)
|
||||
this.menu.scrollTop = itemTop - menuPaddingTop
|
||||
}
|
||||
},
|
||||
recalcMenuPos() {
|
||||
if (!this.menu || !this.$refs.inputWrapper) return
|
||||
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
|
||||
@@ -317,9 +275,6 @@ export default {
|
||||
this.textInput = null
|
||||
this.currentSearch = null
|
||||
this.selectedMenuItemIndex = null
|
||||
this.$nextTick(() => {
|
||||
this.blur()
|
||||
})
|
||||
},
|
||||
submitForm() {
|
||||
if (!this.textInput) return
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<ul ref="menu" v-show="showMenu" class="absolute z-60 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
|
||||
<template v-for="item in itemsToShow">
|
||||
<li :key="item.id" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="itemsToShow[selectedMenuItemIndex] === item ? 'text-yellow-300' : ''" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
|
||||
<li :key="item.id" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="isMenuItemSelected(item) ? 'text-yellow-300' : ''" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
|
||||
<div class="flex items-center">
|
||||
<span class="font-normal ml-3 block truncate">{{ item.name }}</span>
|
||||
</div>
|
||||
@@ -40,7 +40,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import menuKeyboardNavigationMixin from '@/mixins/menuKeyboardNavigation'
|
||||
|
||||
export default {
|
||||
mixins: [menuKeyboardNavigationMixin],
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
@@ -63,8 +66,7 @@ export default {
|
||||
typingTimeout: null,
|
||||
isFocused: false,
|
||||
menu: null,
|
||||
items: [],
|
||||
selectedMenuItemIndex: null
|
||||
items: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -124,34 +126,7 @@ export default {
|
||||
this.items = results || []
|
||||
},
|
||||
keydownInput(event) {
|
||||
let items = this.itemsToShow
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
if (!items.length) return
|
||||
if (event.key === 'ArrowDown') {
|
||||
if (this.selectedMenuItemIndex === null) {
|
||||
this.selectedMenuItemIndex = 0
|
||||
} else {
|
||||
this.selectedMenuItemIndex = Math.min(this.selectedMenuItemIndex + 1, items.length - 1)
|
||||
}
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
if (this.selectedMenuItemIndex === null) {
|
||||
this.selectedMenuItemIndex = items.length - 1
|
||||
} else {
|
||||
this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0)
|
||||
}
|
||||
}
|
||||
this.recalcScroll()
|
||||
return
|
||||
} else if (event.key === 'Enter') {
|
||||
if (this.selectedMenuItemIndex !== null) {
|
||||
this.clickedOption(event, items[this.selectedMenuItemIndex])
|
||||
} else {
|
||||
this.submitForm()
|
||||
}
|
||||
return
|
||||
}
|
||||
this.selectedMenuItemIndex = null
|
||||
this.menuNavigationHandler(event)
|
||||
clearTimeout(this.typingTimeout)
|
||||
this.typingTimeout = setTimeout(() => {
|
||||
this.search()
|
||||
@@ -166,24 +141,6 @@ export default {
|
||||
this.recalcMenuPos()
|
||||
}, 50)
|
||||
},
|
||||
recalcScroll() {
|
||||
if (!this.menu) return
|
||||
var menuItems = this.menu.querySelectorAll('li')
|
||||
if (!menuItems.length) return
|
||||
var selectedItem = menuItems[this.selectedMenuItemIndex]
|
||||
if (!selectedItem) return
|
||||
var menuHeight = this.menu.offsetHeight
|
||||
var itemHeight = selectedItem.offsetHeight
|
||||
var itemTop = selectedItem.offsetTop
|
||||
var itemBottom = itemTop + itemHeight
|
||||
if (itemBottom > this.menu.scrollTop + menuHeight) {
|
||||
let menuPaddingBottom = parseFloat(window.getComputedStyle(this.menu).paddingBottom)
|
||||
this.menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom
|
||||
} else if (itemTop < this.menu.scrollTop) {
|
||||
let menuPaddingTop = parseFloat(window.getComputedStyle(this.menu).paddingTop)
|
||||
this.menu.scrollTop = itemTop - menuPaddingTop
|
||||
}
|
||||
},
|
||||
recalcMenuPos() {
|
||||
if (!this.menu || !this.$refs.inputWrapper) return
|
||||
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
|
||||
@@ -323,9 +280,6 @@ export default {
|
||||
this.textInput = null
|
||||
this.currentSearch = null
|
||||
this.selectedMenuItemIndex = null
|
||||
this.$nextTick(() => {
|
||||
this.blur()
|
||||
})
|
||||
},
|
||||
submitForm() {
|
||||
if (!this.textInput) return
|
||||
|
||||
@@ -24,12 +24,12 @@ export default {
|
||||
computed: {},
|
||||
methods: {
|
||||
clickBtn(e) {
|
||||
e.stopPropagation()
|
||||
if (this.disabled) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
this.$emit('click')
|
||||
e.stopPropagation()
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
@@ -54,4 +54,4 @@ button.icon-btn:hover:not(:disabled)::before {
|
||||
button.icon-btn:disabled::before {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -23,10 +23,10 @@
|
||||
<span class="material-symbols text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
|
||||
</div>
|
||||
<div v-if="type === 'password' && isHovering" class="absolute top-0 right-0 h-full px-4 flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-gray-400 cursor-pointer text-lg" @click.stop.prevent="showPassword = !showPassword">{{ !showPassword ? 'visibility' : 'visibility_off' }}</span>
|
||||
<span class="material-symbols text-gray-400 cursor-pointer text-lg" @click.stop.prevent="showPassword = !showPassword">{{ !showPassword ? 'visibility' : 'visibility_off' }}</span>
|
||||
</div>
|
||||
<div v-else-if="showCopy" class="absolute top-0 right-0 h-full px-4 flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-gray-400 cursor-pointer text-lg" @click.stop.prevent="copyToClipboard">{{ !hasCopied ? 'content_copy' : 'done' }}</span>
|
||||
<span class="material-symbols text-gray-400 cursor-pointer text-lg" @click.stop.prevent="copyToClipboard">{{ !hasCopied ? 'content_copy' : 'done' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="w-full bg-opacity-5 border border-opacity-60 rounded-lg flex items-center relative py-4 pl-16" :class="wrapperClass">
|
||||
<div class="absolute top-0 left-4 h-full flex items-center">
|
||||
<span class="material-symbols-outlined text-2xl">{{ icon }}</span>
|
||||
<span class="material-symbols text-2xl">{{ icon }}</span>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -30,4 +30,4 @@ export default {
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -3,67 +3,67 @@
|
||||
<form class="w-full h-full px-2 md:px-4 py-6" @submit.prevent="submitForm">
|
||||
<div class="flex flex-wrap -mx-1">
|
||||
<div class="w-full md:w-1/2 px-1">
|
||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" />
|
||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" :label="$strings.LabelSubtitle" />
|
||||
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" :label="$strings.LabelSubtitle" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap mt-2 -mx-1">
|
||||
<div class="w-full md:w-3/4 px-1">
|
||||
<!-- Authors filter only contains authors in this library, uses filter data -->
|
||||
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" :label="$strings.LabelAuthors" filter-key="authors" />
|
||||
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" :label="$strings.LabelAuthors" filter-key="authors" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" :label="$strings.LabelPublishYear" />
|
||||
<ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" :label="$strings.LabelPublishYear" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-2 -mx-1">
|
||||
<div class="flex-grow px-1">
|
||||
<widgets-series-input-widget v-model="details.series" />
|
||||
<widgets-series-input-widget v-model="details.series" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" />
|
||||
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" />
|
||||
|
||||
<div class="flex flex-wrap mt-2 -mx-1">
|
||||
<div class="w-full md:w-1/2 px-1">
|
||||
<ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" />
|
||||
<ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1 mt-2 md:mt-0">
|
||||
<ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" />
|
||||
<ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap mt-2 -mx-1">
|
||||
<div class="w-full md:w-1/2 px-1">
|
||||
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" :label="$strings.LabelNarrators" :items="narrators" />
|
||||
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" :label="$strings.LabelNarrators" :items="narrators" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" />
|
||||
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" />
|
||||
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap mt-2 -mx-1">
|
||||
<div class="w-full md:w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" />
|
||||
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" />
|
||||
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
|
||||
<div class="flex justify-center">
|
||||
<ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||
<ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
|
||||
<div class="flex justify-center">
|
||||
<ui-checkbox v-model="details.abridged" :label="$strings.LabelAbridged" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||
<ui-checkbox v-model="details.abridged" :label="$strings.LabelAbridged" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,6 +132,12 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleInputChange() {
|
||||
this.$emit('change', {
|
||||
libraryItemId: this.libraryItem.id,
|
||||
hasChanges: this.checkForChanges().hasChanges
|
||||
})
|
||||
},
|
||||
getDetails() {
|
||||
this.forceBlur()
|
||||
return this.checkForChanges()
|
||||
@@ -172,6 +178,7 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
this.handleInputChange()
|
||||
},
|
||||
forceBlur() {
|
||||
if (this.$refs.titleInput) this.$refs.titleInput.blur()
|
||||
@@ -286,4 +293,4 @@ export default {
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent>
|
||||
<span class="material-symbols" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize" aria-label="Decrease Cover Size" role="button">remove</span>
|
||||
<span class="material-symbols" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize" aria-label="Decrease Cover Size" role="button"></span>
|
||||
<p class="px-2 font-mono" style="font-size: 1rem">{{ bookCoverWidth }}</p>
|
||||
<span class="material-symbols" :class="selectedSizeIndex === availableSizes.length - 1 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="increaseSize" aria-label="Increase Cover Size" role="button">add</span>
|
||||
<span class="material-symbols" :class="selectedSizeIndex === availableSizes.length - 1 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="increaseSize" aria-label="Increase Cover Size" role="button"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -30,14 +30,14 @@
|
||||
|
||||
<div class="flex items-center justify-center">
|
||||
<widgets-loading-spinner v-if="isValidating" class="mr-2" />
|
||||
<span v-else class="material-symbols-outlined mr-2 text-xl" :class="isValid ? 'text-success' : 'text-error'">{{ isValid ? 'check_circle_outline' : 'error_outline' }}</span>
|
||||
<span v-else class="material-symbols mr-2 text-xl" :class="isValid ? 'text-success' : 'text-error'">{{ isValid ? 'check_circle_outline' : 'error_outline' }}</span>
|
||||
<p v-if="isValidating" class="text-gray-300 text-base md:text-lg text-center">{{ $strings.MessageCheckingCron }}</p>
|
||||
<p v-else-if="customCronError" class="text-error text-base md:text-lg text-center">{{ customCronError }}</p>
|
||||
<p v-else class="text-success text-base md:text-lg text-center">{{ $strings.MessageValidCronExpression }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="cronExpression && isValid" class="flex items-center justify-center text-yellow-400 mt-2">
|
||||
<span class="material-symbols-outlined mr-2 text-xl">event</span>
|
||||
<span class="material-symbols mr-2 text-xl">event</span>
|
||||
<p>{{ $strings.LabelNextScheduledRun }}: {{ nextRun }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,45 +3,45 @@
|
||||
<form class="w-full h-full px-4 py-6" @submit.prevent="submitForm">
|
||||
<div class="flex -mx-1">
|
||||
<div class="w-1/2 px-1">
|
||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" />
|
||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1">
|
||||
<ui-text-input-with-label ref="authorInput" v-model="details.author" :label="$strings.LabelAuthor" />
|
||||
<ui-text-input-with-label ref="authorInput" v-model="details.author" :label="$strings.LabelAuthor" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" :label="$strings.LabelRSSFeedURL" class="mt-2" />
|
||||
<ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" :label="$strings.LabelRSSFeedURL" class="mt-2" @input="handleInputChange" />
|
||||
|
||||
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" />
|
||||
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" />
|
||||
|
||||
<div class="flex mt-2 -mx-1">
|
||||
<div class="w-1/2 px-1">
|
||||
<ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" />
|
||||
<ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1">
|
||||
<ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" />
|
||||
<ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-2 -mx-1">
|
||||
<div class="w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" :label="$strings.LabelReleaseDate" />
|
||||
<ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" :label="$strings.LabelReleaseDate" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" />
|
||||
<ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" />
|
||||
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1 pt-6">
|
||||
<div class="flex justify-center">
|
||||
<ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||
<ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt-2 -mx-1">
|
||||
<div class="w-1/4 px-1">
|
||||
<ui-dropdown :label="$strings.LabelPodcastType" v-model="details.type" :items="podcastTypes" small class="max-w-52" />
|
||||
<ui-dropdown :label="$strings.LabelPodcastType" v-model="details.type" :items="podcastTypes" small class="max-w-52" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -105,6 +105,12 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleInputChange() {
|
||||
this.$emit('change', {
|
||||
libraryItemId: this.libraryItem.id,
|
||||
hasChanges: this.checkForChanges().hasChanges
|
||||
})
|
||||
},
|
||||
getDetails() {
|
||||
this.forceBlur()
|
||||
return this.checkForChanges()
|
||||
@@ -136,6 +142,8 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.handleInputChange()
|
||||
},
|
||||
forceBlur() {
|
||||
if (this.$refs.titleInput) this.$refs.titleInput.blur()
|
||||
|
||||
83
client/mixins/menuKeyboardNavigation.js
Normal file
83
client/mixins/menuKeyboardNavigation.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Mixin for keyboard navigation in dropdown menus.
|
||||
* This can be used in any component that has a dropdown menu with <li> items.
|
||||
* The following example shows how to use this mixin in your component:
|
||||
* <template>
|
||||
* <div>
|
||||
* <input type="text" @keydown="menuNavigationHandler">
|
||||
* <ul ref="menu">
|
||||
* <li v-for="(item, index) in itemsToShow" :key="index" :class="isMenuItemSelected(item) ? ... : ''" @click="clickedOption($event, item)">
|
||||
* {{ item }}
|
||||
* </li>
|
||||
* </ul>
|
||||
* </div>
|
||||
* </template>
|
||||
*
|
||||
* This mixin assumes the following are defined in your component:
|
||||
* itemsToShow: Array of items to show in the dropdown
|
||||
* clickedOption: Event handler for when an item is clicked
|
||||
* submitForm: Event handler for when the form is submitted
|
||||
*
|
||||
* It also assumes you have a ref="menu" on the menu element.
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
selectedMenuItemIndex: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
menuNavigationHandler(event) {
|
||||
let items = this.itemsToShow
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
if (!items.length) return
|
||||
if (event.key === 'ArrowDown') {
|
||||
if (this.selectedMenuItemIndex === null) {
|
||||
this.selectedMenuItemIndex = 0
|
||||
} else {
|
||||
this.selectedMenuItemIndex = Math.min(this.selectedMenuItemIndex + 1, items.length - 1)
|
||||
}
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
if (this.selectedMenuItemIndex === null) {
|
||||
this.selectedMenuItemIndex = items.length - 1
|
||||
} else {
|
||||
this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0)
|
||||
}
|
||||
}
|
||||
this.recalcScroll()
|
||||
} else if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
if (this.selectedMenuItemIndex !== null) {
|
||||
this.clickedOption(event, items[this.selectedMenuItemIndex])
|
||||
} else {
|
||||
this.submitForm()
|
||||
}
|
||||
} else {
|
||||
this.selectedMenuItemIndex = null
|
||||
}
|
||||
},
|
||||
recalcScroll() {
|
||||
const menu = this.$refs.menu
|
||||
if (!menu) return
|
||||
var menuItems = menu.querySelectorAll('li')
|
||||
if (!menuItems.length) return
|
||||
var selectedItem = menuItems[this.selectedMenuItemIndex]
|
||||
if (!selectedItem) return
|
||||
var menuHeight = menu.offsetHeight
|
||||
var itemHeight = selectedItem.offsetHeight
|
||||
var itemTop = selectedItem.offsetTop
|
||||
var itemBottom = itemTop + itemHeight
|
||||
if (itemBottom > menu.scrollTop + menuHeight) {
|
||||
let menuPaddingBottom = parseFloat(window.getComputedStyle(menu).paddingBottom)
|
||||
menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom
|
||||
} else if (itemTop < menu.scrollTop) {
|
||||
let menuPaddingTop = parseFloat(window.getComputedStyle(menu).paddingTop)
|
||||
menu.scrollTop = itemTop - menuPaddingTop
|
||||
}
|
||||
},
|
||||
isMenuItemSelected(item) {
|
||||
return this.selectedMenuItemIndex !== null && this.itemsToShow[this.selectedMenuItemIndex] === item
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,10 +129,12 @@ module.exports = {
|
||||
// Build Configuration: https://go.nuxtjs.dev/config-build
|
||||
build: {
|
||||
postcss: {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
postcssOptions: {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watchers: {
|
||||
|
||||
5126
client/package-lock.json
generated
5126
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.12.3",
|
||||
"version": "2.14.0",
|
||||
"buildNumber": 1,
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
@@ -27,7 +27,7 @@
|
||||
"fast-average-color": "^9.4.0",
|
||||
"hls.js": "^1.5.7",
|
||||
"libarchive.js": "^1.3.0",
|
||||
"nuxt": "^2.17.3",
|
||||
"nuxt": "^2.18.1",
|
||||
"nuxt-socket-io": "^1.1.18",
|
||||
"trix": "^1.3.1",
|
||||
"v-click-outside": "^3.1.2",
|
||||
|
||||
@@ -117,10 +117,10 @@ export default {
|
||||
},
|
||||
submitChangePassword() {
|
||||
if (this.newPassword !== this.confirmPassword) {
|
||||
return this.$toast.error('New password and confirm password do not match')
|
||||
return this.$toast.error(this.$strings.ToastUserPasswordMismatch)
|
||||
}
|
||||
if (this.password === this.newPassword) {
|
||||
return this.$toast.error('Password and New Password cannot be the same')
|
||||
return this.$toast.error(this.$strings.ToastUserPasswordMustChange)
|
||||
}
|
||||
this.changingPassword = true
|
||||
this.$axios
|
||||
@@ -130,16 +130,16 @@ export default {
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
this.$toast.success('Password Changed Successfully')
|
||||
this.$toast.success(this.$strings.ToastUserPasswordChangeSuccess)
|
||||
this.resetForm()
|
||||
} else {
|
||||
this.$toast.error(res.error || 'Unknown Error')
|
||||
this.$toast.error(res.error || this.$strings.ToastUnknownError)
|
||||
}
|
||||
this.changingPassword = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
this.$toast.error('Api call failed')
|
||||
this.$toast.error(this.$strings.ToastUnknownError)
|
||||
this.changingPassword = false
|
||||
})
|
||||
}
|
||||
@@ -148,4 +148,4 @@ export default {
|
||||
this.selectedLanguage = this.$languageCodes.current
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<div class="flex items-center">
|
||||
<ui-tooltip :text="$strings.MessageRemoveChapter" direction="bottom">
|
||||
<button v-if="newChapters.length > 1" class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-error transform hover:scale-110 duration-150" @click="removeChapter(chapter)">
|
||||
<span class="material-symbols-outlined text-base">remove</span>
|
||||
<span class="material-symbols text-base">remove</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
|
||||
@@ -84,14 +84,14 @@
|
||||
<ui-tooltip :text="selectedChapterId === chapter.id && isPlayingChapter ? $strings.MessagePauseChapter : $strings.MessagePlayChapter" direction="bottom">
|
||||
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150" @click="playChapter(chapter)">
|
||||
<widgets-loading-spinner v-if="selectedChapterId === chapter.id && isLoadingChapter" />
|
||||
<span v-else-if="selectedChapterId === chapter.id && isPlayingChapter" class="material-symbols-outlined text-base">pause</span>
|
||||
<span v-else class="material-symbols-outlined text-base">play_arrow</span>
|
||||
<span v-else-if="selectedChapterId === chapter.id && isPlayingChapter" class="material-symbols text-base">pause</span>
|
||||
<span v-else class="material-symbols text-base">play_arrow</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip v-if="chapter.error" :text="chapter.error" direction="left">
|
||||
<button class="w-7 h-7 rounded-full flex items-center justify-center text-error">
|
||||
<span class="material-symbols-outlined text-lg">error_outline</span>
|
||||
<span class="material-symbols text-lg">error_outline</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -106,7 +106,7 @@
|
||||
<div class="flex-grow" />
|
||||
<ui-btn small @click="setChaptersFromTracks">{{ $strings.ButtonSetChaptersFromTracks }}</ui-btn>
|
||||
<ui-tooltip :text="$strings.MessageSetChaptersFromTracksDescription" direction="top" class="flex items-center mx-1 cursor-default">
|
||||
<span class="material-symbols-outlined text-xl text-gray-200">info</span>
|
||||
<span class="material-symbols text-xl text-gray-200">info</span>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
|
||||
@@ -189,7 +189,7 @@
|
||||
<div class="flex items-center pt-2">
|
||||
<ui-btn small color="primary" class="mr-1" @click="applyChapterNamesOnly">{{ $strings.ButtonMapChapterTitles }}</ui-btn>
|
||||
<ui-tooltip :text="$strings.MessageMapChapterTitles" direction="top" class="flex items-center">
|
||||
<span class="material-symbols-outlined text-xl text-gray-200">info</span>
|
||||
<span class="material-symbols text-xl text-gray-200">info</span>
|
||||
</ui-tooltip>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn small color="success" @click="applyChapterData">{{ $strings.ButtonApplyChapters }}</ui-btn>
|
||||
@@ -499,7 +499,7 @@ export default {
|
||||
.catch((error) => {
|
||||
this.saving = false
|
||||
console.error('Failed to update chapters', error)
|
||||
this.$toast.error('Failed to update chapters')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
},
|
||||
applyChapterNamesOnly() {
|
||||
@@ -560,7 +560,7 @@ export default {
|
||||
.catch((error) => {
|
||||
this.findingChapters = false
|
||||
console.error('Failed to get chapter data', error)
|
||||
this.$toast.error('Failed to find chapters')
|
||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||
this.showFindChaptersModal = false
|
||||
})
|
||||
},
|
||||
@@ -611,7 +611,7 @@ export default {
|
||||
.$post(`/api/items/${this.libraryItem.id}/chapters`, payload)
|
||||
.then((data) => {
|
||||
if (data.updated) {
|
||||
this.$toast.success('Chapters removed')
|
||||
this.$toast.success(this.$strings.ToastChaptersRemoved)
|
||||
if (this.previousRoute) {
|
||||
this.$router.push(this.previousRoute)
|
||||
} else {
|
||||
@@ -623,20 +623,32 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove chapters', error)
|
||||
this.$toast.error('Failed to remove chapters')
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.saving = false
|
||||
})
|
||||
},
|
||||
libraryItemUpdated(libraryItem) {
|
||||
if (libraryItem.id === this.libraryItem.id) {
|
||||
if (!!libraryItem.media.metadata.asin && this.mediaMetadata.asin !== libraryItem.media.metadata.asin) {
|
||||
this.asinInput = libraryItem.media.metadata.asin
|
||||
}
|
||||
this.libraryItem = libraryItem
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.regionInput = localStorage.getItem('audibleRegion') || 'US'
|
||||
this.asinInput = this.mediaMetadata.asin || null
|
||||
this.initChapters()
|
||||
|
||||
this.$eventBus.$on(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.destroyAudioEl()
|
||||
|
||||
this.$eventBus.$off(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -331,11 +331,11 @@ export default {
|
||||
this.$axios
|
||||
.$delete(`/api/tools/item/${this.libraryItemId}/encode-m4b`)
|
||||
.then(() => {
|
||||
this.$toast.success('Encode canceled')
|
||||
this.$toast.success(this.$strings.ToastEncodeCancelSucces)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to cancel encode', error)
|
||||
this.$toast.error('Failed to cancel encode')
|
||||
this.$toast.error(this.$strings.ToastEncodeCancelFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.isCancelingEncode = false
|
||||
|
||||
@@ -97,8 +97,8 @@
|
||||
<div class="flex justify-center flex-wrap">
|
||||
<template v-for="libraryItem in libraryItemCopies">
|
||||
<div :key="libraryItem.id" class="w-full max-w-3xl border border-black-300 p-6 -ml-px -mt-px">
|
||||
<widgets-book-details-edit v-if="libraryItem.mediaType === 'book'" :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" />
|
||||
<widgets-podcast-details-edit v-else :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" />
|
||||
<widgets-book-details-edit v-if="libraryItem.mediaType === 'book'" :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" @change="handleItemChange" />
|
||||
<widgets-podcast-details-edit v-else :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" @change="handleItemChange" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -108,7 +108,7 @@
|
||||
|
||||
<div :class="isScrollable ? 'fixed left-0 box-shadow-lg-up bg-primary' : ''" class="w-full h-20 px-4 flex items-center border-t border-bg z-40" :style="{ bottom: streamLibraryItem ? '165px' : '0px' }">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" :padding-x="8" class="text-lg" :loading="isProcessing" @click.prevent="saveClick">{{ $strings.ButtonSave }}</ui-btn>
|
||||
<ui-btn color="success" :padding-x="8" class="text-lg" :loading="isProcessing" :disabled="!hasChanges" @click.prevent="saveClick">{{ $strings.ButtonSave }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -170,7 +170,8 @@ export default {
|
||||
abridged: false
|
||||
},
|
||||
appendableKeys: ['authors', 'genres', 'tags', 'narrators', 'series'],
|
||||
openMapOptions: false
|
||||
openMapOptions: false,
|
||||
itemsWithChanges: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -221,9 +222,19 @@ export default {
|
||||
},
|
||||
hasSelectedBatchUsage() {
|
||||
return Object.values(this.selectedBatchUsage).some((b) => !!b)
|
||||
},
|
||||
hasChanges() {
|
||||
return this.itemsWithChanges.length > 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleItemChange(itemChange) {
|
||||
if (!itemChange.hasChanges) {
|
||||
this.itemsWithChanges = this.itemsWithChanges.filter((id) => id !== itemChange.libraryItemId)
|
||||
} else if (!this.itemsWithChanges.includes(itemChange.libraryItemId)) {
|
||||
this.itemsWithChanges.push(itemChange.libraryItemId)
|
||||
}
|
||||
},
|
||||
blurBatchForm() {
|
||||
if (this.$refs.seriesSelect && this.$refs.seriesSelect.isFocused) {
|
||||
this.$refs.seriesSelect.forceBlur()
|
||||
@@ -283,38 +294,10 @@ export default {
|
||||
removedSeriesItem(item) {},
|
||||
newNarratorItem(item) {},
|
||||
removedNarratorItem(item) {},
|
||||
newTagItem(item) {
|
||||
// if (item && !this.newTagItems.includes(item)) {
|
||||
// this.newTagItems.push(item)
|
||||
// }
|
||||
},
|
||||
removedTagItem(item) {
|
||||
// If newly added, remove if not used on any other items
|
||||
// if (item && this.newTagItems.includes(item)) {
|
||||
// var usedByOtherAb = this.libraryItemCopies.find((ab) => {
|
||||
// return ab.tags && ab.tags.includes(item)
|
||||
// })
|
||||
// if (!usedByOtherAb) {
|
||||
// this.newTagItems = this.newTagItems.filter((t) => t !== item)
|
||||
// }
|
||||
// }
|
||||
},
|
||||
newGenreItem(item) {
|
||||
// if (item && !this.newGenreItems.includes(item)) {
|
||||
// this.newGenreItems.push(item)
|
||||
// }
|
||||
},
|
||||
removedGenreItem(item) {
|
||||
// If newly added, remove if not used on any other items
|
||||
// if (item && this.newGenreItems.includes(item)) {
|
||||
// var usedByOtherAb = this.libraryItemCopies.find((ab) => {
|
||||
// return ab.book.genres && ab.book.genres.includes(item)
|
||||
// })
|
||||
// if (!usedByOtherAb) {
|
||||
// this.newGenreItems = this.newGenreItems.filter((t) => t !== item)
|
||||
// }
|
||||
// }
|
||||
},
|
||||
newTagItem(item) {},
|
||||
removedTagItem(item) {},
|
||||
newGenreItem(item) {},
|
||||
removedGenreItem(item) {},
|
||||
init() {
|
||||
// TODO: Better deep cloning of library items
|
||||
this.libraryItemCopies = this.libraryItems.map((li) => {
|
||||
@@ -366,7 +349,7 @@ export default {
|
||||
}
|
||||
}
|
||||
if (!updates.length) {
|
||||
return this.$toast.warning('No updates were made')
|
||||
return this.$toast.warning(this.$strings.ToastNoUpdatesNecessary)
|
||||
}
|
||||
|
||||
console.log('Pushing updates', updates)
|
||||
@@ -376,6 +359,7 @@ export default {
|
||||
.then((data) => {
|
||||
this.isProcessing = false
|
||||
if (data.updates) {
|
||||
this.itemsWithChanges = []
|
||||
this.$toast.success(`Successfully updated ${data.updates} items`)
|
||||
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf`)
|
||||
} else {
|
||||
@@ -387,10 +371,28 @@ export default {
|
||||
this.$toast.error('Failed to batch update')
|
||||
this.isProcessing = false
|
||||
})
|
||||
},
|
||||
beforeUnload(e) {
|
||||
if (!e || !this.hasChanges) return
|
||||
e.preventDefault()
|
||||
e.returnValue = ''
|
||||
}
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
if (this.hasChanges) {
|
||||
next(false)
|
||||
window.location = to.path
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
|
||||
window.addEventListener('beforeunload', this.beforeUnload)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('beforeunload', this.beforeUnload)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -406,4 +408,4 @@ export default {
|
||||
transform: translateY(-100%);
|
||||
transition: all 150ms ease-in 0s;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="clickPlay">
|
||||
<span v-show="!streaming" class="material-symbols fill text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
||||
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlayAll }}
|
||||
</ui-btn>
|
||||
|
||||
<!-- RSS feed -->
|
||||
|
||||
@@ -317,7 +317,7 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update server settings', error)
|
||||
this.$toast.error(this.$strings.ToastServerSettingsUpdateFailed)
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.savingSettings = false
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<app-settings-content :header-text="$strings.HeaderBackups" :description="$strings.MessageBackupsDescription">
|
||||
<div v-if="backupLocation" class="mb-4 max-w-full overflow-hidden">
|
||||
<div class="flex items-center mb-0.5">
|
||||
<span class="material-symbols-outlined text-2xl text-black-50 mr-2">folder</span>
|
||||
<span class="material-symbols text-2xl text-black-50 mr-2">folder</span>
|
||||
<span class="text-white text-opacity-60 uppercase text-sm whitespace-nowrap">{{ $strings.LabelBackupLocation }}:</span>
|
||||
</div>
|
||||
<div v-if="!showEditBackupPath" class="inline-flex items-center w-full overflow-hidden">
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
<div v-if="enableBackups" class="mb-6">
|
||||
<div class="flex items-center pl-0 sm:pl-6 mb-2">
|
||||
<span class="material-symbols-outlined text-xl sm:text-2xl text-black-50 mr-2">schedule</span>
|
||||
<span class="material-symbols text-xl sm:text-2xl text-black-50 mr-2">schedule</span>
|
||||
<div class="w-32 min-w-32 sm:w-40 sm:min-w-40">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.HeaderSchedule }}:</span>
|
||||
</div>
|
||||
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
|
||||
<div v-if="nextBackupDate" class="flex items-center pl-0 sm:pl-6 py-0.5">
|
||||
<span class="material-symbols-outlined text-xl sm:text-2xl text-black-50 mr-2">event</span>
|
||||
<span class="material-symbols text-xl sm:text-2xl text-black-50 mr-2">event</span>
|
||||
<div class="w-32 min-w-32 sm:w-40 sm:min-w-40">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNextBackupDate }}:</span>
|
||||
</div>
|
||||
@@ -162,7 +162,7 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to save backup path', error)
|
||||
const errorMsg = error.response?.data || 'Failed to save backup path'
|
||||
const errorMsg = error.response?.data || this.$strings.ToastFailedToUpdate
|
||||
this.$toast.error(errorMsg)
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -171,11 +171,11 @@ export default {
|
||||
},
|
||||
updateBackupsSettings() {
|
||||
if (isNaN(this.maxBackupSize) || this.maxBackupSize < 0) {
|
||||
this.$toast.error('Invalid maximum backup size')
|
||||
this.$toast.error(this.$strings.ToastBackupInvalidMaxSize)
|
||||
return
|
||||
}
|
||||
if (isNaN(this.backupsToKeep) || this.backupsToKeep <= 0 || this.backupsToKeep > 99) {
|
||||
this.$toast.error('Invalid number of backups to keep')
|
||||
this.$toast.error(this.$strings.ToastBackupInvalidMaxKeep)
|
||||
return
|
||||
}
|
||||
const updatePayload = {
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
<div v-else-if="!loading" class="text-center py-4">
|
||||
<p class="text-lg text-gray-100">No Devices</p>
|
||||
<p class="text-lg text-gray-100">{{ $strings.MessageNoDevices }}</p>
|
||||
</div>
|
||||
</app-settings-content>
|
||||
|
||||
@@ -199,7 +199,7 @@ export default {
|
||||
},
|
||||
deleteDeviceClick(device) {
|
||||
const payload = {
|
||||
message: `Are you sure you want to delete e-reader device "${device.name}"?`,
|
||||
message: this.$getString('MessageConfirmDeleteDevice', [device.name]),
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.deleteDevice(device)
|
||||
@@ -218,11 +218,10 @@ export default {
|
||||
.$post(`/api/emails/ereader-devices`, payload)
|
||||
.then((data) => {
|
||||
this.ereaderDevicesUpdated(data.ereaderDevices)
|
||||
this.$toast.success('Device deleted')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to delete device', error)
|
||||
this.$toast.error('Failed to delete device')
|
||||
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.deletingDeviceName = null
|
||||
@@ -246,11 +245,11 @@ export default {
|
||||
this.$axios
|
||||
.$post('/api/emails/test')
|
||||
.then(() => {
|
||||
this.$toast.success('Test Email Sent')
|
||||
this.$toast.success(this.$strings.ToastDeviceTestEmailSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to send test email', error)
|
||||
const errorMsg = error.response.data || 'Failed to send test email'
|
||||
const errorMsg = error.response.data || this.$strings.ToastDeviceTestEmailFailed
|
||||
this.$toast.error(errorMsg)
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -289,11 +288,11 @@ export default {
|
||||
this.newSettings = {
|
||||
...data.settings
|
||||
}
|
||||
this.$toast.success('Email settings updated')
|
||||
this.$toast.success(this.$strings.ToastEmailSettingsUpdateSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update email settings', error)
|
||||
this.$toast.error('Failed to update email settings')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.savingSettings = false
|
||||
|
||||
@@ -290,7 +290,7 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update prefixes', error)
|
||||
this.$toast.error(this.$strings.ToastSortingPrefixesUpdateFailed)
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.savingPrefixes = false
|
||||
@@ -328,7 +328,6 @@ export default {
|
||||
.dispatch('updateServerSettings', payload)
|
||||
.then(() => {
|
||||
this.updatingServerSettings = false
|
||||
this.$toast.success(this.$strings.ToastServerSettingsUpdateSuccess)
|
||||
|
||||
if (payload.language) {
|
||||
// Updating language after save allows for re-rendering
|
||||
@@ -338,7 +337,7 @@ export default {
|
||||
.catch((error) => {
|
||||
console.error('Failed to update server settings', error)
|
||||
this.updatingServerSettings = false
|
||||
this.$toast.error(this.$strings.ToastServerSettingsUpdateFailed)
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
},
|
||||
initServerSettings() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user