mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-09 19:01:41 +00:00
Merge branch 'advplyr:master' into feat/all-stats
This commit is contained in:
commit
d6cee0e569
336 changed files with 21709 additions and 13151 deletions
33
.github/pull_request_template.md
vendored
Normal file
33
.github/pull_request_template.md
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<!--
|
||||||
|
For Work In Progress Pull Requests, please use the Draft PR feature,
|
||||||
|
see https://github.blog/2019-02-14-introducing-draft-pull-requests/ for further details.
|
||||||
|
|
||||||
|
If you do not follow this template, the PR may be closed without review.
|
||||||
|
|
||||||
|
Please ensure all checks pass.
|
||||||
|
If you are a new contributor, the workflows will need to be manually approved before they run.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Brief summary
|
||||||
|
|
||||||
|
<!-- Please provide a brief summary of what your PR attempts to achieve. -->
|
||||||
|
|
||||||
|
## Which issue is fixed?
|
||||||
|
|
||||||
|
<!-- Which issue number does this PR fix? Ex: "Fixes #1234" -->
|
||||||
|
|
||||||
|
## In-depth Description
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Describe your solution in more depth.
|
||||||
|
How does it work? Why is this the best solution?
|
||||||
|
Does it solve a problem that affects multiple users or is this an edge case for your setup?
|
||||||
|
-->
|
||||||
|
|
||||||
|
## How have you tested this?
|
||||||
|
|
||||||
|
<!-- Please describe in detail with reproducible steps how you tested your changes. -->
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
<!-- If your PR includes any changes to the web client, please include screenshots or a short video from before and after your changes. -->
|
||||||
55
.github/workflows/apply_comments.yaml
vendored
Normal file
55
.github/workflows/apply_comments.yaml
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
name: Add issue comments by label
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types:
|
||||||
|
- labeled
|
||||||
|
jobs:
|
||||||
|
help-wanted:
|
||||||
|
if: github.event.label.name == 'help wanted'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- name: Help wanted comment
|
||||||
|
run: gh issue comment "$NUMBER" --body "$BODY"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
NUMBER: ${{ github.event.issue.number }}
|
||||||
|
BODY: >
|
||||||
|
This issue is not able to be completed due to limited bandwidth or access to the required test hardware.
|
||||||
|
|
||||||
|
This issue is available for anyone to work on.
|
||||||
|
|
||||||
|
|
||||||
|
config-issue:
|
||||||
|
if: github.event.label.name == 'config-issue'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- name: Config issue comment
|
||||||
|
run: gh issue close "$NUMBER" --reason "not planned" --comment "$BODY"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
NUMBER: ${{ github.event.issue.number }}
|
||||||
|
BODY: >
|
||||||
|
After reviewing this issue, this appears to be a problem with your setup and not Audiobookshelf. This issue is being closed to keep the issue tracker focused on Audiobookshelf itself. Please reach out on the Audiobookshelf Discord for community support.
|
||||||
|
|
||||||
|
Some common search terms to help you find the solution to your problem:
|
||||||
|
- Reverse proxy
|
||||||
|
- Enabling websockets
|
||||||
|
- SSL (https vs http)
|
||||||
|
- Configuring a static IP
|
||||||
|
- `localhost` versus IP address
|
||||||
|
- hairpin NAT
|
||||||
|
- VPN
|
||||||
|
- firewall ports
|
||||||
|
- public versus private network
|
||||||
|
- bridge versus host mode
|
||||||
|
- Docker networking
|
||||||
|
- DNS (such as EAI_AGAIN errors)
|
||||||
|
|
||||||
|
After you have followed these steps, please post the solution or steps you followed to fix the problem to help others in the future, or show that it is a problem with Audiobookshelf so we can reopen the issue.
|
||||||
|
|
||||||
77
.github/workflows/codeql.yml
vendored
77
.github/workflows/codeql.yml
vendored
|
|
@ -1,11 +1,25 @@
|
||||||
name: "CodeQL"
|
name: 'CodeQL'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ 'master' ]
|
branches: ['master']
|
||||||
|
# Only build when files in these directories have been changed
|
||||||
|
paths:
|
||||||
|
- client/**
|
||||||
|
- server/**
|
||||||
|
- test/**
|
||||||
|
- index.js
|
||||||
|
- package.json
|
||||||
pull_request:
|
pull_request:
|
||||||
# The branches below must be a subset of the branches above
|
# The branches below must be a subset of the branches above
|
||||||
branches: [ 'master' ]
|
branches: ['master']
|
||||||
|
# Only build when files in these directories have been changed
|
||||||
|
paths:
|
||||||
|
- client/**
|
||||||
|
- server/**
|
||||||
|
- test/**
|
||||||
|
- index.js
|
||||||
|
- package.json
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '16 5 * * 4'
|
- cron: '16 5 * * 4'
|
||||||
|
|
||||||
|
|
@ -21,45 +35,44 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
language: [ 'javascript' ]
|
language: ['javascript']
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||||
# Use only 'java' to analyze code written in Java, Kotlin or both
|
# Use only 'java' to analyze code written in Java, Kotlin or both
|
||||||
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
||||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
# By default, queries listed here will override any specified in a config file.
|
# By default, queries listed here will override any specified in a config file.
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
|
||||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||||
# queries: security-extended,security-and-quality
|
# queries: security-extended,security-and-quality
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
- name: Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@v2
|
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||||
|
|
||||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
# - run: |
|
||||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
# echo "Run, Build Application using script"
|
||||||
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
# - run: |
|
- name: Perform CodeQL Analysis
|
||||||
# echo "Run, Build Application using script"
|
uses: github/codeql-action/analyze@v2
|
||||||
# ./location_of_script_within_repo/buildscript.sh
|
with:
|
||||||
|
category: '/language:${{matrix.language}}'
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v2
|
|
||||||
with:
|
|
||||||
category: "/language:${{matrix.language}}"
|
|
||||||
|
|
|
||||||
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
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
tags: ${{ github.event.inputs.tags || steps.meta.outputs.tags }}
|
tags: ${{ github.event.inputs.tags || steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
|
|
|
||||||
7
.github/workflows/integration-test.yml
vendored
7
.github/workflows/integration-test.yml
vendored
|
|
@ -5,6 +5,13 @@ on:
|
||||||
push:
|
push:
|
||||||
branches-ignore:
|
branches-ignore:
|
||||||
- 'dependabot/**' # Don't run dependabot branches, as they are already covered by pull requests
|
- 'dependabot/**' # Don't run dependabot branches, as they are already covered by pull requests
|
||||||
|
# Only build when files in these directories have been changed
|
||||||
|
paths:
|
||||||
|
- client/**
|
||||||
|
- server/**
|
||||||
|
- test/**
|
||||||
|
- index.js
|
||||||
|
- package.json
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -7,6 +7,7 @@
|
||||||
/podcasts/
|
/podcasts/
|
||||||
/media/
|
/media/
|
||||||
/metadata/
|
/metadata/
|
||||||
|
/plugins/
|
||||||
/client/.nuxt/
|
/client/.nuxt/
|
||||||
/client/dist/
|
/client/dist/
|
||||||
/dist/
|
/dist/
|
||||||
|
|
@ -16,6 +17,7 @@
|
||||||
/ffmpeg*
|
/ffmpeg*
|
||||||
/ffprobe*
|
/ffprobe*
|
||||||
/unicode*
|
/unicode*
|
||||||
|
/libnusqlite3*
|
||||||
|
|
||||||
sw.*
|
sw.*
|
||||||
.DS_STORE
|
.DS_STORE
|
||||||
|
|
|
||||||
33
Dockerfile
33
Dockerfile
|
|
@ -11,20 +11,35 @@ FROM node:20-alpine
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add --no-cache --update \
|
apk add --no-cache --update \
|
||||||
curl \
|
curl \
|
||||||
tzdata \
|
tzdata \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
make \
|
make \
|
||||||
gcompat \
|
python3 \
|
||||||
python3 \
|
g++ \
|
||||||
g++ \
|
tini \
|
||||||
tini
|
unzip
|
||||||
|
|
||||||
COPY --from=build /client/dist /client/dist
|
COPY --from=build /client/dist /client/dist
|
||||||
COPY index.js package* /
|
COPY index.js package* /
|
||||||
COPY server server
|
COPY server server
|
||||||
|
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
|
ENV NUSQLITE3_DIR="/usr/local/lib/nusqlite3"
|
||||||
|
ENV NUSQLITE3_PATH="${NUSQLITE3_DIR}/libnusqlite3.so"
|
||||||
|
|
||||||
|
RUN case "$TARGETPLATFORM" in \
|
||||||
|
"linux/amd64") \
|
||||||
|
curl -L -o /tmp/library.zip "https://github.com/mikiher/nunicode-sqlite/releases/download/v1.2/libnusqlite3-linux-musl-x64.zip" ;; \
|
||||||
|
"linux/arm64") \
|
||||||
|
curl -L -o /tmp/library.zip "https://github.com/mikiher/nunicode-sqlite/releases/download/v1.2/libnusqlite3-linux-musl-arm64.zip" ;; \
|
||||||
|
*) echo "Unsupported platform: $TARGETPLATFORM" && exit 1 ;; \
|
||||||
|
esac && \
|
||||||
|
unzip /tmp/library.zip -d $NUSQLITE3_DIR && \
|
||||||
|
rm /tmp/library.zip
|
||||||
|
|
||||||
RUN npm ci --only=production
|
RUN npm ci --only=production
|
||||||
|
|
||||||
RUN apk del make python3 g++
|
RUN apk del make python3 g++
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-16 bg-primary relative">
|
<div class="w-full h-16 bg-primary relative">
|
||||||
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-60">
|
<div id="appbar" role="toolbar" aria-label="Appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-60">
|
||||||
<div class="flex h-full items-center">
|
<div class="flex h-full items-center">
|
||||||
<nuxt-link to="/">
|
<nuxt-link to="/">
|
||||||
<img src="~static/icon.svg" :alt="$strings.ButtonHome" class="w-8 min-w-8 h-8 mr-2 sm:w-10 sm:min-w-10 sm:h-10 sm:mr-4" />
|
<img src="~static/icon.svg" :alt="$strings.ButtonHome" class="w-8 min-w-8 h-8 mr-2 sm:w-10 sm:min-w-10 sm:h-10 sm:mr-4" />
|
||||||
|
|
@ -264,7 +264,6 @@ export default {
|
||||||
libraryItems.forEach((item) => {
|
libraryItems.forEach((item) => {
|
||||||
let subtitle = ''
|
let subtitle = ''
|
||||||
if (item.mediaType === 'book') subtitle = item.media.metadata.authors.map((au) => au.name).join(', ')
|
if (item.mediaType === 'book') subtitle = item.media.metadata.authors.map((au) => au.name).join(', ')
|
||||||
else if (item.mediaType === 'music') subtitle = item.media.metadata.artists.join(', ')
|
|
||||||
queueItems.push({
|
queueItems.push({
|
||||||
libraryItemId: item.id,
|
libraryItemId: item.id,
|
||||||
libraryId: item.libraryId,
|
libraryId: item.libraryId,
|
||||||
|
|
@ -332,13 +331,13 @@ export default {
|
||||||
libraryItemIds: this.selectedMediaItems.map((i) => i.id)
|
libraryItemIds: this.selectedMediaItems.map((i) => i.id)
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Batch delete success')
|
this.$toast.success(this.$strings.ToastBatchDeleteSuccess)
|
||||||
this.$store.commit('globals/resetSelectedMediaItems', [])
|
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Batch delete failed', error)
|
console.error('Batch delete failed', error)
|
||||||
this.$toast.error('Batch delete failed')
|
this.$toast.error(this.$strings.ToastBatchDeleteFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.$store.commit('setProcessingBatch', false)
|
this.$store.commit('setProcessingBatch', false)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24e">
|
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24e">
|
||||||
<template v-for="(shelf, index) in supportedShelves">
|
<template v-for="(shelf, index) in supportedShelves">
|
||||||
<widgets-item-slider :shelf-id="shelf.id" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" :type="shelf.type" class="bookshelf-row pl-8e my-6e" @selectEntity="(payload) => selectEntity(payload, index)">
|
<widgets-item-slider :shelf-id="shelf.id" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" :type="shelf.type" class="bookshelf-row pl-8e my-6e" @selectEntity="(payload) => selectEntity(payload, index)">
|
||||||
<p class="font-semibold text-gray-100">{{ $strings[shelf.labelStringKey] }}</p>
|
<h2 class="font-semibold text-gray-100">{{ $strings[shelf.labelStringKey] }}</h2>
|
||||||
</widgets-item-slider>
|
</widgets-item-slider>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -347,6 +347,13 @@ export default {
|
||||||
libraryItemsAdded(libraryItems) {
|
libraryItemsAdded(libraryItems) {
|
||||||
console.log('libraryItems added', libraryItems)
|
console.log('libraryItems added', libraryItems)
|
||||||
|
|
||||||
|
// First items added to library
|
||||||
|
const isThisLibrary = libraryItems.some((li) => li.libraryId === this.currentLibraryId)
|
||||||
|
if (!this.shelves.length && !this.search && isThisLibrary) {
|
||||||
|
this.fetchCategories()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const recentlyAddedShelf = this.shelves.find((shelf) => shelf.id === 'recently-added')
|
const recentlyAddedShelf = this.shelves.find((shelf) => shelf.id === 'recently-added')
|
||||||
if (!recentlyAddedShelf) return
|
if (!recentlyAddedShelf) return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-if="shelf.type === 'authors'" class="flex items-center">
|
<div v-if="shelf.type === 'authors'" class="flex items-center">
|
||||||
<template v-for="entity in shelf.entities">
|
<template v-for="entity in shelf.entities">
|
||||||
<cards-author-card :key="entity.id" :author="entity" @hook:updated="updatedBookCard" class="mx-2e" @edit="editAuthor" />
|
<cards-author-card :key="entity.id" :authorMount="entity" @hook:updated="updatedBookCard" class="mx-2e" @edit="editAuthor" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="shelf.type === 'narrators'" class="flex items-center">
|
<div v-if="shelf.type === 'narrators'" class="flex items-center">
|
||||||
|
|
@ -37,18 +37,18 @@
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="relative text-center categoryPlacard transform z-30 top-0 left-4e md:left-8e w-44e rounded-md">
|
<div class="relative text-center categoryPlacard transform z-30 top-0 left-4e md:left-8e w-44e rounded-md">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em 0.5em` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em 0.5em` }">
|
||||||
<p :style="{ fontSize: 0.9 + 'em' }">{{ $strings[shelf.labelStringKey] }}</p>
|
<h2 :style="{ fontSize: 0.9 + 'em' }">{{ $strings[shelf.labelStringKey] }}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bookshelfDividerCategorized h-6e w-full absolute top-0 left-0 right-0 z-20"></div>
|
<div class="bookshelfDividerCategorized h-6e w-full absolute top-0 left-0 right-0 z-20"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="canScrollLeft && !isScrolling" class="hidden sm:flex absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollLeft">
|
<button v-show="canScrollLeft && !isScrolling" :aria-label="$strings.ButtonScrollLeft" class="hidden sm:flex absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollLeft">
|
||||||
<span class="material-symbols text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_left</span>
|
<span class="material-symbols text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_left</span>
|
||||||
</div>
|
</button>
|
||||||
<div v-show="canScrollRight && !isScrolling" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollRight">
|
<button v-show="canScrollRight && !isScrolling" :aria-label="$strings.ButtonScrollRight" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollRight">
|
||||||
<span class="material-symbols text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_right</span>
|
<span class="material-symbols text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_right</span>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
|
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
|
||||||
<span v-else class="material-symbols text-lg"></span>
|
<span v-else class="material-symbols text-lg"></span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
|
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
|
||||||
<svg v-else class="w-5 h-5" viewBox="0 0 24 24">
|
<svg v-else class="w-5 h-5" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
|
|
@ -42,15 +42,18 @@
|
||||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p class="text-sm">{{ $strings.ButtonAdd }}</p>
|
<p class="text-sm">{{ $strings.ButtonAdd }}</p>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
|
<p class="text-sm">{{ $strings.ButtonDownloadQueue }}</p>
|
||||||
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-40 flex items-center justify-end md:justify-start px-2 md:px-8">
|
<div id="toolbar" role="toolbar" aria-label="Library Toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-40 flex items-center justify-end md:justify-start px-2 md:px-8">
|
||||||
<!-- Series books page -->
|
<!-- Series books page -->
|
||||||
<template v-if="selectedSeries">
|
<template v-if="selectedSeries">
|
||||||
<p class="pl-2 text-base md:text-lg">
|
<p class="pl-2 text-base md:text-lg">
|
||||||
{{ seriesName }}
|
{{ seriesName }}
|
||||||
</p>
|
</p>
|
||||||
<div class="w-6 h-6 rounded-full bg-black bg-opacity-30 flex items-center justify-center ml-3">
|
<div class="w-6 h-6 rounded-full bg-black bg-opacity-30 flex items-center justify-center ml-3">
|
||||||
<span class="font-mono">{{ numShowing }}</span>
|
<span class="font-mono">{{ $formatNumber(numShowing) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
|
|
@ -62,8 +65,8 @@
|
||||||
<ui-context-menu-dropdown v-if="!isBatchSelecting && seriesContextMenuItems.length" :items="seriesContextMenuItems" class="mx-px" @action="seriesContextMenuAction" />
|
<ui-context-menu-dropdown v-if="!isBatchSelecting && seriesContextMenuItems.length" :items="seriesContextMenuItems" class="mx-px" @action="seriesContextMenuAction" />
|
||||||
</template>
|
</template>
|
||||||
<!-- library & collections page -->
|
<!-- library & collections page -->
|
||||||
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
|
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome && !isAuthorsPage">
|
||||||
<p class="hidden md:block">{{ numShowing }} {{ entityName }}</p>
|
<p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p>
|
||||||
|
|
||||||
<div class="flex-grow hidden sm:inline-block" />
|
<div class="flex-grow hidden sm:inline-block" />
|
||||||
|
|
||||||
|
|
@ -80,7 +83,7 @@
|
||||||
<controls-sort-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesSortBy" :descending.sync="settings.seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" />
|
<controls-sort-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesSortBy" :descending.sync="settings.seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" />
|
||||||
|
|
||||||
<!-- issues page remove all button -->
|
<!-- issues page remove all button -->
|
||||||
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn>
|
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ $formatNumber(numShowing) }} {{ entityName }}</ui-btn>
|
||||||
|
|
||||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
|
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -92,12 +95,14 @@
|
||||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
|
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
|
||||||
</template>
|
</template>
|
||||||
<!-- authors page -->
|
<!-- authors page -->
|
||||||
<template v-else-if="page === 'authors'">
|
<template v-else-if="isAuthorsPage">
|
||||||
<div class="flex-grow" />
|
<p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p>
|
||||||
<ui-btn v-if="userCanUpdate && authors?.length && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
|
|
||||||
|
<div class="flex-grow hidden sm:inline-block" />
|
||||||
|
<ui-btn v-if="userCanUpdate && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
|
||||||
|
|
||||||
<!-- author sort select -->
|
<!-- author sort select -->
|
||||||
<controls-sort-select v-if="authors?.length" v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" />
|
<controls-sort-select v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" />
|
||||||
</template>
|
</template>
|
||||||
<!-- home page -->
|
<!-- home page -->
|
||||||
<template v-else-if="isHome">
|
<template v-else-if="isHome">
|
||||||
|
|
@ -117,11 +122,7 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => null
|
default: () => null
|
||||||
},
|
},
|
||||||
searchQuery: String,
|
searchQuery: String
|
||||||
authors: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -246,9 +247,6 @@ export default {
|
||||||
isPodcastLibrary() {
|
isPodcastLibrary() {
|
||||||
return this.currentLibraryMediaType === 'podcast'
|
return this.currentLibraryMediaType === 'podcast'
|
||||||
},
|
},
|
||||||
isMusicLibrary() {
|
|
||||||
return this.currentLibraryMediaType === 'music'
|
|
||||||
},
|
|
||||||
isLibraryPage() {
|
isLibraryPage() {
|
||||||
return this.page === ''
|
return this.page === ''
|
||||||
},
|
},
|
||||||
|
|
@ -270,8 +268,11 @@ export default {
|
||||||
isPodcastLatestPage() {
|
isPodcastLatestPage() {
|
||||||
return this.$route.name === 'library-library-podcast-latest'
|
return this.$route.name === 'library-library-podcast-latest'
|
||||||
},
|
},
|
||||||
|
isPodcastDownloadQueuePage() {
|
||||||
|
return this.$route.name === 'library-library-podcast-download-queue'
|
||||||
|
},
|
||||||
isAuthorsPage() {
|
isAuthorsPage() {
|
||||||
return this.$route.name === 'library-library-authors'
|
return this.page === 'authors'
|
||||||
},
|
},
|
||||||
isAlbumsPage() {
|
isAlbumsPage() {
|
||||||
return this.page === 'albums'
|
return this.page === 'albums'
|
||||||
|
|
@ -281,13 +282,13 @@ export default {
|
||||||
},
|
},
|
||||||
entityName() {
|
entityName() {
|
||||||
if (this.isAlbumsPage) return 'Albums'
|
if (this.isAlbumsPage) return 'Albums'
|
||||||
if (this.isMusicLibrary) return 'Tracks'
|
|
||||||
|
|
||||||
if (this.isPodcastLibrary) return this.$strings.LabelPodcasts
|
if (this.isPodcastLibrary) return this.$strings.LabelPodcasts
|
||||||
if (!this.page) return this.$strings.LabelBooks
|
if (!this.page) return this.$strings.LabelBooks
|
||||||
if (this.isSeriesPage) return this.$strings.LabelSeries
|
if (this.isSeriesPage) return this.$strings.LabelSeries
|
||||||
if (this.isCollectionsPage) return this.$strings.LabelCollections
|
if (this.isCollectionsPage) return this.$strings.LabelCollections
|
||||||
if (this.isPlaylistsPage) return this.$strings.LabelPlaylists
|
if (this.isPlaylistsPage) return this.$strings.LabelPlaylists
|
||||||
|
if (this.isAuthorsPage) return this.$strings.LabelAuthors
|
||||||
return ''
|
return ''
|
||||||
},
|
},
|
||||||
seriesId() {
|
seriesId() {
|
||||||
|
|
@ -473,46 +474,58 @@ export default {
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/me/series/${this.seriesId}/readd-to-continue-listening`)
|
.$get(`/api/me/series/${this.seriesId}/readd-to-continue-listening`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Series re-added to continue listening')
|
this.$toast.success(this.$strings.ToastItemUpdateSuccess)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to re-add series to continue listening', error)
|
console.error('Failed to re-add series to continue listening', error)
|
||||||
this.$toast.error('Failed to re-add series to continue listening')
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processingSeries = false
|
this.processingSeries = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
async fetchAllAuthors() {
|
||||||
|
// fetch all authors from the server, in the order that they are currently displayed
|
||||||
|
const response = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/authors?sort=${this.settings.authorSortBy}&desc=${this.settings.authorSortDesc}`)
|
||||||
|
return response.authors
|
||||||
|
},
|
||||||
async matchAllAuthors() {
|
async matchAllAuthors() {
|
||||||
this.processingAuthors = true
|
this.processingAuthors = true
|
||||||
|
|
||||||
for (const author of this.authors) {
|
try {
|
||||||
const payload = {}
|
const authors = await this.fetchAllAuthors()
|
||||||
if (author.asin) payload.asin = author.asin
|
|
||||||
else payload.q = author.name
|
|
||||||
|
|
||||||
payload.region = 'us'
|
for (const author of authors) {
|
||||||
if (this.libraryProvider.startsWith('audible.')) {
|
const payload = {}
|
||||||
payload.region = this.libraryProvider.split('.').pop() || 'us'
|
if (author.asin) payload.asin = author.asin
|
||||||
|
else payload.q = author.name
|
||||||
|
|
||||||
|
payload.region = 'us'
|
||||||
|
if (this.libraryProvider.startsWith('audible.')) {
|
||||||
|
payload.region = this.libraryProvider.split('.').pop() || 'us'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$eventBus.$emit(`searching-author-${author.id}`, true)
|
||||||
|
|
||||||
|
var response = await this.$axios.$post(`/api/authors/${author.id}/match`, payload).catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
if (!response) {
|
||||||
|
console.error(`Author ${author.name} not found`)
|
||||||
|
this.$toast.error(this.$getString('ToastAuthorNotFound', [author.name]))
|
||||||
|
} else if (response.updated) {
|
||||||
|
if (response.author.imagePath) console.log(`Author ${response.author.name} was updated`)
|
||||||
|
else console.log(`Author ${response.author.name} was updated (no image found)`)
|
||||||
|
} else {
|
||||||
|
console.log(`No updates were made for Author ${response.author.name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$eventBus.$emit(`searching-author-${author.id}`, false)
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
this.$eventBus.$emit(`searching-author-${author.id}`, true)
|
console.error('Failed to match all authors', error)
|
||||||
|
this.$toast.error(this.$strings.ToastMatchAllAuthorsFailed)
|
||||||
var response = await this.$axios.$post(`/api/authors/${author.id}/match`, payload).catch((error) => {
|
|
||||||
console.error('Failed', error)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
if (!response) {
|
|
||||||
console.error(`Author ${author.name} not found`)
|
|
||||||
this.$toast.error(`Author ${author.name} not found`)
|
|
||||||
} else if (response.updated) {
|
|
||||||
if (response.author.imagePath) console.log(`Author ${response.author.name} was updated`)
|
|
||||||
else console.log(`Author ${response.author.name} was updated (no image found)`)
|
|
||||||
} else {
|
|
||||||
console.log(`No updates were made for Author ${response.author.name}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$eventBus.$emit(`searching-author-${author.id}`, false)
|
|
||||||
}
|
}
|
||||||
this.processingAuthors = false
|
this.processingAuthors = false
|
||||||
},
|
},
|
||||||
|
|
@ -522,13 +535,13 @@ export default {
|
||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/libraries/${this.currentLibraryId}/issues`)
|
.$delete(`/api/libraries/${this.currentLibraryId}/issues`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Removed library items with issues')
|
this.$toast.success(this.$strings.ToastRemoveItemsWithIssuesSuccess)
|
||||||
this.$router.push(`/library/${this.currentLibraryId}/bookshelf`)
|
this.$router.push(`/library/${this.currentLibraryId}/bookshelf`)
|
||||||
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
|
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove library items with issues', 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(() => {
|
.finally(() => {
|
||||||
this.processingIssues = false
|
this.processingIssues = false
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div role="toolbar" aria-orientation="vertical" aria-label="Config Sidebar">
|
||||||
<div class="w-44 fixed left-0 top-16 bg-bg bg-opacity-100 md:bg-opacity-70 shadow-lg border-r border-white border-opacity-5 py-3 transform transition-transform mb-12 overflow-y-auto" :class="wrapperClass + ' ' + (streamLibraryItem ? 'h-[calc(100%-270px)]' : 'h-[calc(100%-110px)]')" v-click-outside="clickOutside">
|
<div role="navigation" aria-label="Config Navigation" class="w-44 fixed left-0 top-16 bg-bg bg-opacity-100 md:bg-opacity-70 shadow-lg border-r border-white border-opacity-5 py-3 transform transition-transform mb-12 overflow-y-auto" :class="wrapperClass + ' ' + (streamLibraryItem ? 'h-[calc(100%-270px)]' : 'h-[calc(100%-110px)]')" v-click-outside="clickOutside">
|
||||||
<div v-show="isMobilePortrait" class="flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer">
|
<div v-show="isMobilePortrait" class="flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer">
|
||||||
<span class="material-symbols text-2xl">arrow_back</span>
|
<span class="material-symbols text-2xl">arrow_back</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
<p class="text-xs text-gray-300 italic">{{ Source }}</p>
|
<p class="text-xs text-gray-300 italic">{{ Source }}</p>
|
||||||
</div>
|
</div>
|
||||||
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ $config.version }}</a>
|
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ versionData.latestVersion }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
<div id="bookshelf" ref="bookshelf" class="w-full overflow-y-auto" :style="{ fontSize: sizeMultiplier + 'rem' }">
|
<div id="bookshelf" ref="bookshelf" class="w-full overflow-y-auto" :style="{ fontSize: sizeMultiplier + 'rem' }">
|
||||||
<template v-for="shelf in totalShelves">
|
<template v-for="shelf in totalShelves">
|
||||||
<div :key="shelf" :id="`shelf-${shelf - 1}`" class="w-full px-4e sm:px-8e relative" :class="{ bookshelfRow: !isAlternativeBookshelfView }" :style="{ height: shelfHeight + 'px' }">
|
<div :key="shelf" :id="`shelf-${shelf - 1}`" class="w-full px-4e sm:px-8e relative" :class="{ bookshelfRow: !isAlternativeBookshelfView }" :style="{ height: shelfHeight + 'px' }">
|
||||||
|
<!-- Card skeletons -->
|
||||||
|
<template v-for="entityIndex in entitiesInShelf(shelf)">
|
||||||
|
<div :key="entityIndex" class="w-full h-full absolute rounded z-5 top-0 left-0 bg-primary box-shadow-book" :style="{ transform: entityTransform(entityIndex), width: cardWidth + 'px', height: coverHeight + 'px' }" />
|
||||||
|
</template>
|
||||||
<div v-if="!isAlternativeBookshelfView" class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-20 h-6e" />
|
<div v-if="!isAlternativeBookshelfView" class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-20 h-6e" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -65,7 +69,13 @@ export default {
|
||||||
tempIsScanning: false,
|
tempIsScanning: false,
|
||||||
cardWidth: 0,
|
cardWidth: 0,
|
||||||
cardHeight: 0,
|
cardHeight: 0,
|
||||||
resizeObserver: null
|
coverHeight: 0,
|
||||||
|
resizeObserver: null,
|
||||||
|
lastScrollTop: 0,
|
||||||
|
lastTimestamp: 0,
|
||||||
|
postScrollTimeout: null,
|
||||||
|
currFirstEntityIndex: -1,
|
||||||
|
currLastEntityIndex: -1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
@ -91,6 +101,7 @@ export default {
|
||||||
if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries
|
if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries
|
||||||
if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollections
|
if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollections
|
||||||
if (this.page === 'playlists') return this.$strings.MessageNoUserPlaylists
|
if (this.page === 'playlists') return this.$strings.MessageNoUserPlaylists
|
||||||
|
if (this.page === 'authors') return this.$strings.MessageNoAuthors
|
||||||
if (this.hasFilter) {
|
if (this.hasFilter) {
|
||||||
if (this.filterName === 'Issues') return this.$strings.MessageNoIssues
|
if (this.filterName === 'Issues') return this.$strings.MessageNoIssues
|
||||||
else if (this.filterName === 'Feed-open') return this.$strings.MessageBookshelfNoRSSFeeds
|
else if (this.filterName === 'Feed-open') return this.$strings.MessageBookshelfNoRSSFeeds
|
||||||
|
|
@ -111,6 +122,12 @@ export default {
|
||||||
seriesFilterBy() {
|
seriesFilterBy() {
|
||||||
return this.$store.getters['user/getUserSetting']('seriesFilterBy')
|
return this.$store.getters['user/getUserSetting']('seriesFilterBy')
|
||||||
},
|
},
|
||||||
|
authorSortBy() {
|
||||||
|
return this.$store.getters['user/getUserSetting']('authorSortBy')
|
||||||
|
},
|
||||||
|
authorSortDesc() {
|
||||||
|
return !!this.$store.getters['user/getUserSetting']('authorSortDesc')
|
||||||
|
},
|
||||||
orderBy() {
|
orderBy() {
|
||||||
return this.$store.getters['user/getUserSetting']('orderBy')
|
return this.$store.getters['user/getUserSetting']('orderBy')
|
||||||
},
|
},
|
||||||
|
|
@ -164,9 +181,6 @@ export default {
|
||||||
bookWidth() {
|
bookWidth() {
|
||||||
return this.cardWidth
|
return this.cardWidth
|
||||||
},
|
},
|
||||||
bookHeight() {
|
|
||||||
return this.cardHeight
|
|
||||||
},
|
|
||||||
shelfPadding() {
|
shelfPadding() {
|
||||||
if (this.bookshelfWidth < 640) return 32 * this.sizeMultiplier
|
if (this.bookshelfWidth < 640) return 32 * this.sizeMultiplier
|
||||||
return 64 * this.sizeMultiplier
|
return 64 * this.sizeMultiplier
|
||||||
|
|
@ -177,9 +191,6 @@ export default {
|
||||||
entityWidth() {
|
entityWidth() {
|
||||||
return this.cardWidth
|
return this.cardWidth
|
||||||
},
|
},
|
||||||
entityHeight() {
|
|
||||||
return this.cardHeight
|
|
||||||
},
|
|
||||||
shelfPaddingHeight() {
|
shelfPaddingHeight() {
|
||||||
return 16
|
return 16
|
||||||
},
|
},
|
||||||
|
|
@ -217,6 +228,8 @@ export default {
|
||||||
this.$store.commit('globals/setEditCollection', entity)
|
this.$store.commit('globals/setEditCollection', entity)
|
||||||
} else if (this.entityName === 'playlists') {
|
} else if (this.entityName === 'playlists') {
|
||||||
this.$store.commit('globals/setEditPlaylist', entity)
|
this.$store.commit('globals/setEditPlaylist', entity)
|
||||||
|
} else if (this.entityName === 'authors') {
|
||||||
|
this.$store.commit('globals/showEditAuthorModal', entity)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clearSelectedEntities() {
|
clearSelectedEntities() {
|
||||||
|
|
@ -345,50 +358,53 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
loadPage(page) {
|
loadPage(page) {
|
||||||
this.pagesLoaded[page] = true
|
if (!this.pagesLoaded[page]) this.pagesLoaded[page] = this.fetchEntites(page)
|
||||||
this.fetchEntites(page)
|
return this.pagesLoaded[page]
|
||||||
},
|
},
|
||||||
showHideBookPlaceholder(index, show) {
|
showHideBookPlaceholder(index, show) {
|
||||||
var el = document.getElementById(`book-${index}-placeholder`)
|
var el = document.getElementById(`book-${index}-placeholder`)
|
||||||
if (el) el.style.display = show ? 'flex' : 'none'
|
if (el) el.style.display = show ? 'flex' : 'none'
|
||||||
},
|
},
|
||||||
mountEntites(fromIndex, toIndex) {
|
mountEntities(fromIndex, toIndex) {
|
||||||
for (let i = fromIndex; i < toIndex; i++) {
|
for (let i = fromIndex; i < toIndex; i++) {
|
||||||
if (!this.entityIndexesMounted.includes(i)) {
|
if (!this.entityIndexesMounted.includes(i)) {
|
||||||
this.cardsHelpers.mountEntityCard(i)
|
this.cardsHelpers.mountEntityCard(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleScroll(scrollTop) {
|
getVisibleIndices(scrollTop) {
|
||||||
this.currScrollTop = scrollTop
|
const firstShelfIndex = Math.floor(scrollTop / this.shelfHeight)
|
||||||
var firstShelfIndex = Math.floor(scrollTop / this.shelfHeight)
|
const lastShelfIndex = Math.min(Math.ceil((scrollTop + this.bookshelfHeight) / this.shelfHeight), this.totalShelves - 1)
|
||||||
var lastShelfIndex = Math.ceil((scrollTop + this.bookshelfHeight) / this.shelfHeight)
|
const firstEntityIndex = firstShelfIndex * this.entitiesPerShelf
|
||||||
lastShelfIndex = Math.min(this.totalShelves - 1, lastShelfIndex)
|
const lastEntityIndex = Math.min(lastShelfIndex * this.entitiesPerShelf + this.entitiesPerShelf, this.totalEntities)
|
||||||
|
return { firstEntityIndex, lastEntityIndex }
|
||||||
var firstBookIndex = firstShelfIndex * this.entitiesPerShelf
|
},
|
||||||
var lastBookIndex = lastShelfIndex * this.entitiesPerShelf + this.entitiesPerShelf
|
postScroll() {
|
||||||
lastBookIndex = Math.min(this.totalEntities, lastBookIndex)
|
const { firstEntityIndex, lastEntityIndex } = this.getVisibleIndices(this.currScrollTop)
|
||||||
|
|
||||||
var firstBookPage = Math.floor(firstBookIndex / this.booksPerFetch)
|
|
||||||
var lastBookPage = Math.floor(lastBookIndex / this.booksPerFetch)
|
|
||||||
if (!this.pagesLoaded[firstBookPage]) {
|
|
||||||
// console.log('Must load next batch', firstBookPage, 'book index', firstBookIndex)
|
|
||||||
this.loadPage(firstBookPage)
|
|
||||||
}
|
|
||||||
if (!this.pagesLoaded[lastBookPage]) {
|
|
||||||
// console.log('Must load last next batch', lastBookPage, 'book index', lastBookIndex)
|
|
||||||
this.loadPage(lastBookPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.entityIndexesMounted = this.entityIndexesMounted.filter((_index) => {
|
this.entityIndexesMounted = this.entityIndexesMounted.filter((_index) => {
|
||||||
if (_index < firstBookIndex || _index >= lastBookIndex) {
|
if (_index < firstEntityIndex || _index >= lastEntityIndex) {
|
||||||
var el = document.getElementById(`book-card-${_index}`)
|
var el = this.entityComponentRefs[_index]
|
||||||
if (el) el.remove()
|
if (el && el.$el) el.$el.remove()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
this.mountEntites(firstBookIndex, lastBookIndex)
|
},
|
||||||
|
handleScroll(scrollTop) {
|
||||||
|
this.currScrollTop = scrollTop
|
||||||
|
const { firstEntityIndex, lastEntityIndex } = this.getVisibleIndices(scrollTop)
|
||||||
|
if (firstEntityIndex === this.currFirstEntityIndex && lastEntityIndex === this.currLastEntityIndex) return
|
||||||
|
this.currFirstEntityIndex = firstEntityIndex
|
||||||
|
this.currLastEntityIndex = lastEntityIndex
|
||||||
|
|
||||||
|
clearTimeout(this.postScrollTimeout)
|
||||||
|
const firstPage = Math.floor(firstEntityIndex / this.booksPerFetch)
|
||||||
|
const lastPage = Math.floor(lastEntityIndex / this.booksPerFetch)
|
||||||
|
Promise.all([this.loadPage(firstPage), this.loadPage(lastPage)])
|
||||||
|
.then(() => this.mountEntities(firstEntityIndex, lastEntityIndex))
|
||||||
|
.catch((error) => console.error('Failed to load page', error))
|
||||||
|
|
||||||
|
this.postScrollTimeout = setTimeout(this.postScroll, 500)
|
||||||
},
|
},
|
||||||
async resetEntities() {
|
async resetEntities() {
|
||||||
if (this.isFetchingEntities) {
|
if (this.isFetchingEntities) {
|
||||||
|
|
@ -396,8 +412,6 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.destroyEntityComponents()
|
this.destroyEntityComponents()
|
||||||
this.entityIndexesMounted = []
|
|
||||||
this.entityComponentRefs = {}
|
|
||||||
this.pagesLoaded = {}
|
this.pagesLoaded = {}
|
||||||
this.entities = []
|
this.entities = []
|
||||||
this.totalShelves = 0
|
this.totalShelves = 0
|
||||||
|
|
@ -407,40 +421,21 @@ export default {
|
||||||
this.initialized = false
|
this.initialized = false
|
||||||
|
|
||||||
this.initSizeData()
|
this.initSizeData()
|
||||||
this.pagesLoaded[0] = true
|
await this.loadPage(0)
|
||||||
await this.fetchEntites(0)
|
|
||||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
||||||
this.mountEntites(0, lastBookIndex)
|
this.mountEntities(0, lastBookIndex)
|
||||||
},
|
},
|
||||||
remountEntities() {
|
async rebuild() {
|
||||||
for (const key in this.entityComponentRefs) {
|
|
||||||
if (this.entityComponentRefs[key]) {
|
|
||||||
this.entityComponentRefs[key].destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.entityComponentRefs = {}
|
|
||||||
this.entityIndexesMounted.forEach((i) => {
|
|
||||||
this.cardsHelpers.mountEntityCard(i)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
rebuild() {
|
|
||||||
this.initSizeData()
|
this.initSizeData()
|
||||||
|
|
||||||
var lastBookIndex = Math.min(this.totalEntities, this.booksPerFetch)
|
var lastBookIndex = Math.min(this.totalEntities, this.booksPerFetch)
|
||||||
this.entityIndexesMounted = []
|
this.destroyEntityComponents()
|
||||||
for (let i = 0; i < lastBookIndex; i++) {
|
await this.loadPage(0)
|
||||||
this.entityIndexesMounted.push(i)
|
|
||||||
if (!this.entities[i]) {
|
|
||||||
const page = Math.floor(i / this.booksPerFetch)
|
|
||||||
this.loadPage(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var bookshelfEl = document.getElementById('bookshelf')
|
var bookshelfEl = document.getElementById('bookshelf')
|
||||||
if (bookshelfEl) {
|
if (bookshelfEl) {
|
||||||
bookshelfEl.scrollTop = 0
|
bookshelfEl.scrollTop = 0
|
||||||
}
|
}
|
||||||
|
this.mountEntities(0, lastBookIndex)
|
||||||
this.$nextTick(this.remountEntities)
|
|
||||||
},
|
},
|
||||||
buildSearchParams() {
|
buildSearchParams() {
|
||||||
if (this.page === 'search' || this.page === 'collections') {
|
if (this.page === 'search' || this.page === 'collections') {
|
||||||
|
|
@ -457,6 +452,9 @@ export default {
|
||||||
if (this.collapseBookSeries) {
|
if (this.collapseBookSeries) {
|
||||||
searchParams.set('collapseseries', 1)
|
searchParams.set('collapseseries', 1)
|
||||||
}
|
}
|
||||||
|
} else if (this.page === 'authors') {
|
||||||
|
searchParams.set('sort', this.authorSortBy)
|
||||||
|
searchParams.set('desc', this.authorSortDesc ? 1 : 0)
|
||||||
} else {
|
} else {
|
||||||
if (this.filterBy && this.filterBy !== 'all') {
|
if (this.filterBy && this.filterBy !== 'all') {
|
||||||
searchParams.set('filter', this.filterBy)
|
searchParams.set('filter', this.filterBy)
|
||||||
|
|
@ -501,12 +499,29 @@ export default {
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
this.resetEntities()
|
this.resetEntities()
|
||||||
} else if (settings.bookshelfCoverSize !== this.currentBookWidth) {
|
} else if (settings.bookshelfCoverSize !== this.currentBookWidth) {
|
||||||
this.executeRebuild()
|
this.rebuild()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getScrollRate() {
|
||||||
|
const currentTimestamp = Date.now()
|
||||||
|
const timeDelta = currentTimestamp - this.lastTimestamp
|
||||||
|
const scrollDelta = this.currScrollTop - this.lastScrollTop
|
||||||
|
const scrollRate = Math.abs(scrollDelta) / (timeDelta || 1)
|
||||||
|
this.lastScrollTop = this.currScrollTop
|
||||||
|
this.lastTimestamp = currentTimestamp
|
||||||
|
return scrollRate
|
||||||
|
},
|
||||||
scroll(e) {
|
scroll(e) {
|
||||||
if (!e || !e.target) return
|
if (!e || !e.target) return
|
||||||
var { scrollTop } = e.target
|
clearTimeout(this.scrollTimeout)
|
||||||
|
const { scrollTop } = e.target
|
||||||
|
const scrollRate = this.getScrollRate()
|
||||||
|
if (scrollRate > 5) {
|
||||||
|
this.scrollTimeout = setTimeout(() => {
|
||||||
|
this.handleScroll(scrollTop)
|
||||||
|
}, 25)
|
||||||
|
return
|
||||||
|
}
|
||||||
this.handleScroll(scrollTop)
|
this.handleScroll(scrollTop)
|
||||||
},
|
},
|
||||||
libraryItemAdded(libraryItem) {
|
libraryItemAdded(libraryItem) {
|
||||||
|
|
@ -601,6 +616,34 @@ export default {
|
||||||
this.executeRebuild()
|
this.executeRebuild()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
authorAdded(author) {
|
||||||
|
if (this.entityName !== 'authors') return
|
||||||
|
console.log(`[LazyBookshelf] authorAdded ${author.id}`, author)
|
||||||
|
this.resetEntities()
|
||||||
|
},
|
||||||
|
authorUpdated(author) {
|
||||||
|
if (this.entityName !== 'authors') return
|
||||||
|
console.log(`[LazyBookshelf] authorUpdated ${author.id}`, author)
|
||||||
|
const indexOf = this.entities.findIndex((ent) => ent && ent.id === author.id)
|
||||||
|
if (indexOf >= 0) {
|
||||||
|
this.entities[indexOf] = author
|
||||||
|
if (this.entityComponentRefs[indexOf]) {
|
||||||
|
this.entityComponentRefs[indexOf].setEntity(author)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
authorRemoved(author) {
|
||||||
|
if (this.entityName !== 'authors') return
|
||||||
|
console.log(`[LazyBookshelf] authorRemoved ${author.id}`, author)
|
||||||
|
const indexOf = this.entities.findIndex((ent) => ent && ent.id === author.id)
|
||||||
|
if (indexOf >= 0) {
|
||||||
|
this.entities = this.entities.filter((ent) => ent.id !== author.id)
|
||||||
|
this.totalEntities--
|
||||||
|
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
|
||||||
|
this.executeRebuild()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
shareOpen(mediaItemShare) {
|
shareOpen(mediaItemShare) {
|
||||||
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
||||||
var indexOf = this.entities.findIndex((ent) => ent?.media?.id === mediaItemShare.mediaItemId)
|
var indexOf = this.entities.findIndex((ent) => ent?.media?.id === mediaItemShare.mediaItemId)
|
||||||
|
|
@ -627,13 +670,14 @@ export default {
|
||||||
},
|
},
|
||||||
updatePagesLoaded() {
|
updatePagesLoaded() {
|
||||||
let numPages = Math.ceil(this.totalEntities / this.booksPerFetch)
|
let numPages = Math.ceil(this.totalEntities / this.booksPerFetch)
|
||||||
|
this.pagesLoaded = {}
|
||||||
for (let page = 0; page < numPages; page++) {
|
for (let page = 0; page < numPages; page++) {
|
||||||
let numEntities = Math.min(this.totalEntities - page * this.booksPerFetch, this.booksPerFetch)
|
let numEntities = Math.min(this.totalEntities - page * this.booksPerFetch, this.booksPerFetch)
|
||||||
this.pagesLoaded[page] = true
|
this.pagesLoaded[page] = Promise.resolve()
|
||||||
for (let i = 0; i < numEntities; i++) {
|
for (let i = 0; i < numEntities; i++) {
|
||||||
const index = page * this.booksPerFetch + i
|
const index = page * this.booksPerFetch + i
|
||||||
if (!this.entities[index]) {
|
if (!this.entities[index]) {
|
||||||
this.pagesLoaded[page] = false
|
if (this.pagesLoaded[page]) delete this.pagesLoaded[page]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -648,7 +692,6 @@ export default {
|
||||||
var entitiesPerShelfBefore = this.entitiesPerShelf
|
var entitiesPerShelfBefore = this.entitiesPerShelf
|
||||||
|
|
||||||
var { clientHeight, clientWidth } = bookshelf
|
var { clientHeight, clientWidth } = bookshelf
|
||||||
// console.log('Init bookshelf width', clientWidth, 'window width', window.innerWidth)
|
|
||||||
this.mountWindowWidth = window.innerWidth
|
this.mountWindowWidth = window.innerWidth
|
||||||
this.bookshelfHeight = clientHeight
|
this.bookshelfHeight = clientHeight
|
||||||
this.bookshelfWidth = clientWidth
|
this.bookshelfWidth = clientWidth
|
||||||
|
|
@ -673,10 +716,9 @@ export default {
|
||||||
this.initSizeData(bookshelf)
|
this.initSizeData(bookshelf)
|
||||||
this.checkUpdateSearchParams()
|
this.checkUpdateSearchParams()
|
||||||
|
|
||||||
this.pagesLoaded[0] = true
|
await this.loadPage(0)
|
||||||
await this.fetchEntites(0)
|
|
||||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
||||||
this.mountEntites(0, lastBookIndex)
|
this.mountEntities(0, lastBookIndex)
|
||||||
|
|
||||||
// Set last scroll position for this bookshelf page
|
// Set last scroll position for this bookshelf page
|
||||||
if (this.$store.state.lastBookshelfScrollData[this.page] && window.bookshelf) {
|
if (this.$store.state.lastBookshelfScrollData[this.page] && window.bookshelf) {
|
||||||
|
|
@ -707,7 +749,7 @@ export default {
|
||||||
var bookshelf = document.getElementById('bookshelf')
|
var bookshelf = document.getElementById('bookshelf')
|
||||||
if (bookshelf) {
|
if (bookshelf) {
|
||||||
this.init(bookshelf)
|
this.init(bookshelf)
|
||||||
bookshelf.addEventListener('scroll', this.scroll)
|
bookshelf.addEventListener('scroll', this.scroll, { passive: true })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -727,6 +769,9 @@ export default {
|
||||||
this.$root.socket.on('playlist_added', this.playlistAdded)
|
this.$root.socket.on('playlist_added', this.playlistAdded)
|
||||||
this.$root.socket.on('playlist_updated', this.playlistUpdated)
|
this.$root.socket.on('playlist_updated', this.playlistUpdated)
|
||||||
this.$root.socket.on('playlist_removed', this.playlistRemoved)
|
this.$root.socket.on('playlist_removed', this.playlistRemoved)
|
||||||
|
this.$root.socket.on('author_added', this.authorAdded)
|
||||||
|
this.$root.socket.on('author_updated', this.authorUpdated)
|
||||||
|
this.$root.socket.on('author_removed', this.authorRemoved)
|
||||||
this.$root.socket.on('share_open', this.shareOpen)
|
this.$root.socket.on('share_open', this.shareOpen)
|
||||||
this.$root.socket.on('share_closed', this.shareClosed)
|
this.$root.socket.on('share_closed', this.shareClosed)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -756,6 +801,9 @@ export default {
|
||||||
this.$root.socket.off('playlist_added', this.playlistAdded)
|
this.$root.socket.off('playlist_added', this.playlistAdded)
|
||||||
this.$root.socket.off('playlist_updated', this.playlistUpdated)
|
this.$root.socket.off('playlist_updated', this.playlistUpdated)
|
||||||
this.$root.socket.off('playlist_removed', this.playlistRemoved)
|
this.$root.socket.off('playlist_removed', this.playlistRemoved)
|
||||||
|
this.$root.socket.off('author_added', this.authorAdded)
|
||||||
|
this.$root.socket.off('author_updated', this.authorUpdated)
|
||||||
|
this.$root.socket.off('author_removed', this.authorRemoved)
|
||||||
this.$root.socket.off('share_open', this.shareOpen)
|
this.$root.socket.off('share_open', this.shareOpen)
|
||||||
this.$root.socket.off('share_closed', this.shareClosed)
|
this.$root.socket.off('share_closed', this.shareClosed)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -764,10 +812,14 @@ export default {
|
||||||
},
|
},
|
||||||
destroyEntityComponents() {
|
destroyEntityComponents() {
|
||||||
for (const key in this.entityComponentRefs) {
|
for (const key in this.entityComponentRefs) {
|
||||||
if (this.entityComponentRefs[key] && this.entityComponentRefs[key].destroy) {
|
const ref = this.entityComponentRefs[key]
|
||||||
this.entityComponentRefs[key].destroy()
|
if (ref && ref.destroy) {
|
||||||
|
if (ref.$el) ref.$el.remove()
|
||||||
|
ref.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.entityComponentRefs = {}
|
||||||
|
this.entityIndexesMounted = []
|
||||||
},
|
},
|
||||||
scan() {
|
scan() {
|
||||||
this.tempIsScanning = true
|
this.tempIsScanning = true
|
||||||
|
|
@ -780,6 +832,14 @@ export default {
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.tempIsScanning = false
|
this.tempIsScanning = false
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
entitiesInShelf(shelf) {
|
||||||
|
return shelf == this.totalShelves ? this.totalEntities % this.entitiesPerShelf || this.entitiesPerShelf : this.entitiesPerShelf
|
||||||
|
},
|
||||||
|
entityTransform(entityIndex) {
|
||||||
|
const shelfOffsetY = this.shelfPaddingHeight * this.sizeMultiplier
|
||||||
|
const shelfOffsetX = (entityIndex - 1) * this.totalEntityCardWidth + this.bookshelfMarginLeft
|
||||||
|
return `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="streamLibraryItem" id="mediaPlayerContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 lg:h-40 z-50 bg-primary px-2 lg:px-4 pb-1 lg:pb-4 pt-2">
|
<div v-if="streamLibraryItem" id="mediaPlayerContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 lg:h-40 z-50 bg-primary px-2 lg:px-4 pb-1 lg:pb-4 pt-2">
|
||||||
<div id="videoDock" />
|
|
||||||
<div class="absolute left-2 top-2 lg:left-4 cursor-pointer">
|
<div class="absolute left-2 top-2 lg:left-4 cursor-pointer">
|
||||||
<covers-book-cover expand-on-click :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
<covers-book-cover expand-on-click :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-start mb-6 lg:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
|
<div class="flex items-start mb-6 lg:mb-0" :class="isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
|
||||||
<div class="min-w-0 w-full">
|
<div class="min-w-0 w-full">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
|
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
|
||||||
|
|
@ -12,10 +11,9 @@
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<widgets-explicit-indicator v-if="isExplicit" />
|
<widgets-explicit-indicator v-if="isExplicit" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5">
|
<div class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5">
|
||||||
<span class="material-symbols text-sm">person</span>
|
<span class="material-symbols text-sm">person</span>
|
||||||
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
|
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
|
||||||
<div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div>
|
|
||||||
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate">
|
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate">
|
||||||
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -55,7 +53,6 @@
|
||||||
@showBookmarks="showBookmarks"
|
@showBookmarks="showBookmarks"
|
||||||
@showSleepTimer="showSleepTimerModal = true"
|
@showSleepTimer="showSleepTimerModal = true"
|
||||||
@showPlayerQueueItems="showPlayerQueueItemsModal = true"
|
@showPlayerQueueItems="showPlayerQueueItemsModal = true"
|
||||||
@showPlayerSettings="showPlayerSettingsModal = true"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :library-item-id="libraryItemId" @select="selectBookmark" />
|
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :library-item-id="libraryItemId" @select="selectBookmark" />
|
||||||
|
|
@ -63,8 +60,6 @@
|
||||||
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-type="sleepTimerType" :remaining="sleepTimerRemaining" :has-chapters="!!chapters.length" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
|
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-type="sleepTimerType" :remaining="sleepTimerRemaining" :has-chapters="!!chapters.length" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
|
||||||
|
|
||||||
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" />
|
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" />
|
||||||
|
|
||||||
<modals-player-settings-modal v-model="showPlayerSettingsModal" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -83,7 +78,6 @@ export default {
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
showSleepTimerModal: false,
|
showSleepTimerModal: false,
|
||||||
showPlayerQueueItemsModal: false,
|
showPlayerQueueItemsModal: false,
|
||||||
showPlayerSettingsModal: false,
|
|
||||||
sleepTimerSet: false,
|
sleepTimerSet: false,
|
||||||
sleepTimerRemaining: 0,
|
sleepTimerRemaining: 0,
|
||||||
sleepTimerType: null,
|
sleepTimerType: null,
|
||||||
|
|
@ -140,9 +134,6 @@ export default {
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.streamLibraryItem?.mediaType === 'podcast'
|
return this.streamLibraryItem?.mediaType === 'podcast'
|
||||||
},
|
},
|
||||||
isMusic() {
|
|
||||||
return this.streamLibraryItem?.mediaType === 'music'
|
|
||||||
},
|
|
||||||
isExplicit() {
|
isExplicit() {
|
||||||
return !!this.mediaMetadata.explicit
|
return !!this.mediaMetadata.explicit
|
||||||
},
|
},
|
||||||
|
|
@ -172,11 +163,7 @@ export default {
|
||||||
},
|
},
|
||||||
podcastAuthor() {
|
podcastAuthor() {
|
||||||
if (!this.isPodcast) return null
|
if (!this.isPodcast) return null
|
||||||
return this.mediaMetadata.author || 'Unknown'
|
return this.mediaMetadata.author || this.$strings.LabelUnknown
|
||||||
},
|
|
||||||
musicArtists() {
|
|
||||||
if (!this.isMusic) return null
|
|
||||||
return this.mediaMetadata.artists.join(', ')
|
|
||||||
},
|
},
|
||||||
hasNextItemInQueue() {
|
hasNextItemInQueue() {
|
||||||
return this.currentPlayerQueueIndex < this.playerQueueItems.length - 1
|
return this.currentPlayerQueueIndex < this.playerQueueItems.length - 1
|
||||||
|
|
@ -260,7 +247,7 @@ export default {
|
||||||
sleepTimerEnd() {
|
sleepTimerEnd() {
|
||||||
this.clearSleepTimer()
|
this.clearSleepTimer()
|
||||||
this.playerHandler.pause()
|
this.playerHandler.pause()
|
||||||
this.$toast.info('Sleep Timer Done.. zZzzZz')
|
this.$toast.info(this.$strings.ToastSleepTimerDone)
|
||||||
},
|
},
|
||||||
cancelSleepTimer() {
|
cancelSleepTimer() {
|
||||||
this.showSleepTimerModal = false
|
this.showSleepTimerModal = false
|
||||||
|
|
@ -534,7 +521,7 @@ export default {
|
||||||
},
|
},
|
||||||
showFailedProgressSyncs() {
|
showFailedProgressSyncs() {
|
||||||
if (!isNaN(this.syncFailedToast)) this.$toast.dismiss(this.syncFailedToast)
|
if (!isNaN(this.syncFailedToast)) this.$toast.dismiss(this.syncFailedToast)
|
||||||
this.syncFailedToast = this.$toast('Progress is not being synced. Restart playback', { timeout: false, type: 'error' })
|
this.syncFailedToast = this.$toast(this.$strings.ToastProgressIsNotBeingSynced, { timeout: false, type: 'error' })
|
||||||
},
|
},
|
||||||
sessionClosedEvent(sessionId) {
|
sessionClosedEvent(sessionId) {
|
||||||
if (this.playerHandler.currentSessionId === sessionId) {
|
if (this.playerHandler.currentSessionId === sessionId) {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-20 bg-bg h-full fixed left-0 box-shadow-side z-50" style="min-width: 80px" :style="{ top: offsetTop + 'px' }">
|
<div role="toolbar" aria-orientation="vertical" aria-label="Library Sidebar" class="w-20 bg-bg h-full fixed left-0 box-shadow-side z-50" style="min-width: 80px" :style="{ top: offsetTop + 'px' }">
|
||||||
<!-- ugly little workaround to cover up the shadow overlapping the bookshelf toolbar -->
|
<!-- ugly little workaround to cover up the shadow overlapping the bookshelf toolbar -->
|
||||||
<div v-if="isShowingBookshelfToolbar" class="absolute top-0 -right-4 w-4 bg-bg h-10 pointer-events-none" />
|
<div v-if="isShowingBookshelfToolbar" class="absolute top-0 -right-4 w-4 bg-bg h-10 pointer-events-none" />
|
||||||
|
|
||||||
<div id="siderail-buttons-container" :class="{ 'player-open': streamLibraryItem }" class="w-full overflow-y-auto overflow-x-hidden">
|
<div id="siderail-buttons-container" role="navigation" aria-label="Library Navigation" :class="{ 'player-open': streamLibraryItem }" class="w-full overflow-y-auto overflow-x-hidden">
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="homePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link :to="`/library/${currentLibraryId}`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="homePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||||
|
|
@ -58,7 +58,7 @@
|
||||||
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<svg class="w-6 h-6" viewBox="0 0 24 24">
|
<svg class="w-6 h-6" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
|
|
@ -95,14 +95,6 @@
|
||||||
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="isMusicLibrary" :to="`/library/${currentLibraryId}/bookshelf/albums`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isMusicAlbumsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
|
||||||
<span class="material-symbols text-xl">album</span>
|
|
||||||
|
|
||||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">Albums</p>
|
|
||||||
|
|
||||||
<div v-show="isMusicAlbumsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
|
||||||
</nuxt-link>
|
|
||||||
|
|
||||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-symbols text-2xl"></span>
|
<span class="material-symbols text-2xl"></span>
|
||||||
|
|
||||||
|
|
@ -172,9 +164,6 @@ export default {
|
||||||
isPodcastLibrary() {
|
isPodcastLibrary() {
|
||||||
return this.currentLibraryMediaType === 'podcast'
|
return this.currentLibraryMediaType === 'podcast'
|
||||||
},
|
},
|
||||||
isMusicLibrary() {
|
|
||||||
return this.currentLibraryMediaType === 'music'
|
|
||||||
},
|
|
||||||
isPodcastDownloadQueuePage() {
|
isPodcastDownloadQueuePage() {
|
||||||
return this.$route.name === 'library-library-podcast-download-queue'
|
return this.$route.name === 'library-library-podcast-download-queue'
|
||||||
},
|
},
|
||||||
|
|
@ -184,9 +173,6 @@ export default {
|
||||||
isPodcastLatestPage() {
|
isPodcastLatestPage() {
|
||||||
return this.$route.name === 'library-library-podcast-latest'
|
return this.$route.name === 'library-library-podcast-latest'
|
||||||
},
|
},
|
||||||
isMusicAlbumsPage() {
|
|
||||||
return this.paramId === 'albums'
|
|
||||||
},
|
|
||||||
homePage() {
|
homePage() {
|
||||||
return this.$route.name === 'library-library'
|
return this.$route.name === 'library-library'
|
||||||
},
|
},
|
||||||
|
|
@ -194,7 +180,7 @@ export default {
|
||||||
return this.$route.name === 'library-library-series-id' || this.paramId === 'series'
|
return this.$route.name === 'library-library-series-id' || this.paramId === 'series'
|
||||||
},
|
},
|
||||||
isAuthorsPage() {
|
isAuthorsPage() {
|
||||||
return this.$route.name === 'library-library-authors'
|
return this.libraryBookshelfPage && this.paramId === 'authors'
|
||||||
},
|
},
|
||||||
isNarratorsPage() {
|
isNarratorsPage() {
|
||||||
return this.$route.name === 'library-library-narrators'
|
return this.$route.name === 'library-library-narrators'
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div :style="{ minWidth: cardWidth + 'px', maxWidth: cardWidth + 'px' }">
|
<article class="pb-3e" :style="{ minWidth: cardWidth + 'px', maxWidth: cardWidth + 'px' }">
|
||||||
<nuxt-link :to="`/author/${author.id}`">
|
<nuxt-link :to="`/author/${author?.id}`">
|
||||||
<div cy-id="card" @mouseover="mouseover" @mouseleave="mouseleave">
|
<div cy-id="card" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
<div cy-id="imageArea" :style="{ height: cardHeight + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
<div cy-id="imageArea" :style="{ height: cardHeight + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
||||||
<!-- Image or placeholder -->
|
<!-- Image or placeholder -->
|
||||||
|
|
@ -34,13 +34,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
author: {
|
authorMount: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
|
|
@ -57,7 +57,8 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
searching: false,
|
searching: false,
|
||||||
isHovering: false
|
isHovering: false,
|
||||||
|
author: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -67,35 +68,41 @@ export default {
|
||||||
cardHeight() {
|
cardHeight() {
|
||||||
return this.height * this.sizeMultiplier
|
return this.height * this.sizeMultiplier
|
||||||
},
|
},
|
||||||
|
coverHeight() {
|
||||||
|
return this.cardHeight
|
||||||
|
},
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.store.getters['user/getToken']
|
||||||
},
|
},
|
||||||
_author() {
|
_author() {
|
||||||
return this.author || {}
|
return this.author || {}
|
||||||
},
|
},
|
||||||
authorId() {
|
authorId() {
|
||||||
return this._author.id
|
return this._author?.id || ''
|
||||||
},
|
},
|
||||||
name() {
|
name() {
|
||||||
return this._author.name || ''
|
return this._author?.name || ''
|
||||||
},
|
},
|
||||||
asin() {
|
asin() {
|
||||||
return this._author.asin || ''
|
return this._author?.asin || ''
|
||||||
},
|
},
|
||||||
numBooks() {
|
numBooks() {
|
||||||
return this._author.numBooks || 0
|
return this._author?.numBooks || 0
|
||||||
|
},
|
||||||
|
store() {
|
||||||
|
return this.$store || this.$nuxt.$store
|
||||||
},
|
},
|
||||||
userCanUpdate() {
|
userCanUpdate() {
|
||||||
return this.$store.getters['user/getUserCanUpdate']
|
return this.store.getters['user/getUserCanUpdate']
|
||||||
},
|
},
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
libraryProvider() {
|
libraryProvider() {
|
||||||
return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
|
return this.store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
return this.$store.getters['user/getSizeMultiplier']
|
return this.store.getters['user/getSizeMultiplier']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -121,24 +128,54 @@ export default {
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
if (!response) {
|
if (!response) {
|
||||||
this.$toast.error(`Author ${this.name} not found`)
|
this.$toast.error(this.$getString('ToastAuthorNotFound', [this.name]))
|
||||||
} else if (response.updated) {
|
} else if (response.updated) {
|
||||||
if (response.author.imagePath) this.$toast.success(`Author ${response.author.name} was updated`)
|
if (response.author.imagePath) {
|
||||||
else this.$toast.success(`Author ${response.author.name} was updated (no image found)`)
|
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||||
|
} else {
|
||||||
|
this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info(`No updates were made for Author ${response.author.name}`)
|
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
}
|
}
|
||||||
this.searching = false
|
this.searching = false
|
||||||
},
|
},
|
||||||
setSearching(isSearching) {
|
setSearching(isSearching) {
|
||||||
this.searching = isSearching
|
this.searching = isSearching
|
||||||
}
|
},
|
||||||
|
setEntity(author) {
|
||||||
|
this.removeListeners()
|
||||||
|
this.author = author
|
||||||
|
this.addListeners()
|
||||||
|
},
|
||||||
|
addListeners() {
|
||||||
|
if (this.author) {
|
||||||
|
this.$eventBus.$on(`searching-author-${this.authorId}`, this.setSearching)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeListeners() {
|
||||||
|
if (this.author) {
|
||||||
|
this.$eventBus.$off(`searching-author-${this.authorId}`, this.setSearching)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
// destroy the vue listeners, etc
|
||||||
|
this.$destroy()
|
||||||
|
|
||||||
|
// remove the element from the DOM
|
||||||
|
if (this.$el && this.$el.parentNode) {
|
||||||
|
this.$el.parentNode.removeChild(this.$el)
|
||||||
|
} else if (this.$el && this.$el.remove) {
|
||||||
|
this.$el.remove()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setSelectionMode(val) {}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$eventBus.$on(`searching-author-${this.authorId}`, this.setSearching)
|
if (this.authorMount) this.setEntity(this.authorMount)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$eventBus.$off(`searching-author-${this.authorId}`, this.setSearching)
|
this.removeListeners()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
<p class="truncate text-sm">{{ title }}</p>
|
<p class="truncate text-sm">{{ title }}</p>
|
||||||
|
|
||||||
<p class="truncate text-xs text-gray-300">{{ description }}</p>
|
<p class="truncate text-xs text-gray-300">{{ description }}</p>
|
||||||
|
<p v-if="specialMessage" class="truncate text-xs text-gray-300">{{ specialMessage }}</p>
|
||||||
|
|
||||||
<p v-if="isFailed && failedMessage" class="text-xs truncate text-red-500">{{ failedMessage }}</p>
|
<p v-if="isFailed && failedMessage" class="text-xs truncate text-red-500">{{ failedMessage }}</p>
|
||||||
<p v-else-if="!isFinished && cancelingScan" class="text-xs truncate">Canceling...</p>
|
<p v-else-if="!isFinished && cancelingScan" class="text-xs truncate">Canceling...</p>
|
||||||
|
|
@ -26,7 +27,16 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
cancelingScan: false
|
cancelingScan: false,
|
||||||
|
specialMessage: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
task: {
|
||||||
|
immediate: true,
|
||||||
|
handler() {
|
||||||
|
this.initTask()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -34,14 +44,17 @@ export default {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
|
if (this.task.titleKey && this.$strings[this.task.titleKey]) {
|
||||||
|
return this.$getString(this.task.titleKey, this.task.titleSubs)
|
||||||
|
}
|
||||||
return this.task.title || 'No Title'
|
return this.task.title || 'No Title'
|
||||||
},
|
},
|
||||||
description() {
|
description() {
|
||||||
|
if (this.task.descriptionKey && this.$strings[this.task.descriptionKey]) {
|
||||||
|
return this.$getString(this.task.descriptionKey, this.task.descriptionSubs)
|
||||||
|
}
|
||||||
return this.task.description || ''
|
return this.task.description || ''
|
||||||
},
|
},
|
||||||
details() {
|
|
||||||
return this.task.details || 'Unknown'
|
|
||||||
},
|
|
||||||
isFinished() {
|
isFinished() {
|
||||||
return !!this.task.isFinished
|
return !!this.task.isFinished
|
||||||
},
|
},
|
||||||
|
|
@ -52,6 +65,9 @@ export default {
|
||||||
return this.isFinished && !this.isFailed
|
return this.isFinished && !this.isFailed
|
||||||
},
|
},
|
||||||
failedMessage() {
|
failedMessage() {
|
||||||
|
if (this.task.errorKey && this.$strings[this.task.errorKey]) {
|
||||||
|
return this.$getString(this.task.errorKey, this.task.errorSubs)
|
||||||
|
}
|
||||||
return this.task.error || ''
|
return this.task.error || ''
|
||||||
},
|
},
|
||||||
action() {
|
action() {
|
||||||
|
|
@ -87,6 +103,21 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
initTask() {
|
||||||
|
// special message for library scan tasks
|
||||||
|
if (this.task?.data?.scanResults) {
|
||||||
|
const scanResults = this.task.data.scanResults
|
||||||
|
const strs = []
|
||||||
|
if (scanResults.added) strs.push(this.$getString('MessageTaskScanItemsAdded', [scanResults.added]))
|
||||||
|
if (scanResults.updated) strs.push(this.$getString('MessageTaskScanItemsUpdated', [scanResults.updated]))
|
||||||
|
if (scanResults.missing) strs.push(this.$getString('MessageTaskScanItemsMissing', [scanResults.missing]))
|
||||||
|
const changesDetected = strs.length > 0 ? strs.join(', ') : this.$strings.MessageTaskScanNoChangesNeeded
|
||||||
|
const timeElapsed = scanResults.elapsed ? ` (${this.$elapsedPretty(scanResults.elapsed / 1000, false, true)})` : ''
|
||||||
|
this.specialMessage = `${changesDetected}${timeElapsed}`
|
||||||
|
} else {
|
||||||
|
this.specialMessage = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
cancelScan() {
|
cancelScan() {
|
||||||
const libraryId = this.task?.data?.libraryId
|
const libraryId = this.task?.data?.libraryId
|
||||||
if (!libraryId) {
|
if (!libraryId) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="card" :id="`book-card-${index}`" :style="{ minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }" class="absolute rounded-sm z-10 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
<article ref="card" :id="`book-card-${index}`" tabindex="0" :aria-label="displayTitle" :style="{ minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }" class="absolute rounded-sm z-10 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
<div :id="`cover-area-${index}`" class="relative w-full top-0 left-0 rounded overflow-hidden z-10 bg-primary box-shadow-book" :style="{ height: coverHeight + 'px ' }">
|
<div :id="`cover-area-${index}`" class="relative w-full top-0 left-0 rounded overflow-hidden z-10 bg-primary box-shadow-book" :style="{ height: coverHeight + 'px ' }">
|
||||||
<!-- When cover image does not fill -->
|
<!-- When cover image does not fill -->
|
||||||
<div cy-id="coverBg" v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
<div cy-id="coverBg" v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
||||||
|
|
@ -14,21 +14,21 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
||||||
<div cy-id="titleImageNotReady" v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: 0.5 + 'em' }">
|
<div cy-id="titleImageNotReady" v-show="libraryItem && !imageReady" aria-hidden="true" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: 0.5 + 'em' }">
|
||||||
<p :style="{ fontSize: 0.8 + 'em' }" class="text-gray-300 text-center">{{ title }}</p>
|
<p :style="{ fontSize: 0.8 + 'em' }" class="text-gray-300 text-center">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cover Image -->
|
<!-- Cover Image -->
|
||||||
<img cy-id="coverImage" v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="relative w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
<img cy-id="coverImage" v-if="libraryItem" :alt="`${displayTitle}, ${$strings.LabelCover}`" ref="cover" aria-hidden="true" :src="bookCoverSrc" class="relative w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
||||||
|
|
||||||
<!-- Placeholder Cover Title & Author -->
|
<!-- Placeholder Cover Title & Author -->
|
||||||
<div cy-id="placeholderTitle" v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'em' }">
|
<div cy-id="placeholderTitle" v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'em' }">
|
||||||
<div>
|
<div>
|
||||||
<p cy-id="placeholderTitleText" class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'em' }">{{ titleCleaned }}</p>
|
<p cy-id="placeholderTitleText" aria-hidden="true" class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'em' }">{{ titleCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div cy-id="placeholderAuthor" v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'em', bottom: authorBottom + 'em' }">
|
<div cy-id="placeholderAuthor" v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'em', bottom: authorBottom + 'em' }">
|
||||||
<p cy-id="placeholderAuthorText" class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'em' }">{{ authorCleaned }}</p>
|
<p cy-id="placeholderAuthorText" aria-hidden="true" class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'em' }">{{ authorCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }" style="background-color: #78350f">
|
<div v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }" style="background-color: #78350f">
|
||||||
|
|
@ -93,11 +93,11 @@
|
||||||
|
|
||||||
<!-- rss feed icon -->
|
<!-- rss feed icon -->
|
||||||
<div cy-id="rssFeed" v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" :style="{ padding: 0.375 + 'em' }">
|
<div cy-id="rssFeed" v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" :style="{ padding: 0.375 + 'em' }">
|
||||||
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">rss_feed</span>
|
<span class="material-symbols" aria-hidden="true" :style="{ fontSize: 1.5 + 'em' }">rss_feed</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- media item shared icon -->
|
<!-- media item shared icon -->
|
||||||
<div cy-id="mediaItemShare" v-if="mediaItemShare && !isSelectionMode && !isHovering" class="absolute text-success left-0 z-10" :style="{ padding: 0.375 + 'em', top: rssFeed ? '2em' : '0px' }">
|
<div cy-id="mediaItemShare" v-if="mediaItemShare && !isSelectionMode && !isHovering" class="absolute text-success left-0 z-10" :style="{ padding: 0.375 + 'em', top: rssFeed ? '2em' : '0px' }">
|
||||||
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">public</span>
|
<span class="material-symbols" aria-hidden="true" :style="{ fontSize: 1.5 + 'em' }">public</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Series sequence -->
|
<!-- Series sequence -->
|
||||||
|
|
@ -114,7 +114,7 @@
|
||||||
|
|
||||||
<!-- Podcast Num Episodes -->
|
<!-- Podcast Num Episodes -->
|
||||||
<div cy-id="numEpisodes" v-else-if="!numEpisodesIncomplete && numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', width: 1.25 + 'em', height: 1.25 + 'em' }">
|
<div cy-id="numEpisodes" v-else-if="!numEpisodesIncomplete && numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', width: 1.25 + 'em', height: 1.25 + 'em' }">
|
||||||
<p :style="{ fontSize: 0.8 + 'em' }">{{ numEpisodes }}</p>
|
<p :style="{ fontSize: 0.8 + 'em' }" role="status" :aria-label="$strings.LabelNumberOfEpisodes">{{ numEpisodes }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Podcast Num Episodes -->
|
<!-- Podcast Num Episodes -->
|
||||||
|
|
@ -128,7 +128,7 @@
|
||||||
<div cy-id="detailBottom" :id="`description-area-${index}`" v-if="isAlternativeBookshelfView || isAuthorBookshelfView" dir="auto" class="relative mt-2e mb-2e left-0 z-50 w-full">
|
<div cy-id="detailBottom" :id="`description-area-${index}`" v-if="isAlternativeBookshelfView || isAuthorBookshelfView" dir="auto" class="relative mt-2e mb-2e left-0 z-50 w-full">
|
||||||
<div :style="{ fontSize: 0.9 + 'em' }">
|
<div :style="{ fontSize: 0.9 + 'em' }">
|
||||||
<ui-tooltip v-if="displayTitle" :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
|
<ui-tooltip v-if="displayTitle" :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
|
||||||
<p cy-id="title" ref="displayTitle" class="truncate">{{ displayTitle }}</p>
|
<p cy-id="title" ref="displayTitle" aria-hidden="true" class="truncate">{{ displayTitle }}</p>
|
||||||
<widgets-explicit-indicator cy-id="explicitIndicator" v-if="isExplicit" />
|
<widgets-explicit-indicator cy-id="explicitIndicator" v-if="isExplicit" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -138,7 +138,7 @@
|
||||||
<p cy-id="line2" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displayLineTwo || ' ' }}</p>
|
<p cy-id="line2" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displayLineTwo || ' ' }}</p>
|
||||||
<p cy-id="line3" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displaySortLine }}</p>
|
<p cy-id="line3" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displaySortLine }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -226,9 +226,6 @@ export default {
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.mediaType === 'podcast' || this.store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast'
|
return this.mediaType === 'podcast' || this.store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast'
|
||||||
},
|
},
|
||||||
isMusic() {
|
|
||||||
return this.mediaType === 'music'
|
|
||||||
},
|
|
||||||
isExplicit() {
|
isExplicit() {
|
||||||
return this.mediaMetadata.explicit || false
|
return this.mediaMetadata.explicit || false
|
||||||
},
|
},
|
||||||
|
|
@ -328,7 +325,7 @@ export default {
|
||||||
},
|
},
|
||||||
displaySubtitle() {
|
displaySubtitle() {
|
||||||
if (!this.libraryItem) return '\u00A0'
|
if (!this.libraryItem) return '\u00A0'
|
||||||
if (this.collapsedSeries) return this.collapsedSeries.numBooks === 1 ? '1 book' : `${this.collapsedSeries.numBooks} books`
|
if (this.collapsedSeries) return `${this.collapsedSeries.numBooks} ${this.$strings.LabelBooks}`
|
||||||
if (this.mediaMetadata.subtitle) return this.mediaMetadata.subtitle
|
if (this.mediaMetadata.subtitle) return this.mediaMetadata.subtitle
|
||||||
if (this.mediaMetadata.seriesName) return this.mediaMetadata.seriesName
|
if (this.mediaMetadata.seriesName) return this.mediaMetadata.seriesName
|
||||||
return ''
|
return ''
|
||||||
|
|
@ -336,7 +333,6 @@ export default {
|
||||||
displayLineTwo() {
|
displayLineTwo() {
|
||||||
if (this.recentEpisode) return this.title
|
if (this.recentEpisode) return this.title
|
||||||
if (this.isPodcast) return this.author
|
if (this.isPodcast) return this.author
|
||||||
if (this.isMusic) return this.artist
|
|
||||||
if (this.collapsedSeries) return ''
|
if (this.collapsedSeries) return ''
|
||||||
if (this.isAuthorBookshelfView) {
|
if (this.isAuthorBookshelfView) {
|
||||||
return this.mediaMetadata.publishedYear || ''
|
return this.mediaMetadata.publishedYear || ''
|
||||||
|
|
@ -346,14 +342,14 @@ export default {
|
||||||
},
|
},
|
||||||
displaySortLine() {
|
displaySortLine() {
|
||||||
if (this.collapsedSeries) return null
|
if (this.collapsedSeries) return null
|
||||||
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)
|
if (this.orderBy === 'mtimeMs') return this.$getString('LabelFileModifiedDate', [this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)])
|
||||||
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)
|
if (this.orderBy === 'birthtimeMs') return this.$getString('LabelFileBornDate', [this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)])
|
||||||
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt, this.dateFormat)
|
if (this.orderBy === 'addedAt') return this.$getString('LabelAddedDate', [this.$formatDate(this._libraryItem.addedAt, this.dateFormat)])
|
||||||
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
if (this.orderBy === 'media.duration') return this.$strings.LabelDuration + ': ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
||||||
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
if (this.orderBy === 'size') return this.$strings.LabelSize + ': ' + this.$bytesPretty(this._libraryItem.size)
|
||||||
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
|
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} ` + this.$strings.LabelEpisodes
|
||||||
if (this.orderBy === 'media.metadata.publishedYear') {
|
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 '\u00A0'
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
|
@ -364,7 +360,6 @@ export default {
|
||||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
|
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
|
||||||
},
|
},
|
||||||
userProgress() {
|
userProgress() {
|
||||||
if (this.isMusic) return null
|
|
||||||
if (this.episodeProgress) return this.episodeProgress
|
if (this.episodeProgress) return this.episodeProgress
|
||||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
|
|
@ -420,7 +415,7 @@ export default {
|
||||||
return !this.isSelectionMode && !this.showPlayButton && this.ebookFormat
|
return !this.isSelectionMode && !this.showPlayButton && this.ebookFormat
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode || this.isMusic)
|
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode)
|
||||||
},
|
},
|
||||||
showSmallEBookIcon() {
|
showSmallEBookIcon() {
|
||||||
return !this.isSelectionMode && this.ebookFormat
|
return !this.isSelectionMode && this.ebookFormat
|
||||||
|
|
@ -464,8 +459,6 @@ export default {
|
||||||
return this.store.getters['user/getIsAdminOrUp']
|
return this.store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
moreMenuItems() {
|
moreMenuItems() {
|
||||||
if (this.isMusic) return []
|
|
||||||
|
|
||||||
if (this.recentEpisode) {
|
if (this.recentEpisode) {
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
|
|
@ -710,7 +703,7 @@ export default {
|
||||||
toggleFinished(confirmed = false) {
|
toggleFinished(confirmed = false) {
|
||||||
if (!this.itemIsFinished && this.userProgressPercent > 0 && !confirmed) {
|
if (!this.itemIsFinished && this.userProgressPercent > 0 && !confirmed) {
|
||||||
const payload = {
|
const payload = {
|
||||||
message: `Are you sure you want to mark "${this.displayTitle}" as finished?`,
|
message: this.$getString('MessageConfirmMarkItemFinished', [this.displayTitle]),
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.toggleFinished(true)
|
this.toggleFinished(true)
|
||||||
|
|
@ -755,18 +748,18 @@ export default {
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
var result = data.result
|
var result = data.result
|
||||||
if (!result) {
|
if (!result) {
|
||||||
this.$toast.error(`Re-Scan Failed for "${this.title}"`)
|
this.$toast.error(this.$getString('ToastRescanFailed', [this.displayTitle]))
|
||||||
} else if (result === 'UPDATED') {
|
} else if (result === 'UPDATED') {
|
||||||
this.$toast.success(`Re-Scan complete item was updated`)
|
this.$toast.success(this.$strings.ToastRescanUpdated)
|
||||||
} else if (result === 'UPTODATE') {
|
} 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') {
|
} else if (result === 'REMOVED') {
|
||||||
this.$toast.error(`Re-Scan complete item was removed`)
|
this.$toast.error(this.$strings.ToastRescanRemoved)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to scan library item', error)
|
console.error('Failed to scan library item', error)
|
||||||
this.$toast.error('Failed to scan library item')
|
this.$toast.error(this.$strings.ToastScanFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|
@ -823,7 +816,7 @@ export default {
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove series from home', error)
|
console.error('Failed to remove series from home', error)
|
||||||
this.$toast.error('Failed to update user')
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|
@ -841,7 +834,7 @@ export default {
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to hide item from home', error)
|
console.error('Failed to hide item from home', error)
|
||||||
this.$toast.error('Failed to update user')
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|
@ -856,7 +849,7 @@ export default {
|
||||||
episodeId: this.recentEpisode.id,
|
episodeId: this.recentEpisode.id,
|
||||||
title: this.recentEpisode.title,
|
title: this.recentEpisode.title,
|
||||||
subtitle: this.mediaMetadata.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,
|
duration: this.recentEpisode.audioFile.duration || null,
|
||||||
coverPath: this.media.coverPath || null
|
coverPath: this.media.coverPath || null
|
||||||
}
|
}
|
||||||
|
|
@ -906,11 +899,11 @@ export default {
|
||||||
axios
|
axios
|
||||||
.$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`)
|
.$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Item deleted')
|
this.$toast.success(this.$strings.ToastItemDeletedSuccess)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to delete item', error)
|
console.error('Failed to delete item', error)
|
||||||
this.$toast.error('Failed to delete item')
|
this.$toast.error(this.$strings.ToastItemDeletedFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|
@ -1016,7 +1009,7 @@ export default {
|
||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: this.mediaMetadata.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,
|
duration: episode.audioFile.duration || null,
|
||||||
coverPath: this.media.coverPath || null
|
coverPath: this.media.coverPath || null
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="card" :id="`collection-card-${index}`" :style="{ width: cardWidth + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
<div ref="card" :id="`collection-card-${index}`" role="button" :style="{ width: cardWidth + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
<div class="relative" :style="{ height: coverHeight + 'px' }">
|
<div class="relative" :style="{ height: coverHeight + 'px' }">
|
||||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="card" :id="`playlist-card-${index}`" :style="{ width: cardWidth + 'px', fontSize: sizeMultiplier + 'rem' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
<div ref="card" :id="`playlist-card-${index}`" role="button" :style="{ width: cardWidth + 'px', fontSize: sizeMultiplier + 'rem' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
<div class="relative" :style="{ height: coverHeight + 'px' }">
|
<div class="relative" :style="{ height: coverHeight + 'px' }">
|
||||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div cy-id="card" ref="card" :id="`series-card-${index}`" :style="{ width: cardWidth + 'px' }" class="absolute rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
<article cy-id="card" ref="card" :id="`series-card-${index}`" tabindex="0" :aria-label="displayTitle" :style="{ width: cardWidth + 'px' }" class="absolute rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
<div cy-id="covers-area" class="relative" :style="{ height: coverHeight + 'px' }">
|
<div cy-id="covers-area" class="relative" :style="{ height: coverHeight + 'px' }">
|
||||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
||||||
|
|
@ -7,12 +7,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div cy-id="seriesLengthMarker" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #cd9d49dd">
|
<div cy-id="seriesLengthMarker" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #cd9d49dd">
|
||||||
<p :style="{ fontSize: 0.8 + 'em' }">{{ books.length }}</p>
|
<p :style="{ fontSize: 0.8 + 'em' }" role="status" :aria-label="$strings.LabelNumberOfBooks">{{ books.length }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div cy-id="seriesProgressBar" v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1e shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
|
<div cy-id="seriesProgressBar" v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1e shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
|
||||||
|
|
||||||
<div cy-id="hoveringDisplayTitle" v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: '1em' }">
|
<div cy-id="hoveringDisplayTitle" v-if="hasValidCovers" aria-hidden="true" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: '1em' }">
|
||||||
<p :style="{ fontSize: 1.2 + 'em' }">{{ displayTitle }}</p>
|
<p :style="{ fontSize: 1.2 + 'em' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -21,14 +21,14 @@
|
||||||
|
|
||||||
<div cy-id="standardBottomText" v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6e h-6e rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
|
<div cy-id="standardBottomText" v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6e h-6e rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em 0.5em` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em 0.5em` }">
|
||||||
<p cy-id="standardBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ displayTitle }}</p>
|
<p cy-id="standardBottomDisplayTitle" class="truncate" aria-hidden="true" :style="{ fontSize: labelFontSize + 'em' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div cy-id="detailBottomText" v-else class="relative z-30 left-0 right-0 mx-auto py-1e rounded-md text-center">
|
<div cy-id="detailBottomText" v-else class="relative z-30 left-0 right-0 mx-auto py-1e rounded-md text-center">
|
||||||
<p cy-id="detailBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ displayTitle }}</p>
|
<p cy-id="detailBottomDisplayTitle" class="truncate" aria-hidden="true" :style="{ fontSize: labelFontSize + 'em' }">{{ displayTitle }}</p>
|
||||||
<p cy-id="detailBottomSortLine" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displaySortLine }}</p>
|
<p cy-id="detailBottomSortLine" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displaySortLine }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -96,7 +96,7 @@ export default {
|
||||||
displaySortLine() {
|
displaySortLine() {
|
||||||
switch (this.orderBy) {
|
switch (this.orderBy) {
|
||||||
case 'addedAt':
|
case 'addedAt':
|
||||||
return `${this.$strings.LabelAdded} ${this.$formatDate(this.addedAt, this.dateFormat)}`
|
return this.$getString('LabelAddedDate', [this.$formatDate(this.addedAt, this.dateFormat)])
|
||||||
case 'totalDuration':
|
case 'totalDuration':
|
||||||
return `${this.$strings.LabelDuration} ${this.$elapsedPrettyExtended(this.totalDuration, false)}`
|
return `${this.$strings.LabelDuration} ${this.$elapsedPrettyExtended(this.totalDuration, false)}`
|
||||||
case 'lastBookUpdated':
|
case 'lastBookUpdated':
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@
|
||||||
<p class="text-base md:text-lg font-semibold pr-4">{{ eventName }}</p>
|
<p class="text-base md:text-lg font-semibold pr-4">{{ eventName }}</p>
|
||||||
<div class="flex-grow" />
|
<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" @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">Fire & Fail</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-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-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">Enable</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 :size="7" icon-font-size="1.1rem" icon="edit" class="mr-2" @click="editNotification" />
|
||||||
<ui-icon-btn bg-color="error" :size="7" icon-font-size="1.2rem" icon="delete" @click="deleteNotificationClick" />
|
<ui-icon-btn bg-color="error" :size="7" icon-font-size="1.2rem" icon="delete" @click="deleteNotificationClick" />
|
||||||
|
|
@ -65,12 +65,12 @@ export default {
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/notifications/test?fail=${intentionallyFail ? 1 : 0}`)
|
.$get(`/api/notifications/test?fail=${intentionallyFail ? 1 : 0}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Triggered onTest Event')
|
this.$toast.success(this.$strings.ToastNotificationTestTriggerSuccess)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
const errorMsg = error.response ? error.response.data : null
|
const errorMsg = error.response ? error.response.data : null
|
||||||
this.$toast.error(`Failed: ${errorMsg}` || 'Failed to trigger onTest event')
|
this.$toast.error(`Failed: ${errorMsg}` || this.$strings.ToastNotificationTestTriggerFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.testing = false
|
this.testing = false
|
||||||
|
|
@ -91,7 +91,7 @@ export default {
|
||||||
// End testing functions
|
// End testing functions
|
||||||
sendTestClick() {
|
sendTestClick() {
|
||||||
const payload = {
|
const payload = {
|
||||||
message: `Trigger this notification with test data?`,
|
message: this.$strings.MessageConfirmNotificationTestTrigger,
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.sendTest()
|
this.sendTest()
|
||||||
|
|
@ -106,12 +106,12 @@ export default {
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/notifications/${this.notification.id}/test`)
|
.$get(`/api/notifications/${this.notification.id}/test`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Triggered test notification')
|
this.$toast.success(this.$strings.ToastNotificationTestTriggerSuccess)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
const errorMsg = error.response ? error.response.data : null
|
const errorMsg = error.response ? error.response.data : null
|
||||||
this.$toast.error(`Failed: ${errorMsg}` || 'Failed to trigger test notification')
|
this.$toast.error(`Failed: ${errorMsg}` || this.$strings.ToastNotificationTestTriggerFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.sendingTest = false
|
this.sendingTest = false
|
||||||
|
|
@ -127,11 +127,10 @@ export default {
|
||||||
.$patch(`/api/notifications/${this.notification.id}`, payload)
|
.$patch(`/api/notifications/${this.notification.id}`, payload)
|
||||||
.then((updatedSettings) => {
|
.then((updatedSettings) => {
|
||||||
this.$emit('update', updatedSettings)
|
this.$emit('update', updatedSettings)
|
||||||
this.$toast.success('Notification enabled')
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update notification', error)
|
console.error('Failed to update notification', error)
|
||||||
this.$toast.error('Failed to update notification')
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.enabling = false
|
this.enabling = false
|
||||||
|
|
@ -139,7 +138,7 @@ export default {
|
||||||
},
|
},
|
||||||
deleteNotificationClick() {
|
deleteNotificationClick() {
|
||||||
const payload = {
|
const payload = {
|
||||||
message: `Are you sure you want to delete this notification?`,
|
message: this.$strings.MessageConfirmDeleteNotification,
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.deleteNotification()
|
this.deleteNotification()
|
||||||
|
|
@ -155,11 +154,10 @@ export default {
|
||||||
.$delete(`/api/notifications/${this.notification.id}`)
|
.$delete(`/api/notifications/${this.notification.id}`)
|
||||||
.then((updatedSettings) => {
|
.then((updatedSettings) => {
|
||||||
this.$emit('update', updatedSettings)
|
this.$emit('update', updatedSettings)
|
||||||
this.$toast.success('Deleted notification')
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
this.$toast.error('Failed to delete notification')
|
this.$toast.error(this.$strings.ToastNotificationDeleteFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.deleting = false
|
this.deleting = false
|
||||||
|
|
@ -171,4 +169,4 @@ export default {
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -27,38 +27,6 @@
|
||||||
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=publishers.${$encode(publisher)}`" class="hover:underline">{{ publisher }}</nuxt-link>
|
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=publishers.${$encode(publisher)}`" class="hover:underline">{{ publisher }}</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="musicAlbum" class="flex py-0.5">
|
|
||||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">Album</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ musicAlbum }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="musicAlbumArtist" class="flex py-0.5">
|
|
||||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">Album Artist</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ musicAlbumArtist }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="musicTrackPretty" class="flex py-0.5">
|
|
||||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">Track</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ musicTrackPretty }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="musicDiscPretty" class="flex py-0.5">
|
|
||||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">Disc</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ musicDiscPretty }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="podcastType" class="flex py-0.5">
|
<div v-if="podcastType" class="flex py-0.5">
|
||||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span>
|
||||||
|
|
@ -97,7 +65,7 @@
|
||||||
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=languages.${$encode(language)}`" class="hover:underline">{{ language }}</nuxt-link>
|
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=languages.${$encode(language)}`" class="hover:underline">{{ language }}</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
|
<div v-if="tracks.length || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
|
||||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -134,10 +102,6 @@ export default {
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.libraryItem.mediaType === 'podcast'
|
return this.libraryItem.mediaType === 'podcast'
|
||||||
},
|
},
|
||||||
audioFile() {
|
|
||||||
// Music track
|
|
||||||
return this.media.audioFile
|
|
||||||
},
|
|
||||||
media() {
|
media() {
|
||||||
return this.libraryItem.media || {}
|
return this.libraryItem.media || {}
|
||||||
},
|
},
|
||||||
|
|
@ -168,25 +132,6 @@ export default {
|
||||||
publisher() {
|
publisher() {
|
||||||
return this.mediaMetadata.publisher || ''
|
return this.mediaMetadata.publisher || ''
|
||||||
},
|
},
|
||||||
musicArtists() {
|
|
||||||
return this.mediaMetadata.artists || []
|
|
||||||
},
|
|
||||||
musicAlbum() {
|
|
||||||
return this.mediaMetadata.album || ''
|
|
||||||
},
|
|
||||||
musicAlbumArtist() {
|
|
||||||
return this.mediaMetadata.albumArtist || ''
|
|
||||||
},
|
|
||||||
musicTrackPretty() {
|
|
||||||
if (!this.mediaMetadata.trackNumber) return null
|
|
||||||
if (!this.mediaMetadata.trackTotal) return this.mediaMetadata.trackNumber
|
|
||||||
return `${this.mediaMetadata.trackNumber} / ${this.mediaMetadata.trackTotal}`
|
|
||||||
},
|
|
||||||
musicDiscPretty() {
|
|
||||||
if (!this.mediaMetadata.discNumber) return null
|
|
||||||
if (!this.mediaMetadata.discTotal) return this.mediaMetadata.discNumber
|
|
||||||
return `${this.mediaMetadata.discNumber} / ${this.mediaMetadata.discTotal}`
|
|
||||||
},
|
|
||||||
narrators() {
|
narrators() {
|
||||||
return this.mediaMetadata.narrators || []
|
return this.mediaMetadata.narrators || []
|
||||||
},
|
},
|
||||||
|
|
@ -220,4 +165,4 @@ export default {
|
||||||
methods: {},
|
methods: {},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="w-full relative sm:w-80">
|
<div class="w-full relative sm:w-80">
|
||||||
<form @submit.prevent="submitSearch">
|
<form role="search" @submit.prevent="submitSearch">
|
||||||
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
|
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
|
||||||
</form>
|
</form>
|
||||||
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
|
<button :aria-hidden="!search" class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
|
||||||
<span v-if="!search" class="material-symbols" style="font-size: 1.2rem"></span>
|
<span v-if="!search" class="material-symbols" style="font-size: 1.2rem"></span>
|
||||||
<span v-else class="material-symbols" style="font-size: 1.2rem">close</span>
|
<span v-else class="material-symbols" style="font-size: 1.2rem">close</span>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full max-w-64 sm:max-w-80 sm:w-80 bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu">
|
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full max-w-64 sm:max-w-80 sm:w-80 bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu" @mousedown.stop.prevent>
|
||||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<li v-if="isTyping" class="py-2 px-2">
|
<li v-if="isTyping" class="py-2 px-2">
|
||||||
<p>{{ $strings.MessageThinking }}</p>
|
<p>{{ $strings.MessageThinking }}</p>
|
||||||
|
|
@ -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>
|
<p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelAuthors }}</p>
|
||||||
<template v-for="item in authorResults">
|
<template v-for="item in authorResults">
|
||||||
<li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
<li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(item.id)}`">
|
<nuxt-link :to="`/author/${item.id}`">
|
||||||
<cards-author-search-card :author="item" />
|
<cards-author-search-card :author="item" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -157,7 +157,7 @@ export default {
|
||||||
clearTimeout(this.focusTimeout)
|
clearTimeout(this.focusTimeout)
|
||||||
this.focusTimeout = setTimeout(() => {
|
this.focusTimeout = setTimeout(() => {
|
||||||
this.showMenu = false
|
this.showMenu = false
|
||||||
}, 200)
|
}, 100)
|
||||||
},
|
},
|
||||||
async runSearch(value) {
|
async runSearch(value) {
|
||||||
this.lastSearch = value
|
this.lastSearch = value
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,30 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
||||||
<button type="button" class="relative w-full h-full bg-bg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
<div class="relative h-7">
|
||||||
<span class="flex items-center justify-between">
|
<button type="button" class="relative w-full h-full bg-bg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="menu" :aria-expanded="showMenu" @click.prevent="showMenu = !showMenu">
|
||||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
<span class="flex items-center justify-between">
|
||||||
</span>
|
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
<span v-if="selected === 'all'" class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
<span v-if="selected === 'all'" class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<div v-else class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-200" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
|
<button v-else :aria-label="$strings.ButtonClearFilter" class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-200" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
|
||||||
<span class="material-symbols" style="font-size: 1.1rem">close</span>
|
<span class="material-symbols" style="font-size: 1.1rem">close</span>
|
||||||
</div>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
<div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm libraryFilterMenu">
|
<div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm libraryFilterMenu">
|
||||||
<ul v-show="!sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul v-show="!sublist" class="h-full w-full" role="menu">
|
||||||
<template v-for="item in selectItems">
|
<template v-for="item in selectItems">
|
||||||
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="option" @click="clickedOption(item)">
|
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" :aria-haspopup="item.sublist ? '' : 'menu'" @click="clickedOption(item)">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="font-normal ml-3 block truncate text-sm">{{ item.text }}</span>
|
<span class="font-normal ml-3 block truncate text-sm">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
|
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
|
||||||
<span class="material-symbols text-2xl">arrow_right</span>
|
<span class="material-symbols text-2xl" :aria-label="$strings.LabelMore">arrow_right</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- selected checkmark icon -->
|
<!-- selected checkmark icon -->
|
||||||
<div v-if="item.value === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
|
<div v-if="item.value === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
|
||||||
|
|
@ -31,8 +33,8 @@
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
<ul v-show="sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul v-show="sublist" class="h-full w-full" role="menu">
|
||||||
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-white/5" role="option" @click="sublist = null">
|
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-white/5" role="menuitem" @click="sublist = null">
|
||||||
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
|
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
|
||||||
<span class="material-symbols text-2xl">arrow_left</span>
|
<span class="material-symbols text-2xl">arrow_left</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -40,13 +42,13 @@
|
||||||
<span class="font-normal block truncate">{{ $strings.ButtonBack }}</span>
|
<span class="font-normal block truncate">{{ $strings.ButtonBack }}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="option">
|
<li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="menuitem">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<span class="font-normal block truncate py-2">{{ $getString('LabelLibraryFilterSublistEmpty', [selectedSublistText]) }}</span>
|
<span class="font-normal block truncate py-2">{{ $getString('LabelLibraryFilterSublistEmpty', [selectedSublistText]) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<template v-for="item in sublistItems">
|
<template v-for="item in sublistItems">
|
||||||
<li :key="item.value" class="select-none relative px-2 cursor-pointer hover:bg-white/5" :class="`${sublist}.${item.value}` === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="option" @click="clickedSublistOption(item.value)">
|
<li :key="item.value" class="select-none relative px-2 cursor-pointer hover:bg-white/5" :class="`${sublist}.${item.value}` === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" @click="clickedSublistOption(item.value)">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="font-normal truncate py-2 text-xs">{{ item.text }}</span>
|
<span class="font-normal truncate py-2 text-xs">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -98,9 +100,6 @@ export default {
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.libraryMediaType === 'podcast'
|
return this.libraryMediaType === 'podcast'
|
||||||
},
|
},
|
||||||
isMusic() {
|
|
||||||
return this.libraryMediaType === 'music'
|
|
||||||
},
|
|
||||||
seriesItems() {
|
seriesItems() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
@ -192,6 +191,12 @@ export default {
|
||||||
value: 'publishers',
|
value: 'publishers',
|
||||||
sublist: true
|
sublist: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelPublishedDecade,
|
||||||
|
textPlural: this.$strings.LabelPublishedDecades,
|
||||||
|
value: 'publishedDecades',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.LabelLanguage,
|
text: this.$strings.LabelLanguage,
|
||||||
textPlural: this.$strings.LabelLanguages,
|
textPlural: this.$strings.LabelLanguages,
|
||||||
|
|
@ -274,35 +279,9 @@ export default {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
musicItems() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
text: this.$strings.LabelAll,
|
|
||||||
value: 'all'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: this.$strings.LabelGenre,
|
|
||||||
textPlural: this.$strings.LabelGenres,
|
|
||||||
value: 'genres',
|
|
||||||
sublist: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: this.$strings.LabelTag,
|
|
||||||
textPlural: this.$strings.LabelTags,
|
|
||||||
value: 'tags',
|
|
||||||
sublist: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: this.$strings.ButtonIssues,
|
|
||||||
value: 'issues',
|
|
||||||
sublist: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
selectItems() {
|
selectItems() {
|
||||||
if (this.isSeries) return this.seriesItems
|
if (this.isSeries) return this.seriesItems
|
||||||
if (this.isPodcast) return this.podcastItems
|
if (this.isPodcast) return this.podcastItems
|
||||||
if (this.isMusic) return this.musicItems
|
|
||||||
return this.bookItems
|
return this.bookItems
|
||||||
},
|
},
|
||||||
selectedItemSublist() {
|
selectedItemSublist() {
|
||||||
|
|
@ -367,6 +346,9 @@ export default {
|
||||||
publishers() {
|
publishers() {
|
||||||
return this.filterData.publishers || []
|
return this.filterData.publishers || []
|
||||||
},
|
},
|
||||||
|
publishedDecades() {
|
||||||
|
return this.filterData.publishedDecades || []
|
||||||
|
},
|
||||||
progress() {
|
progress() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
@ -433,21 +415,17 @@ export default {
|
||||||
id: 'isbn',
|
id: 'isbn',
|
||||||
name: 'ISBN'
|
name: 'ISBN'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'subtitle',
|
|
||||||
name: this.$strings.LabelSubtitle
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'authors',
|
id: 'authors',
|
||||||
name: this.$strings.LabelAuthor
|
name: this.$strings.LabelAuthor
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'publishedYear',
|
id: 'chapters',
|
||||||
name: this.$strings.LabelPublishYear
|
name: this.$strings.LabelChapters
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'series',
|
id: 'cover',
|
||||||
name: this.$strings.LabelSeries
|
name: this.$strings.LabelCover
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'description',
|
id: 'description',
|
||||||
|
|
@ -458,24 +436,32 @@ export default {
|
||||||
name: this.$strings.LabelGenres
|
name: this.$strings.LabelGenres
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'tags',
|
id: 'language',
|
||||||
name: this.$strings.LabelTags
|
name: this.$strings.LabelLanguage
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'narrators',
|
id: 'narrators',
|
||||||
name: this.$strings.LabelNarrator
|
name: this.$strings.LabelNarrator
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'publishedYear',
|
||||||
|
name: this.$strings.LabelPublishYear
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'publisher',
|
id: 'publisher',
|
||||||
name: this.$strings.LabelPublisher
|
name: this.$strings.LabelPublisher
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'language',
|
id: 'series',
|
||||||
name: this.$strings.LabelLanguage
|
name: this.$strings.LabelSeries
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'cover',
|
id: 'subtitle',
|
||||||
name: this.$strings.LabelCover
|
name: this.$strings.LabelSubtitle
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tags',
|
||||||
|
name: this.$strings.LabelTags
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
||||||
<button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
<button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="menu" :aria-expanded="showMenu" @click.prevent="showMenu = !showMenu">
|
||||||
<span class="flex items-center justify-between">
|
<span class="flex items-center justify-between">
|
||||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||||
<span class="material-symbols text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
<span class="material-symbols text-lg text-yellow-400" :aria-label="descending ? $strings.LabelSortDescending : $strings.LabelSortAscending">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm" role="listbox" aria-labelledby="listbox-label">
|
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm" role="menu">
|
||||||
<template v-for="item in selectItems">
|
<template v-for="item in selectItems">
|
||||||
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="option" @click="clickedOption(item.value)">
|
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" @click="clickedOption(item.value)">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
|
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
||||||
<span class="material-symbols text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
<span class="material-symbols text-xl" :aria-label="descending ? $strings.LabelSortDescending : $strings.LabelSortAscending">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -56,9 +56,6 @@ export default {
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.libraryMediaType === 'podcast'
|
return this.libraryMediaType === 'podcast'
|
||||||
},
|
},
|
||||||
isMusic() {
|
|
||||||
return this.libraryMediaType === 'music'
|
|
||||||
},
|
|
||||||
podcastItems() {
|
podcastItems() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
@ -148,40 +145,10 @@ export default {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
musicItems() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
text: this.$strings.LabelTitle,
|
|
||||||
value: 'media.metadata.title'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: this.$strings.LabelAddedAt,
|
|
||||||
value: 'addedAt'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: this.$strings.LabelSize,
|
|
||||||
value: 'size'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: this.$strings.LabelDuration,
|
|
||||||
value: 'media.duration'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: this.$strings.LabelFileBirthtime,
|
|
||||||
value: 'birthtimeMs'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: this.$strings.LabelFileModified,
|
|
||||||
value: 'mtimeMs'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
selectItems() {
|
selectItems() {
|
||||||
let items = null
|
let items = null
|
||||||
if (this.isPodcast) {
|
if (this.isPodcast) {
|
||||||
items = this.podcastItems
|
items = this.podcastItems
|
||||||
} else if (this.isMusic) {
|
|
||||||
items = this.musicItems
|
|
||||||
} else if (this.$store.getters['user/getUserSetting']('filterBy').startsWith('series.')) {
|
} else if (this.$store.getters['user/getUserSetting']('filterBy').startsWith('series.')) {
|
||||||
items = this.seriesItems
|
items = this.seriesItems
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="relative ml-4 sm:ml-8" v-click-outside="clickOutside">
|
<div ref="wrapper" class="relative ml-4 sm:ml-8" v-click-outside="clickOutside">
|
||||||
<div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="setShowMenu(true)">
|
<div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="setShowMenu(true)">
|
||||||
<span class="font-mono uppercase text-gray-200 text-sm sm:text-base">{{ playbackRate.toFixed(1) }}<span class="text-base">x</span></span>
|
<span class="text-gray-200 text-sm sm:text-base">{{ playbackRate.toFixed(1) }}<span class="text-base">x</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showMenu" class="absolute -top-20 z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }">
|
<div v-show="showMenu" class="absolute -top-[5.5rem] z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }">
|
||||||
<div class="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }">
|
<div class="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }">
|
||||||
<div class="arrow-down" />
|
<div class="arrow-down" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -11,12 +11,12 @@
|
||||||
<template v-for="rate in rates">
|
<template v-for="rate in rates">
|
||||||
<div :key="rate" class="h-full border-black-300 w-11 cursor-pointer border rounded-sm" :class="value === rate ? 'bg-black-100' : 'hover:bg-black hover:bg-opacity-10'" style="min-width: 44px; max-width: 44px" @click="set(rate)">
|
<div :key="rate" class="h-full border-black-300 w-11 cursor-pointer border rounded-sm" :class="value === rate ? 'bg-black-100' : 'hover:bg-black hover:bg-opacity-10'" style="min-width: 44px; max-width: 44px" @click="set(rate)">
|
||||||
<div class="w-full h-full flex justify-center items-center">
|
<div class="w-full h-full flex justify-center items-center">
|
||||||
<p class="text-xs text-center font-mono">{{ rate }}<span class="text-sm">x</span></p>
|
<p class="text-xs text-center">{{ rate }}<span class="text-sm">x</span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full py-1 px-4">
|
<div class="w-full py-1 px-1">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" />
|
<ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" />
|
||||||
<p class="px-2 text-2xl sm:text-3xl">{{ playbackRate }}<span class="text-2xl">x</span></p>
|
<p class="px-2 text-2xl sm:text-3xl">{{ playbackRate }}<span class="text-2xl">x</span></p>
|
||||||
|
|
@ -41,7 +41,7 @@ export default {
|
||||||
currentPlaybackRate: 0,
|
currentPlaybackRate: 0,
|
||||||
MIN_SPEED: 0.5,
|
MIN_SPEED: 0.5,
|
||||||
MAX_SPEED: 10,
|
MAX_SPEED: 10,
|
||||||
menuLeft: -92,
|
menuLeft: -96,
|
||||||
arrowLeft: 0
|
arrowLeft: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -89,9 +89,9 @@ export default {
|
||||||
if (boundingBox.left + 110 > window.innerWidth - 10) {
|
if (boundingBox.left + 110 > window.innerWidth - 10) {
|
||||||
this.menuLeft = window.innerWidth - 230 - boundingBox.left
|
this.menuLeft = window.innerWidth - 230 - boundingBox.left
|
||||||
|
|
||||||
this.arrowLeft = Math.abs(this.menuLeft) - 92
|
this.arrowLeft = Math.abs(this.menuLeft) - 96
|
||||||
} else {
|
} else {
|
||||||
this.menuLeft = -92
|
this.menuLeft = -96
|
||||||
this.arrowLeft = 0
|
this.arrowLeft = 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -109,4 +109,4 @@ export default {
|
||||||
this.currentPlaybackRate = this.playbackRate
|
this.currentPlaybackRate = this.playbackRate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
||||||
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none cursor-pointer" aria-haspopup="menu" :aria-expanded="showMenu" @click.prevent="showMenu = !showMenu">
|
||||||
<span class="flex items-center justify-between">
|
<span class="flex items-center justify-between">
|
||||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||||
<span class="material-symbols text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
<span class="material-symbols text-lg text-yellow-400" :aria-label="descending ? $strings.LabelSortDescending : $strings.LabelSortAscending">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-80 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm" role="listbox" aria-labelledby="listbox-label">
|
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-80 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm" role="menu">
|
||||||
<template v-for="item in items">
|
<template v-for="item in items">
|
||||||
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="option" @click="clickedOption(item.value)">
|
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" @click="clickedOption(item.value)">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
|
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
||||||
<span class="material-symbols text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
<span class="material-symbols text-xl" :aria-label="descending ? $strings.LabelSortDescending : $strings.LabelSortAscending">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@
|
||||||
<span class="material-symbols text-2xl sm:text-3xl">{{ volumeIcon }}</span>
|
<span class="material-symbols text-2xl sm:text-3xl">{{ volumeIcon }}</span>
|
||||||
</button>
|
</button>
|
||||||
<transition name="menux">
|
<transition name="menux">
|
||||||
<div v-show="isOpen" class="volumeMenu h-6 absolute bottom-2 w-28 px-2 bg-bg shadow-sm rounded-lg" style="left: -116px">
|
<div v-show="isOpen" class="volumeMenu h-28 absolute bottom-2 w-6 py-2 bg-bg shadow-sm rounded-lg" style="top: -116px">
|
||||||
<div ref="volumeTrack" class="h-1 w-full bg-gray-500 my-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack">
|
<div ref="volumeTrack" class="w-1 h-full bg-gray-500 mx-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack">
|
||||||
<div class="bg-gray-100 h-full absolute left-0 top-0 pointer-events-none rounded-full" :style="{ width: volume * trackWidth + 'px' }" />
|
<div class="bg-gray-100 w-full absolute left-0 bottom-0 pointer-events-none rounded-full" :style="{ height: volume * trackHeight + 'px' }" />
|
||||||
<div class="w-2.5 h-2.5 bg-white shadow-sm rounded-full absolute pointer-events-none" :class="isDragging ? 'transform scale-125 origin-center' : ''" :style="{ left: cursorLeft + 'px', top: '-3px' }" />
|
<div class="w-2.5 h-2.5 bg-white shadow-sm rounded-full absolute pointer-events-none" :class="isDragging ? 'transform scale-125 origin-center' : ''" :style="{ bottom: cursorBottom + 'px', left: '-3px' }" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
@ -24,10 +24,10 @@ export default {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
isHovering: false,
|
isHovering: false,
|
||||||
posX: 0,
|
posY: 0,
|
||||||
lastValue: 0.5,
|
lastValue: 0.5,
|
||||||
isMute: false,
|
isMute: false,
|
||||||
trackWidth: 112 - 20,
|
trackHeight: 112 - 20,
|
||||||
openTimeout: null
|
openTimeout: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -45,9 +45,9 @@ export default {
|
||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cursorLeft() {
|
cursorBottom() {
|
||||||
var left = this.trackWidth * this.volume
|
var bottom = this.trackHeight * this.volume
|
||||||
return left - 3
|
return bottom - 3
|
||||||
},
|
},
|
||||||
volumeIcon() {
|
volumeIcon() {
|
||||||
if (this.volume <= 0) return 'volume_mute'
|
if (this.volume <= 0) return 'volume_mute'
|
||||||
|
|
@ -89,17 +89,10 @@ export default {
|
||||||
}, 600)
|
}, 600)
|
||||||
},
|
},
|
||||||
mousemove(e) {
|
mousemove(e) {
|
||||||
var diff = this.posX - e.x
|
var diff = this.posY - e.y
|
||||||
this.posX = e.x
|
this.posY = e.y
|
||||||
var volShift = 0
|
var volShift = diff / this.trackHeight
|
||||||
if (diff < 0) {
|
var newVol = this.volume + volShift
|
||||||
// Volume up
|
|
||||||
volShift = diff / this.trackWidth
|
|
||||||
} else {
|
|
||||||
// volume down
|
|
||||||
volShift = diff / this.trackWidth
|
|
||||||
}
|
|
||||||
var newVol = this.volume - volShift
|
|
||||||
newVol = Math.min(Math.max(0, newVol), 1)
|
newVol = Math.min(Math.max(0, newVol), 1)
|
||||||
this.volume = newVol
|
this.volume = newVol
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
@ -113,8 +106,8 @@ export default {
|
||||||
},
|
},
|
||||||
mousedownTrack(e) {
|
mousedownTrack(e) {
|
||||||
this.isDragging = true
|
this.isDragging = true
|
||||||
this.posX = e.x
|
this.posY = e.y
|
||||||
var vol = e.offsetX / this.trackWidth
|
var vol = 1 - e.offsetY / this.trackHeight
|
||||||
vol = Math.min(Math.max(vol, 0), 1)
|
vol = Math.min(Math.max(vol, 0), 1)
|
||||||
this.volume = vol
|
this.volume = vol
|
||||||
document.body.addEventListener('mousemove', this.mousemove)
|
document.body.addEventListener('mousemove', this.mousemove)
|
||||||
|
|
@ -137,7 +130,7 @@ export default {
|
||||||
this.clickVolumeIcon()
|
this.clickVolumeIcon()
|
||||||
},
|
},
|
||||||
clickVolumeTrack(e) {
|
clickVolumeTrack(e) {
|
||||||
var vol = e.offsetX / this.trackWidth
|
var vol = 1 - e.offsetY / this.trackHeight
|
||||||
vol = Math.min(Math.max(vol, 0), 1)
|
vol = Math.min(Math.max(vol, 0), 1)
|
||||||
this.volume = vol
|
this.volume = vol
|
||||||
}
|
}
|
||||||
|
|
@ -147,7 +140,7 @@ export default {
|
||||||
this.isMute = true
|
this.isMute = true
|
||||||
}
|
}
|
||||||
const storageVolume = localStorage.getItem('volume')
|
const storageVolume = localStorage.getItem('volume')
|
||||||
if (storageVolume) {
|
if (storageVolume && !isNaN(storageVolume)) {
|
||||||
this.volume = parseFloat(storageVolume)
|
this.volume = parseFloat(storageVolume)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -157,4 +150,4 @@ export default {
|
||||||
document.body.removeEventListener('mouseup', this.mouseup)
|
document.body.removeEventListener('mouseup', this.mouseup)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -56,24 +56,15 @@ export default {
|
||||||
},
|
},
|
||||||
imgSrc() {
|
imgSrc() {
|
||||||
if (!this.imagePath) return null
|
if (!this.imagePath) return null
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
return `${this.$config.routerBasePath}/api/authors/${this.authorId}/image?ts=${this.updatedAt}`
|
||||||
// Testing
|
|
||||||
return `http://localhost:3333${this.$config.routerBasePath}/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
|
||||||
}
|
|
||||||
return `/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
imageLoaded() {
|
imageLoaded() {
|
||||||
var aspectRatio = 1.25
|
|
||||||
if (this.$refs.wrapper) {
|
|
||||||
aspectRatio = this.$refs.wrapper.clientHeight / this.$refs.wrapper.clientWidth
|
|
||||||
}
|
|
||||||
if (this.$refs.img) {
|
if (this.$refs.img) {
|
||||||
var { naturalWidth, naturalHeight } = this.$refs.img
|
var { naturalWidth, naturalHeight } = this.$refs.img
|
||||||
var imgAr = naturalHeight / naturalWidth
|
var imgAr = naturalHeight / naturalWidth
|
||||||
var arDiff = Math.abs(imgAr - aspectRatio)
|
if (imgAr < 0.5 || imgAr > 2) {
|
||||||
if (arDiff > 0.15) {
|
|
||||||
this.showCoverBg = true
|
this.showCoverBg = true
|
||||||
} else {
|
} else {
|
||||||
this.showCoverBg = false
|
this.showCoverBg = false
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,8 @@ export default {
|
||||||
|
|
||||||
var img = document.createElement('img')
|
var img = document.createElement('img')
|
||||||
img.src = src
|
img.src = src
|
||||||
|
img.alt = `${this.name}, ${this.$strings.LabelCover}`
|
||||||
|
img.ariaHidden = true
|
||||||
img.className = 'absolute top-0 left-0 w-full h-full'
|
img.className = 'absolute top-0 left-0 w-full h-full'
|
||||||
img.style.objectFit = showCoverBg ? 'contain' : 'cover'
|
img.style.objectFit = showCoverBg ? 'contain' : 'cover'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center my-2 max-w-md">
|
||||||
|
<div class="w-1/2">
|
||||||
|
<p id="ereader-permissions-toggle">{{ $strings.LabelPermissionsCreateEreader }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/2">
|
||||||
|
<ui-toggle-switch labeledBy="ereader-permissions-toggle" v-model="newUser.permissions.createEreader" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center my-2 max-w-md">
|
<div class="flex items-center my-2 max-w-md">
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<p id="explicit-content-permissions-toggle">{{ $strings.LabelPermissionsAccessExplicitContent }}</p>
|
<p id="explicit-content-permissions-toggle">{{ $strings.LabelPermissionsAccessExplicitContent }}</p>
|
||||||
|
|
@ -111,7 +120,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex pt-4 px-2">
|
<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>
|
<ui-btn v-if="isEditingRoot" small class="flex items-center" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||||
|
|
@ -212,19 +221,19 @@ export default {
|
||||||
},
|
},
|
||||||
unlinkOpenID() {
|
unlinkOpenID() {
|
||||||
const payload = {
|
const payload = {
|
||||||
message: 'Are you sure you want to unlink this user from OpenID?',
|
message: this.$strings.MessageConfirmUnlinkOpenId,
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.unlinkingFromOpenID = true
|
this.unlinkingFromOpenID = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$patch(`/api/users/${this.account.id}/openid-unlink`)
|
.$patch(`/api/users/${this.account.id}/openid-unlink`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('User unlinked from OpenID')
|
this.$toast.success(this.$strings.ToastUnlinkOpenIdSuccess)
|
||||||
this.show = false
|
this.show = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to unlink user from OpenID', 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(() => {
|
.finally(() => {
|
||||||
this.unlinkingFromOpenID = false
|
this.unlinkingFromOpenID = false
|
||||||
|
|
@ -265,15 +274,15 @@ export default {
|
||||||
},
|
},
|
||||||
submitForm() {
|
submitForm() {
|
||||||
if (!this.newUser.username) {
|
if (!this.newUser.username) {
|
||||||
this.$toast.error('Enter a username')
|
this.$toast.error(this.$strings.ToastNewUserUsernameError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!this.newUser.permissions.accessAllLibraries && !this.newUser.librariesAccessible.length) {
|
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
|
return
|
||||||
}
|
}
|
||||||
if (!this.newUser.permissions.accessAllTags && !this.newUser.itemTagsSelected.length) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,7 +305,7 @@ export default {
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
this.$toast.error(`${this.$strings.ToastAccountUpdateFailed}: ${data.error}`)
|
this.$toast.error(`${this.$strings.ToastFailedToUpdate}: ${data.error}`)
|
||||||
} else {
|
} else {
|
||||||
console.log('Account updated', data.user)
|
console.log('Account updated', data.user)
|
||||||
|
|
||||||
|
|
@ -313,12 +322,12 @@ export default {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
console.error('Failed to update account', error)
|
console.error('Failed to update account', error)
|
||||||
var errMsg = error.response ? error.response.data || '' : ''
|
var errMsg = error.response ? error.response.data || '' : ''
|
||||||
this.$toast.error(errMsg || 'Failed to update account')
|
this.$toast.error(errMsg || this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
submitCreateAccount() {
|
submitCreateAccount() {
|
||||||
if (!this.newUser.password) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -329,9 +338,9 @@ export default {
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
this.$toast.error(`Failed to create account: ${data.error}`)
|
this.$toast.error(this.$strings.ToastNewUserCreatedFailed + ': ' + data.error)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.success('New account created')
|
this.$toast.success(this.$strings.ToastNewUserCreatedSuccess)
|
||||||
this.show = false
|
this.show = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -351,10 +360,11 @@ export default {
|
||||||
update: type === 'admin',
|
update: type === 'admin',
|
||||||
delete: type === 'admin',
|
delete: type === 'admin',
|
||||||
upload: type === 'admin',
|
upload: type === 'admin',
|
||||||
accessExplicitContent: true,
|
accessExplicitContent: type === 'admin',
|
||||||
accessAllLibraries: true,
|
accessAllLibraries: true,
|
||||||
accessAllTags: true,
|
accessAllTags: true,
|
||||||
selectedTagsNotAccessible: false
|
selectedTagsNotAccessible: false,
|
||||||
|
createEreader: type === 'admin'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
|
|
@ -386,8 +396,9 @@ export default {
|
||||||
upload: false,
|
upload: false,
|
||||||
accessAllLibraries: true,
|
accessAllLibraries: true,
|
||||||
accessAllTags: true,
|
accessAllTags: true,
|
||||||
accessExplicitContent: true,
|
accessExplicitContent: false,
|
||||||
selectedTagsNotAccessible: false
|
selectedTagsNotAccessible: false,
|
||||||
|
createEreader: false
|
||||||
},
|
},
|
||||||
librariesAccessible: [],
|
librariesAccessible: [],
|
||||||
itemTagsSelected: []
|
itemTagsSelected: []
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal ref="modal" v-model="show" name="custom-metadata-provider" :width="600" :height="'unset'" :processing="processing">
|
<modals-modal ref="modal" v-model="show" name="custom-metadata-provider" :width="600" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="text-3xl text-white truncate">Add custom metadata provider</p>
|
<p class="text-3xl text-white truncate">{{ $strings.HeaderAddCustomMetadataProvider }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
<ui-text-input-with-label v-model="newUrl" label="URL" />
|
<ui-text-input-with-label v-model="newUrl" label="URL" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full mb-2 p-1">
|
<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>
|
||||||
<div class="flex px-1 pt-4">
|
<div class="flex px-1 pt-4">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
@ -67,7 +67,7 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
submitForm() {
|
submitForm() {
|
||||||
if (!this.newName || !this.newUrl) {
|
if (!this.newName || !this.newUrl) {
|
||||||
this.$toast.error('Must add name and url')
|
this.$toast.error(this.$strings.ToastProviderNameAndUrlRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,13 +81,13 @@ export default {
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.$emit('added', data.provider)
|
this.$emit('added', data.provider)
|
||||||
this.$toast.success('New provider added')
|
this.$toast.success(this.$strings.ToastProviderCreatedSuccess)
|
||||||
this.show = false
|
this.show = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const errorMsg = error.response?.data || 'Unknown error'
|
const errorMsg = error.response?.data || 'Unknown error'
|
||||||
console.error('Failed to add provider', error)
|
console.error('Failed to add provider', error)
|
||||||
this.$toast.error('Failed to add provider: ' + errorMsg)
|
this.$toast.error(this.$strings.ToastProviderCreatedFailed + ': ' + errorMsg)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<p class="text-base text-gray-200 truncate">{{ metadata.filename }}</p>
|
<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-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>
|
||||||
|
|
||||||
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
|
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
|
||||||
|
|
@ -159,7 +159,7 @@ export default {
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to get ffprobe data', error)
|
console.error('Failed to get ffprobe data', error)
|
||||||
this.$toast.error('FFProbe failed')
|
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.probingFile = false
|
this.probingFile = false
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<widgets-cron-expression-builder ref="expressionBuilder" v-model="newCronExpression" @input="expressionUpdated" />
|
<widgets-cron-expression-builder ref="expressionBuilder" v-model="newCronExpression" @input="expressionUpdated" />
|
||||||
|
|
||||||
<div class="flex items-center justify-end">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,7 @@ export default {
|
||||||
options: {
|
options: {
|
||||||
provider: undefined,
|
provider: undefined,
|
||||||
overrideDetails: true,
|
overrideDetails: true,
|
||||||
overrideCover: true,
|
overrideCover: true
|
||||||
overrideDefaults: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -99,8 +98,8 @@ export default {
|
||||||
init() {
|
init() {
|
||||||
// If we don't have a set provider (first open of dialog) or we've switched library, set
|
// If we don't have a set provider (first open of dialog) or we've switched library, set
|
||||||
// the selected provider to the current library default provider
|
// the selected provider to the current library default provider
|
||||||
if (!this.options.provider || this.options.lastUsedLibrary != this.currentLibraryId) {
|
if (!this.options.provider || this.lastUsedLibrary != this.currentLibraryId) {
|
||||||
this.options.lastUsedLibrary = this.currentLibraryId
|
this.lastUsedLibrary = this.currentLibraryId
|
||||||
this.options.provider = this.libraryProvider
|
this.options.provider = this.libraryProvider
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -116,10 +115,10 @@ export default {
|
||||||
libraryItemIds: this.selectedBookIds
|
libraryItemIds: this.selectedBookIds
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.info('Batch quick match of ' + this.selectedBookIds.length + ' books started!')
|
this.$toast.info(this.$getString('ToastBatchQuickMatchStarted', [this.selectedBookIds.length]))
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.$toast.error('Batch quick match failed')
|
this.$toast.error(this.$strings.ToastBatchQuickMatchFailed)
|
||||||
console.error('Failed to batch quick match', error)
|
console.error('Failed to batch quick match', error)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ export default {
|
||||||
this.$toast.success(this.$strings.ToastBookmarkRemoveSuccess)
|
this.$toast.success(this.$strings.ToastBookmarkRemoveSuccess)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.$toast.error(this.$strings.ToastBookmarkRemoveFailed)
|
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||||
console.error(error)
|
console.error(error)
|
||||||
})
|
})
|
||||||
this.show = false
|
this.show = false
|
||||||
|
|
@ -110,7 +110,7 @@ export default {
|
||||||
this.$toast.success(this.$strings.ToastBookmarkUpdateSuccess)
|
this.$toast.success(this.$strings.ToastBookmarkUpdateSuccess)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.$toast.error(this.$strings.ToastBookmarkUpdateFailed)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
console.error(error)
|
console.error(error)
|
||||||
})
|
})
|
||||||
this.show = false
|
this.show = false
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="hidden absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 rounded-lg items-center justify-center" style="z-index: 61" @click="clickClose">
|
<div ref="wrapper" role="dialog" aria-modal="true" class="hidden absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 rounded-lg items-center justify-center" style="z-index: 61" @click="clickClose">
|
||||||
<div class="absolute top-3 right-3 md:top-5 md:right-5 h-8 w-8 md:h-12 md:w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300">
|
<button type="button" class="absolute top-3 right-3 md:top-5 md:right-5 h-8 w-8 md:h-12 md:w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" aria-label="Close modal">
|
||||||
<span class="material-symbols text-2xl md:text-4xl">close</span>
|
<span class="material-symbols text-2xl md:text-4xl">close</span>
|
||||||
</div>
|
</button>
|
||||||
<div ref="content" class="text-white">
|
<div ref="content" class="text-white">
|
||||||
<form v-if="selectedSeries" @submit.prevent="submitSeriesForm">
|
<form v-if="selectedSeries" @submit.prevent="submitSeriesForm">
|
||||||
<div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop>
|
<div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop>
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ui-btn v-if="!isOpenSession && !isMediaItemShareSession" small color="error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
|
|
@ -206,14 +206,13 @@ export default {
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/session/${this._session.id}/close`)
|
.$post(`/api/session/${this._session.id}/close`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Session closed')
|
|
||||||
this.show = false
|
this.show = false
|
||||||
this.$emit('closedSession')
|
this.$emit('closedSession')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to close session', error)
|
console.error('Failed to close session', error)
|
||||||
const errMsg = error.response?.data || ''
|
const errMsg = error.response?.data || ''
|
||||||
this.$toast.error(errMsg || 'Failed to close open session')
|
this.$toast.error(errMsg || this.$strings.ToastSessionCloseFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="modal modal-bg w-full h-full fixed top-0 left-0 bg-primary items-center justify-center opacity-0 hidden" :class="`z-${zIndex} bg-opacity-${bgOpacity}`">
|
<div ref="wrapper" role="dialog" aria-modal="true" class="modal modal-bg w-full h-full fixed top-0 left-0 bg-primary items-center justify-center opacity-0 hidden" :class="`z-${zIndex} bg-opacity-${bgOpacity}`">
|
||||||
<div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" />
|
<div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" />
|
||||||
|
|
||||||
<button class="absolute top-4 right-4 landscape:top-4 landscape:right-4 md:portrait:top-5 md:portrait:right-5 lg:top-5 lg:right-5 inline-flex text-gray-200 hover:text-white" aria-label="Close modal" @click="clickClose">
|
<button class="absolute top-4 right-4 landscape:top-4 landscape:right-4 md:portrait:top-5 md:portrait:right-5 lg:top-5 lg:right-5 inline-flex text-gray-200 hover:text-white" aria-label="Close modal" @click="clickClose">
|
||||||
<span class="material-symbols text-2xl landscape:text-2xl md:portrait:text-4xl lg:text-4xl">close</span>
|
<span class="material-symbols text-2xl landscape:text-2xl md:portrait:text-4xl lg:text-4xl">close</span>
|
||||||
</button>
|
</button>
|
||||||
<slot name="outer" />
|
<slot name="outer" />
|
||||||
<div ref="content" style="min-width: 380px; min-height: 200px; max-width: 100vw" class="relative text-white" aria-modal="true" :style="{ height: modalHeight, width: modalWidth, marginTop: contentMarginTop + 'px' }" @mousedown="mousedownModal" @mouseup="mouseupModal" v-click-outside="clickBg">
|
<div ref="content" tabindex="0" style="min-width: 380px; min-height: 200px; max-width: 100vw" class="relative text-white outline-none" :style="{ height: modalHeight, width: modalWidth, marginTop: contentMarginTop + 'px' }" @mousedown="mousedownModal" @mouseup="mouseupModal" v-click-outside="clickBg">
|
||||||
<slot />
|
<slot />
|
||||||
<div v-if="processing" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-black bg-opacity-60 rounded-lg flex items-center justify-center">
|
<div v-if="processing" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-black bg-opacity-60 rounded-lg flex items-center justify-center">
|
||||||
<ui-loading-indicator />
|
<ui-loading-indicator />
|
||||||
|
|
@ -126,6 +126,9 @@ export default {
|
||||||
|
|
||||||
this.$eventBus.$on('modal-hotkey', this.hotkey)
|
this.$eventBus.$on('modal-hotkey', this.hotkey)
|
||||||
this.$store.commit('setOpenModal', this.name)
|
this.$store.commit('setOpenModal', this.name)
|
||||||
|
|
||||||
|
// Set focus to the modal content
|
||||||
|
this.content.focus()
|
||||||
},
|
},
|
||||||
setHide() {
|
setHide() {
|
||||||
if (this.content) this.content.style.transform = 'scale(0)'
|
if (this.content) this.content.style.transform = 'scale(0)'
|
||||||
|
|
|
||||||
|
|
@ -59,12 +59,19 @@ export default {
|
||||||
setJumpBackwardAmount(val) {
|
setJumpBackwardAmount(val) {
|
||||||
this.jumpBackwardAmount = val
|
this.jumpBackwardAmount = val
|
||||||
this.$store.dispatch('user/updateUserSettings', { jumpBackwardAmount: val })
|
this.$store.dispatch('user/updateUserSettings', { jumpBackwardAmount: val })
|
||||||
|
},
|
||||||
|
settingsUpdated() {
|
||||||
|
this.useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack')
|
||||||
|
this.jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount')
|
||||||
|
this.jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack')
|
this.settingsUpdated()
|
||||||
this.jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount')
|
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||||
this.jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount')
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -112,11 +112,11 @@ export default {
|
||||||
return this.$store.state.user.user
|
return this.$store.state.user.user
|
||||||
},
|
},
|
||||||
demoShareUrl() {
|
demoShareUrl() {
|
||||||
return `${window.origin}/share/${this.newShareSlug}`
|
return `${window.origin}${this.$config.routerBasePath}/share/${this.newShareSlug}`
|
||||||
},
|
},
|
||||||
currentShareUrl() {
|
currentShareUrl() {
|
||||||
if (!this.currentShare) return ''
|
if (!this.currentShare) return ''
|
||||||
return `${window.origin}/share/${this.currentShare.slug}`
|
return `${window.origin}${this.$config.routerBasePath}/share/${this.currentShare.slug}`
|
||||||
},
|
},
|
||||||
currentShareTimeRemaining() {
|
currentShareTimeRemaining() {
|
||||||
if (!this.currentShare) return 'Error'
|
if (!this.currentShare) return 'Error'
|
||||||
|
|
@ -165,7 +165,7 @@ export default {
|
||||||
},
|
},
|
||||||
openShare() {
|
openShare() {
|
||||||
if (!this.newShareSlug) {
|
if (!this.newShareSlug) {
|
||||||
this.$toast.error('Slug is required')
|
this.$toast.error(this.$strings.ToastSlugRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
</template>
|
</template>
|
||||||
<form class="flex items-center justify-center px-6 py-3" @submit.prevent="submitCustomTime">
|
<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-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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="timerSet" class="w-full p-4">
|
<div v-if="timerSet" class="w-full p-4">
|
||||||
|
|
|
||||||
|
|
@ -78,14 +78,13 @@ export default {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
this.$toast.error(data.error)
|
this.$toast.error(data.error)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.success('Cover Uploaded')
|
|
||||||
this.resetCoverPreview()
|
this.resetCoverPreview()
|
||||||
}
|
}
|
||||||
this.processingUpload = false
|
this.processingUpload = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', 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.$toast.error(errorMsg)
|
||||||
this.processingUpload = false
|
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) => {
|
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)
|
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)
|
this.$toast.error(errorMsg)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
@ -104,4 +103,4 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -116,12 +116,12 @@ export default {
|
||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/authors/${this.authorId}`)
|
.$delete(`/api/authors/${this.authorId}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Author removed')
|
this.$toast.success(this.$strings.ToastAuthorRemoveSuccess)
|
||||||
this.show = false
|
this.show = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove author', error)
|
console.error('Failed to remove author', error)
|
||||||
this.$toast.error('Failed to remove author')
|
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|
@ -141,14 +141,14 @@ export default {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (!Object.keys(updatePayload).length) {
|
if (!Object.keys(updatePayload).length) {
|
||||||
this.$toast.info(this.$strings.MessageNoUpdateNecessary)
|
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.processing = true
|
this.processing = true
|
||||||
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
|
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
const errorMsg = error.response ? error.response.data : null
|
const errorMsg = error.response ? error.response.data : null
|
||||||
this.$toast.error(errorMsg || this.$strings.ToastAuthorUpdateFailed)
|
this.$toast.error(errorMsg || this.$strings.ToastFailedToUpdate)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
if (result) {
|
if (result) {
|
||||||
|
|
@ -158,7 +158,7 @@ export default {
|
||||||
} else if (result.merged) {
|
} else if (result.merged) {
|
||||||
this.$toast.success(this.$strings.ToastAuthorUpdateMerged)
|
this.$toast.success(this.$strings.ToastAuthorUpdateMerged)
|
||||||
this.show = false
|
this.show = false
|
||||||
} else this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
} else this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
}
|
}
|
||||||
this.processing = false
|
this.processing = false
|
||||||
},
|
},
|
||||||
|
|
@ -174,7 +174,7 @@ export default {
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
this.$toast.error(this.$strings.ToastAuthorImageRemoveFailed)
|
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|
@ -182,7 +182,7 @@ export default {
|
||||||
},
|
},
|
||||||
submitUploadCover() {
|
submitUploadCover() {
|
||||||
if (!this.imageUrl?.startsWith('http:') && !this.imageUrl?.startsWith('https:')) {
|
if (!this.imageUrl?.startsWith('http:') && !this.imageUrl?.startsWith('https:')) {
|
||||||
this.$toast.error('Invalid image url')
|
this.$toast.error(this.$strings.ToastInvalidImageUrl)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,14 +194,14 @@ export default {
|
||||||
.$post(`/api/authors/${this.authorId}/image`, updatePayload)
|
.$post(`/api/authors/${this.authorId}/image`, updatePayload)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.imageUrl = ''
|
this.imageUrl = ''
|
||||||
this.$toast.success('Author image updated')
|
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||||
|
|
||||||
this.authorCopy.updatedAt = data.author.updatedAt
|
this.authorCopy.updatedAt = data.author.updatedAt
|
||||||
this.authorCopy.imagePath = data.author.imagePath
|
this.authorCopy.imagePath = data.author.imagePath
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', 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(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|
@ -209,7 +209,7 @@ export default {
|
||||||
},
|
},
|
||||||
async searchAuthor() {
|
async searchAuthor() {
|
||||||
if (!this.authorCopy.name && !this.authorCopy.asin) {
|
if (!this.authorCopy.name && !this.authorCopy.asin) {
|
||||||
this.$toast.error('Must enter an author name')
|
this.$toast.error(this.$strings.ToastNameRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
@ -228,17 +228,19 @@ export default {
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
if (!response) {
|
if (!response) {
|
||||||
this.$toast.error('Author not found')
|
this.$toast.error(this.$strings.ToastAuthorSearchNotFound)
|
||||||
} else if (response.updated) {
|
} else if (response.updated) {
|
||||||
if (response.author.imagePath) {
|
if (response.author.imagePath) {
|
||||||
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||||
} else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
} else {
|
||||||
|
this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
||||||
|
}
|
||||||
|
|
||||||
this.authorCopy = {
|
this.authorCopy = {
|
||||||
...response.author
|
...response.author
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info('No updates were made for Author')
|
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
}
|
}
|
||||||
this.processing = false
|
this.processing = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal v-model="show" name="changelog" :width="800" :height="'unset'">
|
<modals-modal v-model="show" name="changelog" :width="800" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="text-3xl text-white truncate">Changelog</p>
|
<h1 class="text-3xl text-white truncate">Changelog</h1>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh">
|
<div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh">
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
</p>
|
</p>
|
||||||
<div class="custom-text" v-html="getChangelog(release)" />
|
<div class="custom-text" v-html="getChangelog(release)" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="release !== releasesToShow[releasesToShow.length - 1]" class="border-b border-black-300 my-8" />
|
<div v-if="release !== releasesToShow[releasesToShow.length - 1]" :key="`${release.name}-divider`" class="border-b border-black-300 my-8" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ export default {
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove books from collection', 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
|
this.processing = false
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -157,7 +157,7 @@ export default {
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove book from collection', 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
|
this.processing = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -172,12 +172,12 @@ export default {
|
||||||
.$post(`/api/collections/${collection.id}/batch/add`, { books: this.selectedBookIds })
|
.$post(`/api/collections/${collection.id}/batch/add`, { books: this.selectedBookIds })
|
||||||
.then((updatedCollection) => {
|
.then((updatedCollection) => {
|
||||||
console.log(`Books added to collection`, updatedCollection)
|
console.log(`Books added to collection`, updatedCollection)
|
||||||
this.$toast.success('Books added to collection')
|
this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to add books to collection', 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
|
this.processing = false
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -187,12 +187,12 @@ export default {
|
||||||
.$post(`/api/collections/${collection.id}/book`, { id: this.selectedLibraryItemId })
|
.$post(`/api/collections/${collection.id}/book`, { id: this.selectedLibraryItemId })
|
||||||
.then((updatedCollection) => {
|
.then((updatedCollection) => {
|
||||||
console.log(`Book added to collection`, updatedCollection)
|
console.log(`Book added to collection`, updatedCollection)
|
||||||
this.$toast.success('Book added to collection')
|
this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to add book to collection', 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
|
this.processing = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -221,7 +221,7 @@ export default {
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to create collection', error)
|
console.error('Failed to create collection', error)
|
||||||
var errMsg = error.response ? error.response.data || '' : ''
|
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
|
this.processing = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ export default {
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove collection', error)
|
console.error('Failed to remove collection', error)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.$toast.error(this.$strings.ToastCollectionRemoveFailed)
|
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -115,7 +115,7 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!this.newCollectionName) {
|
if (!this.newCollectionName) {
|
||||||
return this.$toast.error('Collection must have a name')
|
return this.$toast.error(this.$strings.ToastNameRequired)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
@ -135,7 +135,7 @@ export default {
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update collection', error)
|
console.error('Failed to update collection', error)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.$toast.error(this.$strings.ToastCollectionUpdateFailed)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -125,12 +125,12 @@ export default {
|
||||||
this.$refs.ereaderEmailInput.blur()
|
this.$refs.ereaderEmailInput.blur()
|
||||||
|
|
||||||
if (!this.newDevice.name?.trim() || !this.newDevice.email?.trim()) {
|
if (!this.newDevice.name?.trim() || !this.newDevice.email?.trim()) {
|
||||||
this.$toast.error('Name and email required')
|
this.$toast.error(this.$strings.ToastNameEmailRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.newDevice.availabilityOption === 'specificUsers' && !this.newDevice.users.length) {
|
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
|
return
|
||||||
}
|
}
|
||||||
if (this.newDevice.availabilityOption !== 'specificUsers') {
|
if (this.newDevice.availabilityOption !== 'specificUsers') {
|
||||||
|
|
@ -142,14 +142,14 @@ export default {
|
||||||
|
|
||||||
if (!this.ereaderDevice) {
|
if (!this.ereaderDevice) {
|
||||||
if (this.existingDevices.some((d) => d.name === this.newDevice.name)) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.submitCreate()
|
this.submitCreate()
|
||||||
} else {
|
} else {
|
||||||
if (this.ereaderDevice.name !== this.newDevice.name && this.existingDevices.some((d) => d.name === this.newDevice.name)) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,12 +174,11 @@ export default {
|
||||||
.$post(`/api/emails/ereader-devices`, payload)
|
.$post(`/api/emails/ereader-devices`, payload)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.$emit('update', data.ereaderDevices)
|
this.$emit('update', data.ereaderDevices)
|
||||||
this.$toast.success('Device updated')
|
|
||||||
this.show = false
|
this.show = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update device', error)
|
console.error('Failed to update device', error)
|
||||||
this.$toast.error('Failed to update device')
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|
@ -201,12 +200,11 @@ export default {
|
||||||
.$post('/api/emails/ereader-devices', payload)
|
.$post('/api/emails/ereader-devices', payload)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.$emit('update', data.ereaderDevices || [])
|
this.$emit('update', data.ereaderDevices || [])
|
||||||
this.$toast.success('Device added')
|
|
||||||
this.show = false
|
this.show = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to add device', error)
|
console.error('Failed to add device', error)
|
||||||
this.$toast.error('Failed to add device')
|
this.$toast.error(this.$strings.ToastDeviceAddFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|
|
||||||
188
client/components/modals/emails/UserEReaderDeviceModal.vue
Normal file
188
client/components/modals/emails/UserEReaderDeviceModal.vue
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
<template>
|
||||||
|
<modals-modal ref="modal" v-model="show" name="ereader-device-edit" :width="800" :height="'unset'" :processing="processing">
|
||||||
|
<template #outer>
|
||||||
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<form @submit.prevent="submitForm">
|
||||||
|
<div class="w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300">
|
||||||
|
<div class="w-full px-3 py-5 md:p-12">
|
||||||
|
<div class="flex items-center -mx-1 mb-4">
|
||||||
|
<div class="w-full md:w-1/2 px-1">
|
||||||
|
<ui-text-input-with-label ref="ereaderNameInput" v-model="newDevice.name" :disabled="processing" :label="$strings.LabelName" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full md:w-1/2 px-1">
|
||||||
|
<ui-text-input-with-label ref="ereaderEmailInput" v-model="newDevice.email" :disabled="processing" :label="$strings.LabelEmail" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center pt-4">
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean,
|
||||||
|
existingDevices: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
ereaderDevice: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processing: false,
|
||||||
|
newDevice: {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
availabilityOption: 'adminAndUp',
|
||||||
|
users: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
user() {
|
||||||
|
return this.$store.state.user.user
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return !this.ereaderDevice ? 'Create Device' : 'Update Device'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submitForm() {
|
||||||
|
this.$refs.ereaderNameInput.blur()
|
||||||
|
this.$refs.ereaderEmailInput.blur()
|
||||||
|
|
||||||
|
if (!this.newDevice.name?.trim() || !this.newDevice.email?.trim()) {
|
||||||
|
this.$toast.error(this.$strings.ToastNameEmailRequired)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.newDevice.name = this.newDevice.name.trim()
|
||||||
|
this.newDevice.email = this.newDevice.email.trim()
|
||||||
|
|
||||||
|
// Only catches duplicate names for the current user
|
||||||
|
// Duplicates with other users caught on server side
|
||||||
|
if (!this.ereaderDevice) {
|
||||||
|
if (this.existingDevices.some((d) => d.name === this.newDevice.name)) {
|
||||||
|
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitCreate()
|
||||||
|
} else {
|
||||||
|
if (this.ereaderDevice.name !== this.newDevice.name && this.existingDevices.some((d) => d.name === this.newDevice.name)) {
|
||||||
|
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitUpdate()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submitUpdate() {
|
||||||
|
this.processing = true
|
||||||
|
|
||||||
|
const existingDevicesWithoutThisOne = this.existingDevices.filter((d) => d.name !== this.ereaderDevice.name)
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
ereaderDevices: [
|
||||||
|
...existingDevicesWithoutThisOne,
|
||||||
|
{
|
||||||
|
...this.newDevice
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/me/ereader-devices`, payload)
|
||||||
|
.then((data) => {
|
||||||
|
this.$emit('update', data.ereaderDevices)
|
||||||
|
this.show = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to update device', error)
|
||||||
|
if (error.response?.data?.toLowerCase().includes('duplicate')) {
|
||||||
|
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
|
||||||
|
} else {
|
||||||
|
this.$toast.error(this.$strings.ToastDeviceAddFailed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitCreate() {
|
||||||
|
this.processing = true
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
ereaderDevices: [
|
||||||
|
...this.existingDevices,
|
||||||
|
{
|
||||||
|
...this.newDevice
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$post('/api/me/ereader-devices', payload)
|
||||||
|
.then((data) => {
|
||||||
|
this.$emit('update', data.ereaderDevices || [])
|
||||||
|
this.show = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to add device', error)
|
||||||
|
if (error.response?.data?.toLowerCase().includes('duplicate')) {
|
||||||
|
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
|
||||||
|
} else {
|
||||||
|
this.$toast.error(this.$strings.ToastDeviceAddFailed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
if (this.ereaderDevice) {
|
||||||
|
this.newDevice.name = this.ereaderDevice.name
|
||||||
|
this.newDevice.email = this.ereaderDevice.email
|
||||||
|
this.newDevice.availabilityOption = this.ereaderDevice.availabilityOption || 'specificUsers'
|
||||||
|
this.newDevice.users = this.ereaderDevice.users || [this.user.id]
|
||||||
|
} else {
|
||||||
|
this.newDevice.name = ''
|
||||||
|
this.newDevice.email = ''
|
||||||
|
this.newDevice.availabilityOption = 'specificUsers'
|
||||||
|
this.newDevice.users = [this.user.id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -2,24 +2,24 @@
|
||||||
<modals-modal v-model="show" name="edit-book" :width="800" :height="height" :processing="processing" :content-margin-top="marginTop">
|
<modals-modal v-model="show" name="edit-book" :width="800" :height="height" :processing="processing" :content-margin-top="marginTop">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-4 landscape:px-4 landscape:py-2 md:portrait:p-5 lg:p-5 w-2/3 overflow-hidden pointer-events-none">
|
<div class="absolute top-0 left-0 p-4 landscape:px-4 landscape:py-2 md:portrait:p-5 lg:p-5 w-2/3 overflow-hidden pointer-events-none">
|
||||||
<p class="text-xl md:portrait:text-3xl md:landscape:text-lg lg:text-3xl text-white truncate pointer-events-none">{{ title }}</p>
|
<h1 class="text-xl md:portrait:text-3xl md:landscape:text-lg lg:text-3xl text-white truncate pointer-events-none">{{ title }}</h1>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
<div role="tablist" class="absolute -top-10 left-0 z-10 w-full flex">
|
||||||
<template v-for="tab in availableTabs">
|
<template v-for="tab in availableTabs">
|
||||||
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
<button :key="tab.id" role="tab" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
<div role="tabpanel" class="w-full h-full max-h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative">
|
||||||
<div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevBook" @mousedown.prevent>arrow_back_ios</div>
|
<component v-if="libraryItem && show" :is="tabName" :library-item="libraryItem" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
|
||||||
</div>
|
|
||||||
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
|
||||||
<div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextBook" @mousedown.prevent>arrow_forward_ios</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full h-full max-h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative">
|
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||||
<component v-if="libraryItem && show" :is="tabName" :library-item="libraryItem" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
|
<button class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" :aria-label="$strings.ButtonNext" @click.stop.prevent="goPrevBook" @mousedown.prevent>arrow_back_ios</button>
|
||||||
|
</div>
|
||||||
|
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||||
|
<button class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" :aria-label="$strings.ButtonPrevious" @click.stop.prevent="goNextBook" @mousedown.prevent>arrow_forward_ios</button>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,6 @@ export default {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
this.$toast.error(data.error)
|
this.$toast.error(data.error)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.success('Cover Uploaded')
|
|
||||||
this.resetCoverPreview()
|
this.resetCoverPreview()
|
||||||
}
|
}
|
||||||
this.processingUpload = false
|
this.processingUpload = false
|
||||||
|
|
@ -204,7 +203,7 @@ export default {
|
||||||
if (error.response && error.response.data) {
|
if (error.response && error.response.data) {
|
||||||
this.$toast.error(error.response.data)
|
this.$toast.error(error.response.data)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.error('Oops, something went wrong...')
|
this.$toast.error(this.$strings.ToastUnknownError)
|
||||||
}
|
}
|
||||||
this.processingUpload = false
|
this.processingUpload = false
|
||||||
})
|
})
|
||||||
|
|
@ -255,7 +254,7 @@ export default {
|
||||||
},
|
},
|
||||||
async updateCover(cover) {
|
async updateCover(cover) {
|
||||||
if (!cover.startsWith('http:') && !cover.startsWith('https:')) {
|
if (!cover.startsWith('http:') && !cover.startsWith('https:')) {
|
||||||
this.$toast.error('Invalid URL')
|
this.$toast.error(this.$strings.ToastInvalidUrl)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -264,11 +263,10 @@ export default {
|
||||||
.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover })
|
.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.imageUrl = ''
|
this.imageUrl = ''
|
||||||
this.$toast.success('Update Successful')
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update cover', 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(() => {
|
.finally(() => {
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
|
|
@ -308,12 +306,9 @@ export default {
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$patch(`/api/items/${this.libraryItemId}/cover`, { cover: coverFile.metadata.path })
|
.$patch(`/api/items/${this.libraryItemId}/cover`, { cover: coverFile.metadata.path })
|
||||||
.then(() => {
|
|
||||||
this.$toast.success('Update Successful')
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to set local cover', 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(() => {
|
.finally(() => {
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
|
|
@ -321,4 +316,4 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ export default {
|
||||||
|
|
||||||
var { title, author } = this.$refs.itemDetailsEdit.getTitleAndAuthorName()
|
var { title, author } = this.$refs.itemDetailsEdit.getTitleAndAuthorName()
|
||||||
if (!title) {
|
if (!title) {
|
||||||
this.$toast.error('Must have a title for quick match')
|
this.$toast.error(this.$strings.ToastTitleRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.quickMatching = true
|
this.quickMatching = true
|
||||||
|
|
@ -108,9 +108,9 @@ export default {
|
||||||
if (res.warning) {
|
if (res.warning) {
|
||||||
this.$toast.warning(res.warning)
|
this.$toast.warning(res.warning)
|
||||||
} else if (res.updated) {
|
} else if (res.updated) {
|
||||||
this.$toast.success('Item details updated')
|
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info('No updates were made')
|
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|
@ -128,18 +128,18 @@ export default {
|
||||||
this.rescanning = false
|
this.rescanning = false
|
||||||
var result = data.result
|
var result = data.result
|
||||||
if (!result) {
|
if (!result) {
|
||||||
this.$toast.error(`Re-Scan Failed for "${this.title}"`)
|
this.$toast.error(this.$getString('ToastRescanFailed', [this.title]))
|
||||||
} else if (result === 'UPDATED') {
|
} else if (result === 'UPDATED') {
|
||||||
this.$toast.success(`Re-Scan complete item was updated`)
|
this.$toast.success(this.$strings.ToastRescanUpdated)
|
||||||
} else if (result === 'UPTODATE') {
|
} 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') {
|
} else if (result === 'REMOVED') {
|
||||||
this.$toast.error(`Re-Scan complete item was removed`)
|
this.$toast.error(this.$strings.ToastRescanRemoved)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to scan library item', 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
|
this.rescanning = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
@ -156,7 +156,7 @@ export default {
|
||||||
}
|
}
|
||||||
var updatedDetails = this.$refs.itemDetailsEdit.getDetails()
|
var updatedDetails = this.$refs.itemDetailsEdit.getDetails()
|
||||||
if (!updatedDetails.hasChanges) {
|
if (!updatedDetails.hasChanges) {
|
||||||
this.$toast.info('No changes were made')
|
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return this.updateDetails(updatedDetails)
|
return this.updateDetails(updatedDetails)
|
||||||
|
|
@ -170,7 +170,7 @@ export default {
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
if (updateResult) {
|
if (updateResult) {
|
||||||
if (updateResult.updated) {
|
if (updateResult.updated) {
|
||||||
this.$toast.success('Item details updated')
|
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||||
|
|
@ -217,4 +217,4 @@ export default {
|
||||||
height: calc(100% - 80px);
|
height: calc(100% - 80px);
|
||||||
max-height: calc(100% - 80px);
|
max-height: calc(100% - 80px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<ui-text-input-with-label ref="maxEpisodesInput" v-model="maxEpisodesToDownload" :disabled="checkingNewEpisodes" type="number" :label="$strings.LabelLimit" class="w-16 mr-2" input-class="h-10">
|
<ui-text-input-with-label ref="maxEpisodesInput" v-model="maxEpisodesToDownload" :disabled="checkingNewEpisodes" type="number" :label="$strings.LabelLimit" class="w-16 mr-2" input-class="h-10">
|
||||||
<div class="flex -mb-0.5">
|
<div class="flex -mb-0.5">
|
||||||
<p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': checkingNewEpisodes }">{{ $strings.LabelLimit }}</p>
|
<p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': checkingNewEpisodes }">{{ $strings.LabelLimit }}</p>
|
||||||
<ui-tooltip direction="top" text="Max # of episodes to download. Use 0 for unlimited.">
|
<ui-tooltip direction="top" :text="$strings.LabelMaxEpisodesToDownload">
|
||||||
<span class="material-symbols text-base">info</span>
|
<span class="material-symbols text-base">info</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -99,7 +99,7 @@ export default {
|
||||||
|
|
||||||
if (this.maxEpisodesToDownload < 0) {
|
if (this.maxEpisodesToDownload < 0) {
|
||||||
this.maxEpisodesToDownload = 3
|
this.maxEpisodesToDownload = 3
|
||||||
this.$toast.error('Invalid max episodes to download')
|
this.$toast.error(this.$strings.ToastInvalidMaxEpisodesToDownload)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,9 +120,9 @@ export default {
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.episodes && response.episodes.length) {
|
if (response.episodes && response.episodes.length) {
|
||||||
console.log('New episodes', response.episodes.length)
|
console.log('New episodes', response.episodes.length)
|
||||||
this.$toast.success(`${response.episodes.length} new episodes found!`)
|
this.$toast.success(this.$getString('ToastNewEpisodesFound', [response.episodes.length]))
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info('No new episodes found')
|
this.$toast.info(this.$strings.ToastNoNewEpisodesFound)
|
||||||
}
|
}
|
||||||
this.checkingNewEpisodes = false
|
this.checkingNewEpisodes = false
|
||||||
})
|
})
|
||||||
|
|
@ -141,4 +141,4 @@ export default {
|
||||||
this.setLastEpisodeCheckInput()
|
this.setLastEpisodeCheckInput()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" :label="$strings.LabelTitle" />
|
<ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" :label="$strings.LabelTitle" />
|
||||||
<p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60">
|
<p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('title', mediaMetadata.title)">{{ mediaMetadata.title || '' }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('title', mediaMetadata.title)">{{ mediaMetadata.title || '' }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -69,7 +69,7 @@
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" :label="$strings.LabelSubtitle" />
|
<ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" :label="$strings.LabelSubtitle" />
|
||||||
<p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60">
|
<p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('subtitle', mediaMetadata.subtitle)">{{ mediaMetadata.subtitle }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('subtitle', mediaMetadata.subtitle)">{{ mediaMetadata.subtitle }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" :label="$strings.LabelAuthor" />
|
<ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" :label="$strings.LabelAuthor" />
|
||||||
<p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60">
|
<p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('author', mediaMetadata.authorName)">{{ mediaMetadata.authorName }}</a>
|
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('author', mediaMetadata.authorName)">{{ mediaMetadata.authorName }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-multi-select v-model="selectedMatch.narrator" :items="narrators" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" />
|
<ui-multi-select v-model="selectedMatch.narrator" :items="narrators" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" />
|
||||||
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">
|
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('narrator', mediaMetadata.narrators)">{{ mediaMetadata.narratorName }}</a>
|
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('narrator', mediaMetadata.narrators)">{{ mediaMetadata.narratorName }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -96,7 +96,7 @@
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" />
|
<ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" />
|
||||||
<p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60">
|
<p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</a>
|
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -105,7 +105,7 @@
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" :label="$strings.LabelPublisher" />
|
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" :label="$strings.LabelPublisher" />
|
||||||
<p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60">
|
<p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publisher', mediaMetadata.publisher)">{{ mediaMetadata.publisher }}</a>
|
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publisher', mediaMetadata.publisher)">{{ mediaMetadata.publisher }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -114,7 +114,7 @@
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" :label="$strings.LabelPublishYear" />
|
<ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" :label="$strings.LabelPublishYear" />
|
||||||
<p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60">
|
<p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publishedYear', mediaMetadata.publishedYear)">{{ mediaMetadata.publishedYear }}</a>
|
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publishedYear', mediaMetadata.publishedYear)">{{ mediaMetadata.publishedYear }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -124,7 +124,7 @@
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<widgets-series-input-widget v-model="selectedMatch.series" :disabled="!selectedMatchUsage.series" />
|
<widgets-series-input-widget v-model="selectedMatch.series" :disabled="!selectedMatchUsage.series" />
|
||||||
<p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">
|
<p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('series', mediaMetadata.series)">{{ mediaMetadata.seriesName }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('series', mediaMetadata.series)">{{ mediaMetadata.seriesName }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -133,7 +133,7 @@
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-multi-select v-model="selectedMatch.genres" :items="genres" :disabled="!selectedMatchUsage.genres" :label="$strings.LabelGenres" />
|
<ui-multi-select v-model="selectedMatch.genres" :items="genres" :disabled="!selectedMatchUsage.genres" :label="$strings.LabelGenres" />
|
||||||
<p v-if="mediaMetadata.genres?.length" class="text-xs ml-1 text-white text-opacity-60">
|
<p v-if="mediaMetadata.genres?.length" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('genres', mediaMetadata.genres)">{{ mediaMetadata.genres.join(', ') }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('genres', mediaMetadata.genres)">{{ mediaMetadata.genres.join(', ') }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -142,7 +142,7 @@
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-multi-select v-model="selectedMatch.tags" :items="tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" />
|
<ui-multi-select v-model="selectedMatch.tags" :items="tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" />
|
||||||
<p v-if="media.tags?.length" class="text-xs ml-1 text-white text-opacity-60">
|
<p v-if="media.tags?.length" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('tags', media.tags)">{{ media.tags.join(', ') }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('tags', media.tags)">{{ media.tags.join(', ') }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -151,7 +151,7 @@
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" :label="$strings.LabelLanguage" />
|
<ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" :label="$strings.LabelLanguage" />
|
||||||
<p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60">
|
<p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('language', mediaMetadata.language)">{{ mediaMetadata.language }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('language', mediaMetadata.language)">{{ mediaMetadata.language }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -160,7 +160,7 @@
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" />
|
<ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" />
|
||||||
<p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60">
|
<p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('isbn', mediaMetadata.isbn)">{{ mediaMetadata.isbn }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('isbn', mediaMetadata.isbn)">{{ mediaMetadata.isbn }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -169,7 +169,7 @@
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" />
|
<ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" />
|
||||||
<p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60">
|
<p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('asin', mediaMetadata.asin)">{{ mediaMetadata.asin }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('asin', mediaMetadata.asin)">{{ mediaMetadata.asin }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -179,7 +179,7 @@
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.itunesId" type="number" :disabled="!selectedMatchUsage.itunesId" label="iTunes ID" />
|
<ui-text-input-with-label v-model="selectedMatch.itunesId" type="number" :disabled="!selectedMatchUsage.itunesId" label="iTunes ID" />
|
||||||
<p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60">
|
<p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesId', mediaMetadata.itunesId)">{{ mediaMetadata.itunesId }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesId', mediaMetadata.itunesId)">{{ mediaMetadata.itunesId }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -188,7 +188,7 @@
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.feedUrl" :disabled="!selectedMatchUsage.feedUrl" label="RSS Feed URL" />
|
<ui-text-input-with-label v-model="selectedMatch.feedUrl" :disabled="!selectedMatchUsage.feedUrl" label="RSS Feed URL" />
|
||||||
<p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60">
|
<p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('feedUrl', mediaMetadata.feedUrl)">{{ mediaMetadata.feedUrl }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('feedUrl', mediaMetadata.feedUrl)">{{ mediaMetadata.feedUrl }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -197,7 +197,7 @@
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.itunesPageUrl" :disabled="!selectedMatchUsage.itunesPageUrl" label="iTunes Page URL" />
|
<ui-text-input-with-label v-model="selectedMatch.itunesPageUrl" :disabled="!selectedMatchUsage.itunesPageUrl" label="iTunes Page URL" />
|
||||||
<p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60">
|
<p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesPageUrl', mediaMetadata.itunesPageUrl)">{{ mediaMetadata.itunesPageUrl }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesPageUrl', mediaMetadata.itunesPageUrl)">{{ mediaMetadata.itunesPageUrl }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -206,7 +206,7 @@
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" :label="$strings.LabelReleaseDate" />
|
<ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" :label="$strings.LabelReleaseDate" />
|
||||||
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">
|
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('releaseDate', mediaMetadata.releaseDate)">{{ mediaMetadata.releaseDate }}</a>
|
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('releaseDate', mediaMetadata.releaseDate)">{{ mediaMetadata.releaseDate }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -397,7 +397,7 @@ export default {
|
||||||
},
|
},
|
||||||
submitSearch() {
|
submitSearch() {
|
||||||
if (!this.searchTitle) {
|
if (!this.searchTitle) {
|
||||||
this.$toast.warning('Search title is required')
|
this.$toast.warning(this.$strings.ToastTitleRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.persistProvider()
|
this.persistProvider()
|
||||||
|
|
@ -618,12 +618,12 @@ export default {
|
||||||
if (updateResult.updated) {
|
if (updateResult.updated) {
|
||||||
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info(this.$strings.ToastItemDetailsUpdateUnneeded)
|
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
}
|
}
|
||||||
this.clearSelectedMatch()
|
this.clearSelectedMatch()
|
||||||
this.$emit('selectTab', 'details')
|
this.$emit('selectTab', 'details')
|
||||||
} else {
|
} else {
|
||||||
this.$toast.error(this.$strings.ToastItemDetailsUpdateFailed)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.clearSelectedMatch()
|
this.clearSelectedMatch()
|
||||||
|
|
|
||||||
|
|
@ -2,28 +2,28 @@
|
||||||
<div class="w-full h-full relative">
|
<div class="w-full h-full relative">
|
||||||
<div id="scheduleWrapper" class="w-full overflow-y-auto px-2 py-4 md:px-6 md:py-6">
|
<div id="scheduleWrapper" class="w-full overflow-y-auto px-2 py-4 md:px-6 md:py-6">
|
||||||
<template v-if="!feedUrl">
|
<template v-if="!feedUrl">
|
||||||
<widgets-alert type="warning" class="text-base mb-4">No RSS feed URL is set for this podcast</widgets-alert>
|
<widgets-alert type="warning" class="text-base mb-4">{{ $strings.ToastPodcastNoRssFeed }}</widgets-alert>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="feedUrl || autoDownloadEpisodes">
|
<template v-if="feedUrl || autoDownloadEpisodes">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<p class="text-base md:text-xl font-semibold">Schedule Automatic Episode Downloads</p>
|
<p class="text-base md:text-xl font-semibold">{{ $strings.HeaderScheduleEpisodeDownloads }}</p>
|
||||||
<ui-checkbox v-model="enableAutoDownloadEpisodes" label="Enable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" />
|
<ui-checkbox v-model="enableAutoDownloadEpisodes" :label="$strings.LabelEnable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
|
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
|
||||||
<ui-text-input ref="maxEpisodesInput" type="number" v-model="newMaxEpisodesToKeep" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updatedMaxEpisodesToKeep" />
|
<ui-text-input ref="maxEpisodesInput" type="number" v-model="newMaxEpisodesToKeep" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updatedMaxEpisodesToKeep" />
|
||||||
<ui-tooltip text="Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes. <br>This will only delete 1 episode per new download.">
|
<ui-tooltip :text="$strings.LabelMaxEpisodesToKeepHelp">
|
||||||
<p class="pl-4 text-base">
|
<p class="pl-4 text-base">
|
||||||
Max episodes to keep
|
{{ $strings.LabelMaxEpisodesToKeep }}
|
||||||
<span class="material-symbols icon-text">info</span>
|
<span class="material-symbols icon-text">info</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
|
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
|
||||||
<ui-text-input ref="maxEpisodesToDownloadInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" />
|
<ui-text-input ref="maxEpisodesToDownloadInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" />
|
||||||
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded.">
|
<ui-tooltip :text="$strings.LabelUseZeroForUnlimited">
|
||||||
<p class="pl-4 text-base">
|
<p class="pl-4 text-base">
|
||||||
Max new episodes to download per check
|
{{ $strings.LabelMaxEpisodesToDownloadPerCheck }}
|
||||||
<span class="material-symbols icon-text">info</span>
|
<span class="material-symbols icon-text">info</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
<div v-if="feedUrl || autoDownloadEpisodes" class="absolute bottom-0 left-0 w-full py-2 md:py-4 bg-bg border-t border-white border-opacity-5">
|
<div v-if="feedUrl || autoDownloadEpisodes" class="absolute bottom-0 left-0 w-full py-2 md:py-4 bg-bg border-t border-white border-opacity-5">
|
||||||
<div class="flex items-center px-2 md:px-4">
|
<div class="flex items-center px-2 md:px-4">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn @click="save" :disabled="!isUpdated" :color="isUpdated ? 'success' : 'primary'" class="mx-2">{{ isUpdated ? 'Save' : 'No update necessary' }}</ui-btn>
|
<ui-btn @click="save" :disabled="!isUpdated" :color="isUpdated ? 'success' : 'primary'" class="mx-2">{{ isUpdated ? $strings.ButtonSave : $strings.MessageNoUpdatesWereNecessary }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -163,7 +163,7 @@ export default {
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
if (updateResult) {
|
if (updateResult) {
|
||||||
if (updateResult.updated) {
|
if (updateResult.updated) {
|
||||||
this.$toast.success('Item details updated')
|
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||||
|
|
|
||||||
|
|
@ -33,18 +33,18 @@
|
||||||
<span class="material-symbols text-lg ml-2">launch</span>
|
<span class="material-symbols text-lg ml-2">launch</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
<ui-btn v-if="!isMetadataEmbedQueued && !isEmbedTaskRunning" class="w-full mt-4" small @click.stop="quickEmbed">Quick Embed</ui-btn>
|
<ui-btn v-if="!isMetadataEmbedQueued && !isEmbedTaskRunning" class="w-full mt-4" small @click.stop="quickEmbed">{{ $strings.ButtonQuickEmbed }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- queued alert -->
|
<!-- queued alert -->
|
||||||
<widgets-alert v-if="isMetadataEmbedQueued" type="warning" class="mt-4">
|
<widgets-alert v-if="isMetadataEmbedQueued" type="warning" class="mt-4">
|
||||||
<p class="text-lg">Queued for metadata embed ({{ queuedEmbedLIds.length }} in queue)</p>
|
<p class="text-lg">{{ $getString('MessageQuickEmbedQueue', [queuedEmbedLIds.length]) }}</p>
|
||||||
</widgets-alert>
|
</widgets-alert>
|
||||||
|
|
||||||
<!-- processing alert -->
|
<!-- processing alert -->
|
||||||
<widgets-alert v-if="isEmbedTaskRunning" type="warning" class="mt-4">
|
<widgets-alert v-if="isEmbedTaskRunning" type="warning" class="mt-4">
|
||||||
<p class="text-lg">Currently embedding metadata</p>
|
<p class="text-lg">{{ $strings.MessageQuickEmbedInProgress }}</p>
|
||||||
</widgets-alert>
|
</widgets-alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -113,7 +113,7 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
quickEmbed() {
|
quickEmbed() {
|
||||||
const payload = {
|
const payload = {
|
||||||
message: 'Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?',
|
message: this.$strings.MessageConfirmQuickEmbed,
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.$axios
|
this.$axios
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,6 @@ export default {
|
||||||
},
|
},
|
||||||
updateLibrary(library) {
|
updateLibrary(library) {
|
||||||
this.mapLibraryToCopy(library)
|
this.mapLibraryToCopy(library)
|
||||||
console.log('Updated library', this.libraryCopy)
|
|
||||||
},
|
},
|
||||||
getNewLibraryData() {
|
getNewLibraryData() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -128,7 +127,9 @@ export default {
|
||||||
autoScanCronExpression: null,
|
autoScanCronExpression: null,
|
||||||
hideSingleBookSeries: false,
|
hideSingleBookSeries: false,
|
||||||
onlyShowLaterBooksInContinueSeries: false,
|
onlyShowLaterBooksInContinueSeries: false,
|
||||||
metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
|
metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'],
|
||||||
|
markAsFinishedPercentComplete: null,
|
||||||
|
markAsFinishedTimeRemaining: 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -156,11 +157,11 @@ export default {
|
||||||
},
|
},
|
||||||
validate() {
|
validate() {
|
||||||
if (!this.libraryCopy.name) {
|
if (!this.libraryCopy.name) {
|
||||||
this.$toast.error('Library must have a name')
|
this.$toast.error(this.$strings.ToastNameRequired)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (!this.libraryCopy.folders.length) {
|
if (!this.libraryCopy.folders.length) {
|
||||||
this.$toast.error('Library must have at least 1 path')
|
this.$toast.error(this.$strings.ToastMustHaveAtLeastOnePath)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,7 +206,7 @@ export default {
|
||||||
submitUpdateLibrary() {
|
submitUpdateLibrary() {
|
||||||
var newLibraryPayload = this.getLibraryUpdatePayload()
|
var newLibraryPayload = this.getLibraryUpdatePayload()
|
||||||
if (!Object.keys(newLibraryPayload).length) {
|
if (!Object.keys(newLibraryPayload).length) {
|
||||||
this.$toast.info('No updates are necessary')
|
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,7 +223,7 @@ export default {
|
||||||
if (error.response && error.response.data) {
|
if (error.response && error.response.data) {
|
||||||
this.$toast.error(error.response.data)
|
this.$toast.error(error.response.data)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.error(this.$strings.ToastLibraryUpdateFailed)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
}
|
}
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
|
|
@ -236,7 +237,6 @@ export default {
|
||||||
this.show = false
|
this.show = false
|
||||||
this.$toast.success(this.$getString('ToastLibraryCreateSuccess', [res.name]))
|
this.$toast.success(this.$getString('ToastLibraryCreateSuccess', [res.name]))
|
||||||
if (!this.$store.state.libraries.currentLibraryId) {
|
if (!this.$store.state.libraries.currentLibraryId) {
|
||||||
console.log('Setting initially library id', res.id)
|
|
||||||
// First library added
|
// First library added
|
||||||
this.$store.dispatch('libraries/fetch', res.id)
|
this.$store.dispatch('libraries/fetch', res.id)
|
||||||
}
|
}
|
||||||
|
|
@ -264,4 +264,4 @@ export default {
|
||||||
.tab.tab-selected {
|
.tab.tab-selected {
|
||||||
height: 41px;
|
height: 41px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ export default {
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to get filesystem paths', error)
|
console.error('Failed to get filesystem paths', error)
|
||||||
this.$toast.error('Failed to get filesystem paths')
|
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,78 +1,94 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full px-1 md:px-4 py-1 mb-4">
|
<div class="w-full h-full px-1 md:px-4 py-1 mb-4">
|
||||||
<div class="flex items-center py-3">
|
<div class="flex flex-wrap">
|
||||||
<ui-toggle-switch v-model="useSquareBookCovers" @input="formUpdated" />
|
<div class="flex items-center p-2 w-full md:w-1/2">
|
||||||
<ui-tooltip :text="$strings.LabelSettingsSquareBookCoversHelp">
|
<ui-toggle-switch v-model="useSquareBookCovers" size="sm" @input="formUpdated" />
|
||||||
<p class="pl-4 text-base">
|
<ui-tooltip :text="$strings.LabelSettingsSquareBookCoversHelp">
|
||||||
{{ $strings.LabelSettingsSquareBookCovers }}
|
<p class="pl-4 text-sm">
|
||||||
<span class="material-symbols icon-text text-sm">info</span>
|
{{ $strings.LabelSettingsSquareBookCovers }}
|
||||||
</p>
|
|
||||||
</ui-tooltip>
|
|
||||||
</div>
|
|
||||||
<div class="py-3">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<ui-toggle-switch v-if="!globalWatcherDisabled" v-model="enableWatcher" @input="formUpdated" />
|
|
||||||
<ui-toggle-switch v-else disabled :value="false" />
|
|
||||||
<p class="pl-4 text-base">{{ $strings.LabelSettingsEnableWatcherForLibrary }}</p>
|
|
||||||
</div>
|
|
||||||
<p v-if="globalWatcherDisabled" class="text-xs text-warning">*{{ $strings.MessageWatcherIsDisabledGlobally }}</p>
|
|
||||||
</div>
|
|
||||||
<div v-if="isBookLibrary" class="flex items-center py-3">
|
|
||||||
<ui-toggle-switch v-model="audiobooksOnly" @input="formUpdated" />
|
|
||||||
<ui-tooltip :text="$strings.LabelSettingsAudiobooksOnlyHelp">
|
|
||||||
<p class="pl-4 text-base">
|
|
||||||
{{ $strings.LabelSettingsAudiobooksOnly }}
|
|
||||||
<span class="material-symbols icon-text text-sm">info</span>
|
|
||||||
</p>
|
|
||||||
</ui-tooltip>
|
|
||||||
</div>
|
|
||||||
<div v-if="isBookLibrary" class="py-3">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<ui-toggle-switch v-model="skipMatchingMediaWithAsin" @input="formUpdated" />
|
|
||||||
<p class="pl-4 text-base">{{ $strings.LabelSettingsSkipMatchingBooksWithASIN }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="isBookLibrary" class="py-3">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<ui-toggle-switch v-model="skipMatchingMediaWithIsbn" @input="formUpdated" />
|
|
||||||
<p class="pl-4 text-base">{{ $strings.LabelSettingsSkipMatchingBooksWithISBN }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="isBookLibrary" class="py-3">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<ui-toggle-switch v-model="hideSingleBookSeries" @input="formUpdated" />
|
|
||||||
<ui-tooltip :text="$strings.LabelSettingsHideSingleBookSeriesHelp">
|
|
||||||
<p class="pl-4 text-base">
|
|
||||||
{{ $strings.LabelSettingsHideSingleBookSeries }}
|
|
||||||
<span class="material-symbols icon-text text-sm">info</span>
|
<span class="material-symbols icon-text text-sm">info</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="p-2 w-full md:w-1/2">
|
||||||
<div v-if="isBookLibrary" class="py-3">
|
<div class="flex items-center">
|
||||||
<div class="flex items-center">
|
<ui-toggle-switch v-if="!globalWatcherDisabled" v-model="enableWatcher" size="sm" @input="formUpdated" />
|
||||||
<ui-toggle-switch v-model="onlyShowLaterBooksInContinueSeries" @input="formUpdated" />
|
<ui-toggle-switch v-else disabled size="sm" :value="false" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp">
|
<p class="pl-4 text-sm">{{ $strings.LabelSettingsEnableWatcherForLibrary }}</p>
|
||||||
<p class="pl-4 text-base">
|
</div>
|
||||||
{{ $strings.LabelSettingsOnlyShowLaterBooksInContinueSeries }}
|
<p v-if="globalWatcherDisabled" class="text-xs text-warning">*{{ $strings.MessageWatcherIsDisabledGlobally }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="isBookLibrary" class="flex items-center p-2 w-full md:w-1/2">
|
||||||
|
<ui-toggle-switch v-model="audiobooksOnly" size="sm" @input="formUpdated" />
|
||||||
|
<ui-tooltip :text="$strings.LabelSettingsAudiobooksOnlyHelp">
|
||||||
|
<p class="pl-4 text-sm">
|
||||||
|
{{ $strings.LabelSettingsAudiobooksOnly }}
|
||||||
<span class="material-symbols icon-text text-sm">info</span>
|
<span class="material-symbols icon-text text-sm">info</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
|
||||||
<div v-if="isBookLibrary" class="py-3">
|
<div class="flex items-center">
|
||||||
<div class="flex items-center">
|
<ui-toggle-switch v-model="skipMatchingMediaWithAsin" size="sm" @input="formUpdated" />
|
||||||
<ui-toggle-switch v-model="epubsAllowScriptedContent" @input="formUpdated" />
|
<p class="pl-4 text-sm">{{ $strings.LabelSettingsSkipMatchingBooksWithASIN }}</p>
|
||||||
<ui-tooltip :text="$strings.LabelSettingsEpubsAllowScriptedContentHelp">
|
</div>
|
||||||
<p class="pl-4 text-base">
|
</div>
|
||||||
{{ $strings.LabelSettingsEpubsAllowScriptedContent }}
|
<div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
|
||||||
<span class="material-symbols icon-text text-sm">info</span>
|
<div class="flex items-center">
|
||||||
</p>
|
<ui-toggle-switch v-model="skipMatchingMediaWithIsbn" size="sm" @input="formUpdated" />
|
||||||
</ui-tooltip>
|
<p class="pl-4 text-sm">{{ $strings.LabelSettingsSkipMatchingBooksWithISBN }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<ui-toggle-switch v-model="hideSingleBookSeries" size="sm" @input="formUpdated" />
|
||||||
|
<ui-tooltip :text="$strings.LabelSettingsHideSingleBookSeriesHelp">
|
||||||
|
<p class="pl-4 text-sm">
|
||||||
|
{{ $strings.LabelSettingsHideSingleBookSeries }}
|
||||||
|
<span class="material-symbols icon-text text-sm">info</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<ui-toggle-switch v-model="onlyShowLaterBooksInContinueSeries" size="sm" @input="formUpdated" />
|
||||||
|
<ui-tooltip :text="$strings.LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp">
|
||||||
|
<p class="pl-4 text-sm">
|
||||||
|
{{ $strings.LabelSettingsOnlyShowLaterBooksInContinueSeries }}
|
||||||
|
<span class="material-symbols icon-text text-sm">info</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<ui-toggle-switch v-model="epubsAllowScriptedContent" size="sm" @input="formUpdated" />
|
||||||
|
<ui-tooltip :text="$strings.LabelSettingsEpubsAllowScriptedContentHelp">
|
||||||
|
<p class="pl-4 text-sm">
|
||||||
|
{{ $strings.LabelSettingsEpubsAllowScriptedContent }}
|
||||||
|
<span class="material-symbols icon-text text-sm">info</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="isPodcastLibrary" class="p-2 w-full md:w-1/2">
|
||||||
|
<ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-72" menu-max-height="200px" @input="formUpdated" />
|
||||||
|
</div>
|
||||||
|
<div class="p-2 w-full flex items-center space-x-2 flex-wrap">
|
||||||
|
<div>
|
||||||
|
<ui-dropdown v-model="markAsFinishedWhen" :items="maskAsFinishedWhenItems" :label="$strings.LabelSettingsLibraryMarkAsFinishedWhen" small class="w-72 min-w-72 text-sm" menu-max-height="200px" @input="markAsFinishedWhenChanged" />
|
||||||
|
</div>
|
||||||
|
<div class="w-16">
|
||||||
|
<div>
|
||||||
|
<label class="px-1 text-sm font-semibold"></label>
|
||||||
|
<div class="relative">
|
||||||
|
<ui-text-input v-model="markAsFinishedValue" type="number" label="" no-spinner custom-input-class="pr-5" @input="markAsFinishedChanged" />
|
||||||
|
<div class="absolute top-0 bottom-0 right-4 flex items-center">{{ markAsFinishedWhen === 'timeRemaining' ? '' : '%' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div v-if="isPodcastLibrary" class="py-3">
|
|
||||||
<ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-72" menu-max-height="200px" @input="formUpdated" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -97,7 +113,9 @@ export default {
|
||||||
epubsAllowScriptedContent: false,
|
epubsAllowScriptedContent: false,
|
||||||
hideSingleBookSeries: false,
|
hideSingleBookSeries: false,
|
||||||
onlyShowLaterBooksInContinueSeries: false,
|
onlyShowLaterBooksInContinueSeries: false,
|
||||||
podcastSearchRegion: 'us'
|
podcastSearchRegion: 'us',
|
||||||
|
markAsFinishedWhen: 'timeRemaining',
|
||||||
|
markAsFinishedValue: 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -119,10 +137,34 @@ export default {
|
||||||
providers() {
|
providers() {
|
||||||
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
|
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
|
||||||
return this.$store.state.scanners.providers
|
return this.$store.state.scanners.providers
|
||||||
|
},
|
||||||
|
maskAsFinishedWhenItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelSettingsLibraryMarkAsFinishedTimeRemaining,
|
||||||
|
value: 'timeRemaining'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelSettingsLibraryMarkAsFinishedPercentComplete,
|
||||||
|
value: 'percentComplete'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
markAsFinishedWhenChanged(val) {
|
||||||
|
if (val === 'percentComplete' && this.markAsFinishedValue > 100) {
|
||||||
|
this.markAsFinishedValue = 100
|
||||||
|
}
|
||||||
|
this.formUpdated()
|
||||||
|
},
|
||||||
|
markAsFinishedChanged(val) {
|
||||||
|
this.formUpdated()
|
||||||
|
},
|
||||||
getLibraryData() {
|
getLibraryData() {
|
||||||
|
let markAsFinishedTimeRemaining = this.markAsFinishedWhen === 'timeRemaining' ? Number(this.markAsFinishedValue) : null
|
||||||
|
let markAsFinishedPercentComplete = this.markAsFinishedWhen === 'percentComplete' ? Number(this.markAsFinishedValue) : null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
settings: {
|
settings: {
|
||||||
coverAspectRatio: this.useSquareBookCovers ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD,
|
coverAspectRatio: this.useSquareBookCovers ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD,
|
||||||
|
|
@ -133,7 +175,9 @@ export default {
|
||||||
epubsAllowScriptedContent: !!this.epubsAllowScriptedContent,
|
epubsAllowScriptedContent: !!this.epubsAllowScriptedContent,
|
||||||
hideSingleBookSeries: !!this.hideSingleBookSeries,
|
hideSingleBookSeries: !!this.hideSingleBookSeries,
|
||||||
onlyShowLaterBooksInContinueSeries: !!this.onlyShowLaterBooksInContinueSeries,
|
onlyShowLaterBooksInContinueSeries: !!this.onlyShowLaterBooksInContinueSeries,
|
||||||
podcastSearchRegion: this.podcastSearchRegion
|
podcastSearchRegion: this.podcastSearchRegion,
|
||||||
|
markAsFinishedTimeRemaining: markAsFinishedTimeRemaining,
|
||||||
|
markAsFinishedPercentComplete: markAsFinishedPercentComplete
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -150,6 +194,11 @@ export default {
|
||||||
this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries
|
this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries
|
||||||
this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries
|
this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries
|
||||||
this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us'
|
this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us'
|
||||||
|
this.markAsFinishedWhen = this.librarySettings.markAsFinishedTimeRemaining ? 'timeRemaining' : 'percentComplete'
|
||||||
|
if (!this.librarySettings.markAsFinishedTimeRemaining && !this.librarySettings.markAsFinishedPercentComplete) {
|
||||||
|
this.markAsFinishedWhen = 'timeRemaining'
|
||||||
|
}
|
||||||
|
this.markAsFinishedValue = this.librarySettings.markAsFinishedTimeRemaining || this.librarySettings.markAsFinishedPercentComplete || 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@
|
||||||
<div class="w-full border border-black-200 p-4 my-8">
|
<div class="w-full border border-black-200 p-4 my-8">
|
||||||
<div class="flex flex-wrap items-center">
|
<div class="flex flex-wrap items-center">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-lg">Remove metadata files in library item folders</p>
|
<p class="text-lg">{{ $strings.LabelRemoveMetadataFile }}</p>
|
||||||
<p class="max-w-sm text-sm pt-2 text-gray-300">Remove all metadata.json or metadata.abs files in your {{ mediaType }} folders</p>
|
<p class="max-w-sm text-sm pt-2 text-gray-300">{{ $getString('LabelRemoveMetadataFileHelp', [mediaType]) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<div>
|
<div>
|
||||||
<ui-btn class="mb-4 block" @click.stop="removeAllMetadataClick('json')">Remove all metadata.json</ui-btn>
|
<ui-btn class="mb-4 block" @click.stop="removeAllMetadataClick('json')">{{ $strings.LabelRemoveAllMetadataJson }}</ui-btn>
|
||||||
<ui-btn @click.stop="removeAllMetadataClick('abs')">Remove all metadata.abs</ui-btn>
|
<ui-btn @click.stop="removeAllMetadataClick('abs')">{{ $strings.LabelRemoveAllMetadataAbs }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -43,7 +43,7 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
removeAllMetadataClick(ext) {
|
removeAllMetadataClick(ext) {
|
||||||
const payload = {
|
const payload = {
|
||||||
message: `Are you sure you want to remove all metadata.${ext} files in your library item folders?`,
|
message: this.$getString('MessageConfirmRemoveMetadataFiles', [ext]),
|
||||||
persistent: true,
|
persistent: true,
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
|
|
@ -60,16 +60,16 @@ export default {
|
||||||
.$post(`/api/libraries/${this.libraryId}/remove-metadata?ext=${ext}`)
|
.$post(`/api/libraries/${this.libraryId}/remove-metadata?ext=${ext}`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (!data.found) {
|
if (!data.found) {
|
||||||
this.$toast.info(`No metadata.${ext} files were found in library`)
|
this.$toast.info(this.$getString('ToastMetadataFilesRemovedNoneFound', [ext]))
|
||||||
} else if (!data.removed) {
|
} else if (!data.removed) {
|
||||||
this.$toast.success(`No metadata.${ext} files removed`)
|
this.$toast.success(this.$getString('ToastMetadataFilesRemovedNoneRemoved', [ext]))
|
||||||
} else {
|
} else {
|
||||||
this.$toast.success(`Successfully removed ${data.removed} metadata.${ext} files`)
|
this.$toast.success(this.$getString('ToastMetadataFilesRemovedSuccess', [data.removed, ext]))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove metadata files', error)
|
console.error('Failed to remove metadata files', error)
|
||||||
this.$toast.error('Failed to remove metadata files')
|
this.$toast.error(this.$getString('ToastMetadataFilesRemovedError', [ext]))
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.$emit('update:processing', false)
|
this.$emit('update:processing', false)
|
||||||
|
|
@ -78,4 +78,4 @@ export default {
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,13 @@ export default {
|
||||||
return this.notificationData.events || []
|
return this.notificationData.events || []
|
||||||
},
|
},
|
||||||
eventOptions() {
|
eventOptions() {
|
||||||
return this.notificationEvents.map((e) => ({ value: e.name, text: e.name, subtext: e.description }))
|
return this.notificationEvents.map((e) => {
|
||||||
|
return {
|
||||||
|
value: e.name,
|
||||||
|
text: e.name,
|
||||||
|
subtext: this.$strings[e.descriptionKey] || e.description
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
selectedEventData() {
|
selectedEventData() {
|
||||||
return this.notificationEvents.find((e) => e.name === this.newNotification.eventName)
|
return this.notificationEvents.find((e) => e.name === this.newNotification.eventName)
|
||||||
|
|
@ -86,7 +92,7 @@ export default {
|
||||||
return this.selectedEventData && this.selectedEventData.requiresLibrary
|
return this.selectedEventData && this.selectedEventData.requiresLibrary
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.isNew ? 'Create Notification' : 'Update Notification'
|
return this.isNew ? this.$strings.HeaderNotificationCreate : this.$strings.HeaderNotificationUpdate
|
||||||
},
|
},
|
||||||
availableVariables() {
|
availableVariables() {
|
||||||
return this.selectedEventData ? this.selectedEventData.variables || null : null
|
return this.selectedEventData ? this.selectedEventData.variables || null : null
|
||||||
|
|
@ -104,9 +110,9 @@ export default {
|
||||||
},
|
},
|
||||||
submitForm() {
|
submitForm() {
|
||||||
this.$refs.urlsInput?.forceBlur()
|
this.$refs.urlsInput?.forceBlur()
|
||||||
|
|
||||||
if (!this.newNotification.urls.length) {
|
if (!this.newNotification.urls.length) {
|
||||||
this.$toast.error('Must enter an Apprise URL')
|
this.$toast.error(this.$strings.ToastAppriseUrlRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,12 +133,12 @@ export default {
|
||||||
.$patch(`/api/notifications/${payload.id}`, payload)
|
.$patch(`/api/notifications/${payload.id}`, payload)
|
||||||
.then((updatedSettings) => {
|
.then((updatedSettings) => {
|
||||||
this.$emit('update', updatedSettings)
|
this.$emit('update', updatedSettings)
|
||||||
this.$toast.success('Notification updated')
|
this.$toast.success(this.$strings.ToastNotificationUpdateSuccess)
|
||||||
this.show = false
|
this.show = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update notification', error)
|
console.error('Failed to update notification', error)
|
||||||
this.$toast.error('Failed to update notification')
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|
@ -149,12 +155,11 @@ export default {
|
||||||
.$post('/api/notifications', payload)
|
.$post('/api/notifications', payload)
|
||||||
.then((updatedSettings) => {
|
.then((updatedSettings) => {
|
||||||
this.$emit('update', updatedSettings)
|
this.$emit('update', updatedSettings)
|
||||||
this.$toast.success('Notification created')
|
|
||||||
this.show = false
|
this.show = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to create notification', error)
|
console.error('Failed to create notification', error)
|
||||||
this.$toast.error('Failed to create notification')
|
this.$toast.error(this.$strings.ToastNotificationCreateFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|
|
||||||
|
|
@ -130,12 +130,12 @@ export default {
|
||||||
.$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects })
|
.$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects })
|
||||||
.then((updatedPlaylist) => {
|
.then((updatedPlaylist) => {
|
||||||
console.log(`Items removed from playlist`, updatedPlaylist)
|
console.log(`Items removed from playlist`, updatedPlaylist)
|
||||||
this.$toast.success('Playlist item(s) removed')
|
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove items from playlist', error)
|
console.error('Failed to remove items from playlist', error)
|
||||||
this.$toast.error('Failed to remove playlist item(s)')
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
@ -148,12 +148,12 @@ export default {
|
||||||
.$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects })
|
.$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects })
|
||||||
.then((updatedPlaylist) => {
|
.then((updatedPlaylist) => {
|
||||||
console.log(`Items added to playlist`, updatedPlaylist)
|
console.log(`Items added to playlist`, updatedPlaylist)
|
||||||
this.$toast.success('Items added to playlist')
|
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to add items to playlist', error)
|
console.error('Failed to add items to playlist', error)
|
||||||
this.$toast.error('Failed to add items to playlist')
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
@ -174,14 +174,14 @@ export default {
|
||||||
.$post('/api/playlists', newPlaylist)
|
.$post('/api/playlists', newPlaylist)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log('New playlist created', data)
|
console.log('New playlist created', data)
|
||||||
this.$toast.success(`Playlist "${data.name}" created`)
|
this.$toast.success(this.$strings.ToastPlaylistCreateSuccess + ': ' + data.name)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.newPlaylistName = ''
|
this.newPlaylistName = ''
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to create playlist', error)
|
console.error('Failed to create playlist', error)
|
||||||
var errMsg = error.response ? error.response.data || '' : ''
|
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
|
this.processing = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ export default {
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove playlist', error)
|
console.error('Failed to remove playlist', error)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.$toast.error(this.$strings.ToastPlaylistRemoveFailed)
|
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -95,7 +95,7 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!this.newPlaylistName) {
|
if (!this.newPlaylistName) {
|
||||||
return this.$toast.error('Playlist must have a name')
|
return this.$toast.error(this.$strings.ToastNameRequired)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
@ -115,7 +115,7 @@ export default {
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update playlist', error)
|
console.error('Failed to update playlist', error)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,12 @@ export default {
|
||||||
return this.selectedFolder.fullPath
|
return this.selectedFolder.fullPath
|
||||||
},
|
},
|
||||||
podcastTypes() {
|
podcastTypes() {
|
||||||
return this.$store.state.globals.podcastTypes || []
|
return this.$store.state.globals.podcastTypes.map((e) => {
|
||||||
|
return {
|
||||||
|
text: this.$strings[e.descriptionKey] || e.text,
|
||||||
|
value: e.value
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,23 @@
|
||||||
<p dir="auto" class="text-lg font-semibold mb-6">{{ title }}</p>
|
<p dir="auto" class="text-lg font-semibold mb-6">{{ title }}</p>
|
||||||
<div v-if="description" dir="auto" class="default-style" v-html="description" />
|
<div v-if="description" dir="auto" class="default-style" v-html="description" />
|
||||||
<p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p>
|
<p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p>
|
||||||
|
|
||||||
|
<div class="w-full h-px bg-white/5 my-4" />
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-grow">
|
||||||
|
<p class="font-semibold text-xs mb-1">{{ $strings.LabelFilename }}</p>
|
||||||
|
<p class="mb-2 text-xs">
|
||||||
|
{{ audioFileFilename }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<p class="font-semibold text-xs mb-1">{{ $strings.LabelSize }}</p>
|
||||||
|
<p class="mb-2 text-xs">
|
||||||
|
{{ audioFileSize }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -54,7 +71,7 @@ export default {
|
||||||
return this.episode.description || ''
|
return this.episode.description || ''
|
||||||
},
|
},
|
||||||
media() {
|
media() {
|
||||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
return this.libraryItem?.media || {}
|
||||||
},
|
},
|
||||||
mediaMetadata() {
|
mediaMetadata() {
|
||||||
return this.media.metadata || {}
|
return this.media.metadata || {}
|
||||||
|
|
@ -65,6 +82,14 @@ export default {
|
||||||
podcastAuthor() {
|
podcastAuthor() {
|
||||||
return this.mediaMetadata.author
|
return this.mediaMetadata.author
|
||||||
},
|
},
|
||||||
|
audioFileFilename() {
|
||||||
|
return this.episode.audioFile?.metadata?.filename || ''
|
||||||
|
},
|
||||||
|
audioFileSize() {
|
||||||
|
const size = this.episode.audioFile?.metadata?.size || 0
|
||||||
|
|
||||||
|
return this.$bytesPretty(size)
|
||||||
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-if="enclosureUrl" class="pb-4 pt-6">
|
<div v-if="enclosureUrl" class="pb-4 pt-6">
|
||||||
<ui-text-input-with-label :value="enclosureUrl" readonly class="text-xs">
|
<ui-text-input-with-label :value="enclosureUrl" readonly class="text-xs">
|
||||||
<label class="px-1 text-xs text-gray-200 font-semibold">Episode URL from RSS feed</label>
|
<label class="px-1 text-xs text-gray-200 font-semibold">{{ $strings.LabelEpisodeUrlFromRssFeed }}</label>
|
||||||
</ui-text-input-with-label>
|
</ui-text-input-with-label>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="py-4">
|
<div v-else class="py-4">
|
||||||
<p class="text-xs text-gray-300 font-semibold">Episode not linked to RSS feed episode</p>
|
<p class="text-xs text-gray-300 font-semibold">{{ $strings.LabelEpisodeNotLinkedToRssFeed }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -97,7 +97,12 @@ export default {
|
||||||
return this.enclosure.url
|
return this.enclosure.url
|
||||||
},
|
},
|
||||||
episodeTypes() {
|
episodeTypes() {
|
||||||
return this.$store.state.globals.episodeTypes || []
|
return this.$store.state.globals.episodeTypes.map((e) => {
|
||||||
|
return {
|
||||||
|
text: this.$strings[e.descriptionKey] || e.text,
|
||||||
|
value: e.value
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -142,7 +147,7 @@ export default {
|
||||||
|
|
||||||
const updatedDetails = this.getUpdatePayload()
|
const updatedDetails = this.getUpdatePayload()
|
||||||
if (!Object.keys(updatedDetails).length) {
|
if (!Object.keys(updatedDetails).length) {
|
||||||
this.$toast.info('No changes were made')
|
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return this.updateDetails(updatedDetails)
|
return this.updateDetails(updatedDetails)
|
||||||
|
|
@ -152,14 +157,14 @@ export default {
|
||||||
const updateResult = await this.$axios.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatedDetails).catch((error) => {
|
const updateResult = await this.$axios.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatedDetails).catch((error) => {
|
||||||
console.error('Failed update episode', error)
|
console.error('Failed update episode', error)
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
this.$toast.error(error?.response?.data || 'Failed to update episode')
|
this.$toast.error(error?.response?.data || this.$strings.ToastFailedToUpdate)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
if (updateResult) {
|
if (updateResult) {
|
||||||
if (updateResult) {
|
if (updateResult) {
|
||||||
this.$toast.success('Podcast episode updated')
|
this.$toast.success(this.$strings.ToastItemUpdateSuccess)
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ export default {
|
||||||
}
|
}
|
||||||
const updatePayload = this.getUpdatePayload(episodeData)
|
const updatePayload = this.getUpdatePayload(episodeData)
|
||||||
if (!Object.keys(updatePayload).length) {
|
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)
|
console.log('Episode update payload', updatePayload)
|
||||||
|
|
||||||
|
|
@ -126,7 +126,7 @@ export default {
|
||||||
},
|
},
|
||||||
submitForm() {
|
submitForm() {
|
||||||
if (!this.episodeTitle || !this.episodeTitle.length) {
|
if (!this.episodeTitle || !this.episodeTitle.length) {
|
||||||
this.$toast.error('Must enter an episode title')
|
this.$toast.error(this.$strings.ToastTitleRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.searchedTitle = this.episodeTitle
|
this.searchedTitle = this.episodeTitle
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@
|
||||||
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</p>
|
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</p>
|
||||||
|
|
||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
<ui-text-input v-model="currentFeed.feedUrl" readonly />
|
<ui-text-input :value="feedUrl" readonly />
|
||||||
|
|
||||||
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(currentFeed.feedUrl)">content_copy</span>
|
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feedUrl)">content_copy</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="currentFeed.meta" class="mt-5">
|
<div v-if="currentFeed.meta" class="mt-5">
|
||||||
|
|
@ -111,8 +111,11 @@ export default {
|
||||||
userIsAdminOrUp() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
|
feedUrl() {
|
||||||
|
return this.currentFeed ? `${window.origin}${this.$config.routerBasePath}${this.currentFeed.feedUrl}` : ''
|
||||||
|
},
|
||||||
demoFeedUrl() {
|
demoFeedUrl() {
|
||||||
return `${window.origin}/feed/${this.newFeedSlug}`
|
return `${window.origin}${this.$config.routerBasePath}/feed/${this.newFeedSlug}`
|
||||||
},
|
},
|
||||||
isHttp() {
|
isHttp() {
|
||||||
return window.origin.startsWith('http://')
|
return window.origin.startsWith('http://')
|
||||||
|
|
@ -121,14 +124,14 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
openFeed() {
|
openFeed() {
|
||||||
if (!this.newFeedSlug) {
|
if (!this.newFeedSlug) {
|
||||||
this.$toast.error('Must set a feed slug')
|
this.$toast.error(this.$strings.ToastSlugRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitized = this.$sanitizeSlug(this.newFeedSlug)
|
const sanitized = this.$sanitizeSlug(this.newFeedSlug)
|
||||||
if (this.newFeedSlug !== sanitized) {
|
if (this.newFeedSlug !== sanitized) {
|
||||||
this.newFeedSlug = sanitized
|
this.newFeedSlug = sanitized
|
||||||
this.$toast.warning('Slug had to be modified - Run again')
|
this.$toast.warning(this.$strings.ToastSlugMustChange)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,7 +142,7 @@ export default {
|
||||||
slug: this.newFeedSlug,
|
slug: this.newFeedSlug,
|
||||||
metadataDetails: this.metadataDetails
|
metadataDetails: this.metadataDetails
|
||||||
}
|
}
|
||||||
if (this.$isDev) payload.serverAddress = `http://localhost:3333${this.$config.routerBasePath}`
|
if (this.$isDev) payload.serverAddress = process.env.serverUrl
|
||||||
|
|
||||||
console.log('Payload', payload)
|
console.log('Payload', payload)
|
||||||
this.$axios
|
this.$axios
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedGeneral }}</p>
|
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedGeneral }}</p>
|
||||||
|
|
||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
<ui-text-input v-model="feed.feedUrl" readonly />
|
<ui-text-input :value="feedUrl" readonly />
|
||||||
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feed.feedUrl)">content_copy</span>
|
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feedUrl)">content_copy</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="feed.meta" class="mt-5">
|
<div v-if="feed.meta" class="mt-5">
|
||||||
|
|
@ -70,6 +70,9 @@ export default {
|
||||||
},
|
},
|
||||||
_feed() {
|
_feed() {
|
||||||
return this.feed || {}
|
return this.feed || {}
|
||||||
|
},
|
||||||
|
feedUrl() {
|
||||||
|
return this.feed ? `${window.origin}${this.$config.routerBasePath}${this.feed.feedUrl}` : ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,37 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center pt-4 pb-2 lg:pt-0 lg:pb-2">
|
<div class="flex justify-center pt-4 pb-2 lg:pt-0 lg:pb-2">
|
||||||
<div class="flex-grow" />
|
<div class="flex items-center justify-center flex-grow">
|
||||||
<template v-if="!loading">
|
<template v-if="!loading">
|
||||||
<ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 lg:mr-8">
|
<ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 lg:mr-8">
|
||||||
<button :aria-label="$strings.ButtonPreviousChapter" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
|
<button :aria-label="$strings.ButtonPreviousChapter" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
|
||||||
<span class="material-symbols text-2xl sm:text-3xl">first_page</span>
|
<span class="material-symbols text-2xl sm:text-3xl">first_page</span>
|
||||||
|
</button>
|
||||||
|
</ui-tooltip>
|
||||||
|
<ui-tooltip direction="top" :text="jumpBackwardText">
|
||||||
|
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
||||||
|
<span class="material-symbols text-2xl sm:text-3xl">replay</span>
|
||||||
|
</button>
|
||||||
|
</ui-tooltip>
|
||||||
|
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
||||||
|
<span class="material-symbols fill text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
<ui-tooltip direction="top" :text="jumpForwardText">
|
||||||
<ui-tooltip direction="top" :text="jumpBackwardText">
|
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
||||||
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
<span class="material-symbols text-2xl sm:text-3xl">forward_media</span>
|
||||||
<span class="material-symbols text-2xl sm:text-3xl">replay</span>
|
</button>
|
||||||
</button>
|
</ui-tooltip>
|
||||||
</ui-tooltip>
|
<ui-tooltip direction="top" :text="hasNextLabel" class="ml-4 lg:ml-8">
|
||||||
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
<button :aria-label="hasNextLabel" :disabled="!hasNext" class="text-gray-300 disabled:text-gray-500" @mousedown.prevent @mouseup.prevent @click.stop="next">
|
||||||
<span class="material-symbols fill text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
<span class="material-symbols text-2xl sm:text-3xl">last_page</span>
|
||||||
</button>
|
</button>
|
||||||
<ui-tooltip direction="top" :text="jumpForwardText">
|
</ui-tooltip>
|
||||||
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
</template>
|
||||||
<span class="material-symbols text-2xl sm:text-3xl">forward_media</span>
|
<template v-else>
|
||||||
</button>
|
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
|
||||||
</ui-tooltip>
|
<span class="material-symbols text-2xl">autorenew</span>
|
||||||
<ui-tooltip direction="top" :text="hasNextLabel" class="ml-4 lg:ml-8">
|
</div>
|
||||||
<button :aria-label="hasNextLabel" :disabled="!hasNext" class="text-gray-300 disabled:text-gray-500" @mousedown.prevent @mouseup.prevent @click.stop="next">
|
</template>
|
||||||
<span class="material-symbols text-2xl sm:text-3xl">last_page</span>
|
</div>
|
||||||
</button>
|
|
||||||
</ui-tooltip>
|
|
||||||
<controls-playback-speed-control v-model="playbackRateInput" @input="playbackRateUpdated" @change="playbackRateChanged" />
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
|
|
||||||
<span class="material-symbols text-2xl">autorenew</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="flex-grow" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -41,7 +40,6 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
seekLoading: Boolean,
|
seekLoading: Boolean,
|
||||||
playbackRate: Number,
|
|
||||||
paused: Boolean,
|
paused: Boolean,
|
||||||
hasNextChapter: Boolean,
|
hasNextChapter: Boolean,
|
||||||
hasNextItemInQueue: Boolean
|
hasNextItemInQueue: Boolean
|
||||||
|
|
@ -50,14 +48,6 @@ export default {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
playbackRateInput: {
|
|
||||||
get() {
|
|
||||||
return this.playbackRate
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$emit('update:playbackRate', val)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
jumpForwardText() {
|
jumpForwardText() {
|
||||||
return this.getJumpText('jumpForwardAmount', this.$strings.ButtonJumpForward)
|
return this.getJumpText('jumpForwardAmount', this.$strings.ButtonJumpForward)
|
||||||
},
|
},
|
||||||
|
|
@ -89,15 +79,6 @@ export default {
|
||||||
jumpForward() {
|
jumpForward() {
|
||||||
this.$emit('jumpForward')
|
this.$emit('jumpForward')
|
||||||
},
|
},
|
||||||
playbackRateUpdated(playbackRate) {
|
|
||||||
this.$emit('setPlaybackRate', playbackRate)
|
|
||||||
},
|
|
||||||
playbackRateChanged(playbackRate) {
|
|
||||||
this.$emit('setPlaybackRate', playbackRate)
|
|
||||||
this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => {
|
|
||||||
console.error('Failed to update settings', err)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getJumpText(setting, prefix) {
|
getJumpText(setting, prefix) {
|
||||||
const amount = this.$store.getters['user/getUserSetting'](setting)
|
const amount = this.$store.getters['user/getUserSetting'](setting)
|
||||||
if (!amount) return prefix
|
if (!amount) return prefix
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
<div class="w-full -mt-6">
|
<div class="w-full -mt-6">
|
||||||
<div class="w-full relative mb-1">
|
<div class="w-full relative mb-1">
|
||||||
<div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full">
|
<div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full">
|
||||||
<!-- <span class="material-symbols text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
|
<controls-playback-speed-control v-model="playbackRate" @input="setPlaybackRate" @change="playbackRateChanged" class="mx-2 block" />
|
||||||
|
|
||||||
<ui-tooltip direction="top" :text="$strings.LabelVolume">
|
<ui-tooltip direction="left" :text="$strings.LabelVolume">
|
||||||
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden sm:block" />
|
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden sm:block" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<span v-if="!sleepTimerSet" class="material-symbols text-2xl">snooze</span>
|
<span v-if="!sleepTimerSet" class="material-symbols text-2xl">snooze</span>
|
||||||
<div v-else class="flex items-center">
|
<div v-else class="flex items-center">
|
||||||
<span class="material-symbols text-lg text-warning">snooze</span>
|
<span class="material-symbols text-lg text-warning">snooze</span>
|
||||||
<p class="text-sm sm:text-lg text-warning font-mono font-semibold text-center px-0.5 sm:pb-0.5 sm:min-w-8">{{ sleepTimerRemainingString }}</p>
|
<p class="text-sm sm:text-lg text-warning font-semibold text-center px-0.5 sm:pb-0.5 sm:min-w-8">{{ sleepTimerRemainingString }}</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip direction="top" :text="$strings.LabelViewPlayerSettings">
|
<ui-tooltip direction="top" :text="$strings.LabelViewPlayerSettings">
|
||||||
<button :aria-label="$strings.LabelViewPlayerSettings" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerSettings')">
|
<button :aria-label="$strings.LabelViewPlayerSettings" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="showPlayerSettings">
|
||||||
<span class="material-symbols text-2xl sm:text-2.5xl">settings_slow_motion</span>
|
<span class="material-symbols text-2xl sm:text-2.5xl">settings_slow_motion</span>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
@ -48,18 +48,24 @@
|
||||||
|
|
||||||
<player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" :current-chapter="currentChapter" :playback-rate="playbackRate" @seek="seek" />
|
<player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" :current-chapter="currentChapter" :playback-rate="playbackRate" @seek="seek" />
|
||||||
|
|
||||||
<div class="flex">
|
<div class="relative flex items-center justify-between">
|
||||||
<p ref="currentTimestamp" class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">00:00:00</p>
|
<div class="flex-grow flex items-center">
|
||||||
<p class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto"> / {{ progressPercent }}%</p>
|
<p ref="currentTimestamp" class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">00:00:00</p>
|
||||||
<div class="flex-grow" />
|
<p class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto"> / {{ progressPercent }}%</p>
|
||||||
<p class="text-xs sm:text-sm text-gray-300 pt-0.5 px-2 truncate">
|
</div>
|
||||||
{{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400"> ({{ $getString('LabelPlayerChapterNumberMarker', [currentChapterIndex + 1, chapters.length]) }})</span>
|
<div class="absolute left-1/2 transform -translate-x-1/2">
|
||||||
</p>
|
<p class="text-xs sm:text-sm text-gray-300 pt-0.5 px-2 truncate">
|
||||||
<div class="flex-grow" />
|
{{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400"> ({{ $getString('LabelPlayerChapterNumberMarker', [currentChapterIndex + 1, chapters.length]) }})</span>
|
||||||
<p class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow flex items-center justify-end">
|
||||||
|
<p class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :playback-rate="playbackRate" :chapters="chapters" @select="selectChapter" />
|
<modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :playback-rate="playbackRate" :chapters="chapters" @select="selectChapter" />
|
||||||
|
|
||||||
|
<modals-player-settings-modal v-model="showPlayerSettingsModal" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -92,6 +98,7 @@ export default {
|
||||||
audioEl: null,
|
audioEl: null,
|
||||||
seekLoading: false,
|
seekLoading: false,
|
||||||
showChaptersModal: false,
|
showChaptersModal: false,
|
||||||
|
showPlayerSettingsModal: false,
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
duration: 0
|
duration: 0
|
||||||
}
|
}
|
||||||
|
|
@ -178,22 +185,6 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
toggleFullscreen(isFullscreen) {
|
toggleFullscreen(isFullscreen) {
|
||||||
this.$store.commit('setPlayerIsFullscreen', isFullscreen)
|
this.$store.commit('setPlayerIsFullscreen', isFullscreen)
|
||||||
|
|
||||||
var videoPlayerEl = document.getElementById('video-player')
|
|
||||||
if (videoPlayerEl) {
|
|
||||||
if (isFullscreen) {
|
|
||||||
videoPlayerEl.style.width = '100vw'
|
|
||||||
videoPlayerEl.style.height = '100vh'
|
|
||||||
videoPlayerEl.style.top = '0px'
|
|
||||||
videoPlayerEl.style.left = '0px'
|
|
||||||
} else {
|
|
||||||
videoPlayerEl.style.width = '384px'
|
|
||||||
videoPlayerEl.style.height = '216px'
|
|
||||||
videoPlayerEl.style.top = 'unset'
|
|
||||||
videoPlayerEl.style.bottom = '80px'
|
|
||||||
videoPlayerEl.style.left = '16px'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
setDuration(duration) {
|
setDuration(duration) {
|
||||||
this.duration = duration
|
this.duration = duration
|
||||||
|
|
@ -240,6 +231,12 @@ export default {
|
||||||
this.playbackRate = Number((this.playbackRate - 0.1).toFixed(1))
|
this.playbackRate = Number((this.playbackRate - 0.1).toFixed(1))
|
||||||
this.setPlaybackRate(this.playbackRate)
|
this.setPlaybackRate(this.playbackRate)
|
||||||
},
|
},
|
||||||
|
playbackRateChanged(playbackRate) {
|
||||||
|
this.setPlaybackRate(playbackRate)
|
||||||
|
this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => {
|
||||||
|
console.error('Failed to update settings', err)
|
||||||
|
})
|
||||||
|
},
|
||||||
setPlaybackRate(playbackRate) {
|
setPlaybackRate(playbackRate) {
|
||||||
this.$emit('setPlaybackRate', playbackRate)
|
this.$emit('setPlaybackRate', playbackRate)
|
||||||
},
|
},
|
||||||
|
|
@ -321,6 +318,9 @@ export default {
|
||||||
if (!this.chapters.length) return
|
if (!this.chapters.length) return
|
||||||
this.showChaptersModal = !this.showChaptersModal
|
this.showChaptersModal = !this.showChaptersModal
|
||||||
},
|
},
|
||||||
|
showPlayerSettings() {
|
||||||
|
this.showPlayerSettingsModal = !this.showPlayerSettingsModal
|
||||||
|
},
|
||||||
init() {
|
init() {
|
||||||
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,22 +35,22 @@
|
||||||
<div class="flex justify-between pt-12">
|
<div class="flex justify-between pt-12">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-center">{{ $strings.LabelStatsWeekListening }}</p>
|
<p class="text-sm text-center">{{ $strings.LabelStatsWeekListening }}</p>
|
||||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ totalMinutesListeningThisWeek }}</p>
|
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ $formatNumber(totalMinutesListeningThisWeek) }}</p>
|
||||||
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
|
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-center">{{ $strings.LabelStatsDailyAverage }}</p>
|
<p class="text-sm text-center">{{ $strings.LabelStatsDailyAverage }}</p>
|
||||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ averageMinutesPerDay }}</p>
|
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ $formatNumber(averageMinutesPerDay) }}</p>
|
||||||
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
|
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-center">{{ $strings.LabelStatsBestDay }}</p>
|
<p class="text-sm text-center">{{ $strings.LabelStatsBestDay }}</p>
|
||||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ mostListenedDay }}</p>
|
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ $formatNumber(mostListenedDay) }}</p>
|
||||||
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
|
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-center">{{ $strings.LabelStatsDays }}</p>
|
<p class="text-sm text-center">{{ $strings.LabelStatsDays }}</p>
|
||||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ daysInARow }}</p>
|
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ $formatNumber(daysInARow) }}</p>
|
||||||
<p class="text-sm text-center">{{ $strings.LabelStatsInARow }}</p>
|
<p class="text-sm text-center">{{ $strings.LabelStatsInARow }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
|
<div v-if="processing" role="img" :aria-label="$strings.MessageLoading" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
|
||||||
<widgets-loading-spinner />
|
<widgets-loading-spinner />
|
||||||
</div>
|
</div>
|
||||||
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" />
|
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" :aria-label="$getString('LabelPersonalYearReview', [variant + 1])" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -261,7 +261,7 @@ export default {
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to share', error)
|
console.error('Failed to share', error)
|
||||||
if (error.name !== 'AbortError') {
|
if (error.name !== 'AbortError') {
|
||||||
this.$toast.error('Failed to share: ' + error.message)
|
this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<p class="hidden md:block text-xl font-semibold">{{ $getString('HeaderYearReview', [yearInReviewYear]) }}</p>
|
<h1 class="hidden md:block text-xl font-semibold">{{ $getString('HeaderYearReview', [yearInReviewYear]) }}</h1>
|
||||||
<div class="hidden md:block flex-grow" />
|
<div class="hidden md:block flex-grow" />
|
||||||
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide : $strings.LabelYearReviewShow }}</ui-btn>
|
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide : $strings.LabelYearReviewShow }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -16,17 +16,22 @@
|
||||||
<div v-if="showYearInReview">
|
<div v-if="showYearInReview">
|
||||||
<div class="w-full h-px bg-slate-200/10 my-4" />
|
<div class="w-full h-px bg-slate-200/10 my-4" />
|
||||||
|
|
||||||
<div class="flex items-center justify-center mb-2 max-w-[800px] mx-auto">
|
<div v-if="availableYears.length > 1" class="mb-2 py-2 max-w-[800px] mx-auto">
|
||||||
|
<!-- year selector -->
|
||||||
|
<ui-dropdown v-model="yearInReviewYear" small :items="availableYears" :disabled="processingYearInReview" class="max-w-24" @input="yearInReviewYearChanged" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div role="toolbar" class="flex items-center justify-center mb-2 max-w-[800px] mx-auto">
|
||||||
<!-- previous button -->
|
<!-- previous button -->
|
||||||
<ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--">
|
<ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" :aria-label="$strings.ButtonPrevious" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--">
|
||||||
<span class="material-symbols text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
<span class="material-symbols text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
||||||
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
|
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<!-- share button -->
|
<!-- share button -->
|
||||||
<ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview">{{ $strings.ButtonShare }} </ui-btn>
|
<ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview">{{ $strings.ButtonShare }} </ui-btn>
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelPersonalYearReview', [yearInReviewVariant + 1]) }}</p>
|
<h2 class="hidden sm:block text-lg font-semibold">{{ $getString('LabelPersonalYearReview', [yearInReviewVariant + 1]) }}</h2>
|
||||||
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p>
|
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
|
|
@ -36,7 +41,7 @@
|
||||||
<span class="material-symbols sm:!hidden text-lg py-px">refresh</span>
|
<span class="material-symbols sm:!hidden text-lg py-px">refresh</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<!-- next button -->
|
<!-- next button -->
|
||||||
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
|
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" :aria-label="$strings.ButtonNext" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
|
||||||
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
|
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
|
||||||
<span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
<span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
@ -46,23 +51,23 @@
|
||||||
<!-- your year in review short -->
|
<!-- your year in review short -->
|
||||||
<div class="w-full max-w-[800px] mx-auto my-4">
|
<div class="w-full max-w-[800px] mx-auto my-4">
|
||||||
<!-- share button -->
|
<!-- share button -->
|
||||||
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex sm:hidden items-center font-semibold mb-1" @click="shareYearInReviewShort">{{ $strings.ButtonShare }}</ui-btn>
|
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex items-center font-semibold mb-1" @click="shareYearInReviewShort">{{ $strings.ButtonShare }}</ui-btn>
|
||||||
<stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" />
|
<stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- your server in review -->
|
<!-- your server in review -->
|
||||||
<div v-if="isAdminOrUp" class="w-full max-w-[800px] mx-auto mb-2 mt-4 border-t pt-4 border-white/10">
|
<div v-if="isAdminOrUp" role="toolbar" class="w-full max-w-[800px] mx-auto mb-2 mt-4 border-t pt-4 border-white/10">
|
||||||
<div class="flex items-center justify-center mb-2">
|
<div class="flex items-center justify-center mb-2">
|
||||||
<!-- previous button -->
|
<!-- previous button -->
|
||||||
<ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--">
|
<ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" :aria-label="$strings.ButtonPrevious" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--">
|
||||||
<span class="material-symbols text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
<span class="material-symbols text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
||||||
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
|
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<!-- share button -->
|
<!-- share button -->
|
||||||
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer">{{ $strings.ButtonShare }} </ui-btn>
|
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer">{{ $strings.ButtonShare }} </ui-btn>
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelServerYearReview', [yearInReviewServerVariant + 1]) }}</p>
|
<h2 class="hidden sm:block text-lg font-semibold">{{ $getString('LabelServerYearReview', [yearInReviewServerVariant + 1]) }}</h2>
|
||||||
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p>
|
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
|
|
@ -72,7 +77,7 @@
|
||||||
<span class="material-symbols sm:!hidden text-lg py-px">refresh</span>
|
<span class="material-symbols sm:!hidden text-lg py-px">refresh</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<!-- next button -->
|
<!-- next button -->
|
||||||
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
|
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" :aria-label="$strings.ButtonNext" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
|
||||||
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
|
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
|
||||||
<span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
<span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
@ -88,6 +93,7 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showYearInReview: false,
|
showYearInReview: false,
|
||||||
|
availableYears: [],
|
||||||
yearInReviewYear: 0,
|
yearInReviewYear: 0,
|
||||||
yearInReviewVariant: 0,
|
yearInReviewVariant: 0,
|
||||||
yearInReviewServerVariant: 0,
|
yearInReviewServerVariant: 0,
|
||||||
|
|
@ -100,6 +106,9 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
isAdminOrUp() {
|
isAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
|
},
|
||||||
|
user() {
|
||||||
|
return this.$store.state.user.user
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -112,25 +121,57 @@ export default {
|
||||||
shareYearInReviewShort() {
|
shareYearInReviewShort() {
|
||||||
this.$refs.yearInReviewShort.share()
|
this.$refs.yearInReviewShort.share()
|
||||||
},
|
},
|
||||||
|
yearInReviewYearChanged() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.refreshYearInReview()
|
||||||
|
this.refreshYearInReviewServer()
|
||||||
|
})
|
||||||
|
},
|
||||||
refreshYearInReviewServer() {
|
refreshYearInReviewServer() {
|
||||||
this.$refs.yearInReviewServer.refresh()
|
if (this.$refs.yearInReviewServer != null) {
|
||||||
|
this.$refs.yearInReviewServer.refresh()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
refreshYearInReview() {
|
refreshYearInReview() {
|
||||||
this.$refs.yearInReview.refresh()
|
if (this.$refs.yearInReview != null && this.$refs.yearInReviewShort != null) {
|
||||||
this.$refs.yearInReviewShort.refresh()
|
this.$refs.yearInReview.refresh()
|
||||||
|
this.$refs.yearInReviewShort.refresh()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
clickShowYearInReview() {
|
clickShowYearInReview() {
|
||||||
this.showYearInReview = !this.showYearInReview
|
this.showYearInReview = !this.showYearInReview
|
||||||
|
},
|
||||||
|
getAvailableYears() {
|
||||||
|
if (this.user) {
|
||||||
|
const oldestDate = this.user.createdAt
|
||||||
|
if (oldestDate) {
|
||||||
|
const date = new Date(oldestDate)
|
||||||
|
const oldestYear = date.getFullYear()
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
|
||||||
|
const years = []
|
||||||
|
for (let year = currentYear; year >= oldestYear; year--) {
|
||||||
|
years.push({ value: year, text: year.toString() })
|
||||||
|
}
|
||||||
|
|
||||||
|
return years
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback on error
|
||||||
|
return [{ value: this.yearInReviewYear, text: this.yearInReviewYear.toString() }]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
this.yearInReviewYear = new Date().getFullYear()
|
this.yearInReviewYear = new Date().getFullYear()
|
||||||
|
|
||||||
// When not December show previous year
|
// When not December show previous year
|
||||||
if (new Date().getMonth() < 11) {
|
if (new Date().getMonth() < 11) {
|
||||||
this.yearInReviewYear--
|
this.yearInReviewYear--
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.availableYears = this.getAvailableYears()
|
||||||
|
|
||||||
if (typeof navigator.share !== 'undefined' && navigator.share) {
|
if (typeof navigator.share !== 'undefined' && navigator.share) {
|
||||||
this.showShareButton = true
|
this.showShareButton = true
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
|
<div v-if="processing" role="img" :aria-label="$strings.MessageLoading" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
|
||||||
<widgets-loading-spinner />
|
<widgets-loading-spinner />
|
||||||
</div>
|
</div>
|
||||||
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" />
|
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" :aria-label="$getString('LabelServerYearReview', [variant + 1])" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -237,7 +237,7 @@ export default {
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to share', error)
|
console.error('Failed to share', error)
|
||||||
if (error.name !== 'AbortError') {
|
if (error.name !== 'AbortError') {
|
||||||
this.$toast.error('Failed to share: ' + error.message)
|
this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ export default {
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to share', error)
|
console.error('Failed to share', error)
|
||||||
if (error.name !== 'AbortError') {
|
if (error.name !== 'AbortError') {
|
||||||
this.$toast.error('Failed to share: ' + error.message)
|
this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@ export default {
|
||||||
mounted() {
|
mounted() {
|
||||||
this.loadBackups()
|
this.loadBackups()
|
||||||
if (this.$route.query.backup) {
|
if (this.$route.query.backup) {
|
||||||
this.$toast.success('Backup applied successfully')
|
this.$toast.success(this.$strings.ToastBackupAppliedSuccess)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ export default {
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update collection', error)
|
console.error('Failed to update collection', error)
|
||||||
this.$toast.error('Failed to save collection books order')
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
editBook(book) {
|
editBook(book) {
|
||||||
|
|
@ -110,4 +110,4 @@ export default {
|
||||||
.collection-book-leave-active {
|
.collection-book-leave-active {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
removeProvider(provider) {
|
removeProvider(provider) {
|
||||||
const payload = {
|
const payload = {
|
||||||
message: `Are you sure you want remove custom metadata provider "${provider.name}"?`,
|
message: this.$getString('MessageConfirmDeleteMetadataProvider', [provider.name]),
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.$emit('update:processing', true)
|
this.$emit('update:processing', true)
|
||||||
|
|
@ -53,12 +53,12 @@ export default {
|
||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/custom-metadata-providers/${provider.id}`)
|
.$delete(`/api/custom-metadata-providers/${provider.id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Provider removed')
|
this.$toast.success(this.$strings.ToastProviderRemoveSuccess)
|
||||||
this.$emit('removed', provider.id)
|
this.$emit('removed', provider.id)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove provider', error)
|
console.error('Failed to remove provider', error)
|
||||||
this.$toast.error('Failed to remove provider')
|
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.$emit('update:processing', false)
|
this.$emit('update:processing', false)
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ export default {
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update playlist', error)
|
console.error('Failed to update playlist', error)
|
||||||
this.$toast.error('Failed to save playlist items order')
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
|
|
@ -119,4 +119,4 @@ export default {
|
||||||
.playlist-item-leave-active {
|
.playlist-item-leave-active {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ export default {
|
||||||
this.users = res.users.sort((a, b) => {
|
this.users = res.users.sort((a, b) => {
|
||||||
return a.createdAt - b.createdAt
|
return a.createdAt - b.createdAt
|
||||||
})
|
})
|
||||||
|
this.$emit('numUsers', this.users.length)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,7 @@ export default {
|
||||||
var newOrder = libraryOrderData.map((lib) => lib.id).join(',')
|
var newOrder = libraryOrderData.map((lib) => lib.id).join(',')
|
||||||
if (currOrder !== newOrder) {
|
if (currOrder !== newOrder) {
|
||||||
this.$axios.$post('/api/libraries/order', libraryOrderData).then((response) => {
|
this.$axios.$post('/api/libraries/order', libraryOrderData).then((response) => {
|
||||||
if (response.libraries && response.libraries.length) {
|
if (response.libraries?.length) {
|
||||||
this.$toast.success('Library order saved', { timeout: 1500 })
|
|
||||||
this.$store.commit('libraries/set', response.libraries)
|
this.$store.commit('libraries/set', response.libraries)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -110,4 +109,4 @@ export default {
|
||||||
this.$store.commit('libraries/removeListener', 'libraries-table')
|
this.$store.commit('libraries/removeListener', 'libraries-table')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -218,12 +218,12 @@ export default {
|
||||||
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
|
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
|
||||||
} else {
|
} else {
|
||||||
console.log(`Item removed from playlist`, updatedPlaylist)
|
console.log(`Item removed from playlist`, updatedPlaylist)
|
||||||
this.$toast.success('Item removed from playlist')
|
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove item from playlist', error)
|
console.error('Failed to remove item from playlist', error)
|
||||||
this.$toast.error('Failed to remove item from playlist')
|
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processingRemove = false
|
this.processingRemove = false
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="h-8 flex items-center">
|
<div class="h-8 flex items-center">
|
||||||
<div class="w-full inline-flex justify-between max-w-xl">
|
<div class="w-full inline-flex justify-between max-w-xl">
|
||||||
<p v-if="episode?.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p>
|
<p v-if="episode?.season" class="text-sm text-gray-300">{{ $getString('LabelSeasonNumber', [episode.season]) }}</p>
|
||||||
<p v-if="episode?.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p>
|
<p v-if="episode?.episode" class="text-sm text-gray-300">{{ $getString('LabelEpisodeNumber', [episode.episode]) }}</p>
|
||||||
<p v-if="episode?.chapters?.length" class="text-sm text-gray-300">{{ episode.chapters.length }} Chapters</p>
|
<p v-if="episode?.chapters?.length" class="text-sm text-gray-300">{{ $getString('LabelChapterCount', [episode.chapters.length]) }}</p>
|
||||||
<p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, dateFormat) }}</p>
|
<p v-if="publishedAt" class="text-sm text-gray-300">{{ $getString('LabelPublishedDate', [$formatDate(publishedAt, dateFormat)]) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -132,13 +132,13 @@ export default {
|
||||||
return this.store.state.streamIsPlaying && this.isStreaming
|
return this.store.state.streamIsPlaying && this.isStreaming
|
||||||
},
|
},
|
||||||
timeRemaining() {
|
timeRemaining() {
|
||||||
if (this.streamIsPlaying) return 'Playing'
|
if (this.streamIsPlaying) return this.$strings.ButtonPlaying
|
||||||
if (!this.itemProgress) return this.$elapsedPretty(this.episode?.duration || 0)
|
if (!this.itemProgress) return this.$elapsedPretty(this.episode?.duration || 0)
|
||||||
if (this.userIsFinished) return 'Finished'
|
if (this.userIsFinished) return this.$strings.LabelFinished
|
||||||
|
|
||||||
const duration = this.itemProgress.duration || this.episode?.duration || 0
|
const duration = this.itemProgress.duration || this.episode?.duration || 0
|
||||||
const remaining = Math.floor(duration - this.itemProgress.currentTime)
|
const remaining = Math.floor(duration - this.itemProgress.currentTime)
|
||||||
return `${this.$elapsedPretty(remaining)} left`
|
return this.$getString('LabelTimeLeft', [this.$elapsedPretty(remaining)])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -182,7 +182,7 @@ export default {
|
||||||
toggleFinished(confirmed = false) {
|
toggleFinished(confirmed = false) {
|
||||||
if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) {
|
if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) {
|
||||||
const payload = {
|
const payload = {
|
||||||
message: `Are you sure you want to mark "${this.episodeTitle}" as finished?`,
|
message: this.$getString('MessageConfirmMarkItemFinished', [this.episodeTitle]),
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.toggleFinished(true)
|
this.toggleFinished(true)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p> -->
|
|
||||||
<div v-if="episodes.length" class="w-full py-3 mx-auto flex">
|
<div v-if="episodes.length" class="w-full py-3 mx-auto flex">
|
||||||
<form @submit.prevent="submit" class="flex flex-grow">
|
<form @submit.prevent="submit" class="flex flex-grow">
|
||||||
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
|
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
|
||||||
|
|
@ -93,17 +92,18 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
contextMenuItems() {
|
contextMenuItems() {
|
||||||
if (!this.userIsAdminOrUp) return []
|
const menuItems = []
|
||||||
return [
|
if (this.userIsAdminOrUp) {
|
||||||
{
|
menuItems.push({
|
||||||
text: 'Quick match all episodes',
|
text: this.$strings.MessageQuickMatchAllEpisodes,
|
||||||
action: 'quick-match-episodes'
|
action: 'quick-match-episodes'
|
||||||
},
|
})
|
||||||
{
|
}
|
||||||
text: this.allEpisodesFinished ? this.$strings.MessageMarkAllEpisodesNotFinished : this.$strings.MessageMarkAllEpisodesFinished,
|
menuItems.push({
|
||||||
action: 'batch-mark-as-finished'
|
text: this.allEpisodesFinished ? this.$strings.MessageMarkAllEpisodesNotFinished : this.$strings.MessageMarkAllEpisodesFinished,
|
||||||
}
|
action: 'batch-mark-as-finished'
|
||||||
]
|
})
|
||||||
|
return menuItems
|
||||||
},
|
},
|
||||||
sortItems() {
|
sortItems() {
|
||||||
return [
|
return [
|
||||||
|
|
@ -261,21 +261,21 @@ export default {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
message: 'Quick matching episodes will overwrite details if a match is found. Only unmatched episodes will be updated. Are you sure?',
|
message: this.$strings.MessageConfirmQuickMatchEpisodes,
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/podcasts/${this.libraryItem.id}/match-episodes?override=1`)
|
.$post(`/api/podcasts/${this.libraryItem.id}/match-episodes?override=1`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.numEpisodesUpdated) {
|
if (data.numEpisodesUpdated) {
|
||||||
this.$toast.success(`${data.numEpisodesUpdated} episodes updated`)
|
this.$toast.success(this.$getString('ToastEpisodeUpdateSuccess', [data.numEpisodesUpdated]))
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info('No changes were made')
|
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to request match episodes', error)
|
console.error('Failed to request match episodes', error)
|
||||||
this.$toast.error('Failed to match episodes')
|
this.$toast.error(this.$strings.ToastFailedToMatch)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|
@ -295,7 +295,7 @@ export default {
|
||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: this.mediaMetadata.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,
|
duration: episode.audioFile.duration || null,
|
||||||
coverPath: this.media.coverPath || null
|
coverPath: this.media.coverPath || null
|
||||||
}
|
}
|
||||||
|
|
@ -372,7 +372,7 @@ export default {
|
||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: this.mediaMetadata.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,
|
duration: episode.audioFile.duration || null,
|
||||||
coverPath: this.media.coverPath || null
|
coverPath: this.media.coverPath || null
|
||||||
})
|
})
|
||||||
|
|
@ -514,6 +514,10 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
filterSortChanged() {
|
filterSortChanged() {
|
||||||
|
// Save filterKey and sortKey to local storage
|
||||||
|
localStorage.setItem('podcastEpisodesFilter', this.filterKey)
|
||||||
|
localStorage.setItem('podcastEpisodesSortBy', this.sortKey + (this.sortDesc ? '-desc' : ''))
|
||||||
|
|
||||||
this.init()
|
this.init()
|
||||||
},
|
},
|
||||||
refresh() {
|
refresh() {
|
||||||
|
|
@ -536,6 +540,11 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.filterKey = localStorage.getItem('podcastEpisodesFilter') || 'incomplete'
|
||||||
|
const sortBy = localStorage.getItem('podcastEpisodesSortBy') || 'publishedAt-desc'
|
||||||
|
this.sortKey = sortBy.split('-')[0]
|
||||||
|
this.sortDesc = sortBy.split('-')[1] === 'desc'
|
||||||
|
|
||||||
this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
|
this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
|
||||||
this.initListeners()
|
this.initListeners()
|
||||||
this.init()
|
this.init()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
|
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
|
||||||
<slot :disabled="disabled" :showMenu="showMenu" :clickShowMenu="clickShowMenu" :processing="processing">
|
<slot :disabled="disabled" :showMenu="showMenu" :clickShowMenu="clickShowMenu" :processing="processing">
|
||||||
<button v-if="!processing" type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
<button v-if="!processing" type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" :aria-label="$strings.LabelMore" aria-haspopup="menu" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||||
<span class="material-symbols text-2xl" :class="iconClass"></span>
|
<span class="material-symbols text-2xl" :class="iconClass"></span>
|
||||||
</button>
|
</button>
|
||||||
<div v-else class="h-full w-full flex items-center justify-center">
|
<div v-else class="h-full w-full flex items-center justify-center">
|
||||||
|
|
@ -10,12 +10,12 @@
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<transition name="menu">
|
<transition name="menu">
|
||||||
<div v-show="showMenu" ref="menuWrapper" class="absolute right-0 mt-1 z-10 bg-bg border border-black-200 shadow-lg rounded-md py-1 focus:outline-none sm:text-sm" :style="{ width: menuWidth + 'px' }">
|
<div v-show="showMenu" ref="menuWrapper" role="menu" class="absolute right-0 mt-1 z-10 bg-bg border border-black-200 shadow-lg rounded-md py-1 focus:outline-none sm:text-sm" :style="{ width: menuWidth + 'px' }">
|
||||||
<template v-for="(item, index) in items">
|
<template v-for="(item, index) in items">
|
||||||
<template v-if="item.subitems">
|
<template v-if="item.subitems">
|
||||||
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-default" :class="{ 'bg-white/5': mouseoverItemIndex == index }" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop>
|
<button :key="index" role="menuitem" aria-haspopup="menu" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-default w-full" :class="{ 'bg-white/5': mouseoverItemIndex == index }" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop>
|
||||||
<p>{{ item.text }}</p>
|
<p>{{ item.text }}</p>
|
||||||
</div>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-if="mouseoverItemIndex === index"
|
v-if="mouseoverItemIndex === index"
|
||||||
:key="`subitems-${index}`"
|
:key="`subitems-${index}`"
|
||||||
|
|
@ -25,14 +25,14 @@
|
||||||
:class="openSubMenuLeft ? 'rounded-l-md' : 'rounded-r-md'"
|
:class="openSubMenuLeft ? 'rounded-l-md' : 'rounded-r-md'"
|
||||||
:style="{ left: submenuLeftPos + 'px', top: index * 28 + 'px', width: submenuWidth + 'px' }"
|
:style="{ left: submenuLeftPos + 'px', top: index * 28 + 'px', width: submenuWidth + 'px' }"
|
||||||
>
|
>
|
||||||
<div v-for="(subitem, subitemindex) in item.subitems" :key="`subitem-${subitemindex}`" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer" @click.stop="clickAction(subitem.action, subitem.data)">
|
<button v-for="(subitem, subitemindex) in item.subitems" :key="`subitem-${subitemindex}`" role="menuitem" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer w-full" @click.stop="clickAction(subitem.action, subitem.data)">
|
||||||
<p>{{ subitem.text }}</p>
|
<p>{{ subitem.text }}</p>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.action)">
|
<button v-else :key="index" role="menuitem" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer w-full" @click.stop="clickAction(item.action)">
|
||||||
<p class="text-left">{{ item.text }}</p>
|
<p class="text-left">{{ item.text }}</p>
|
||||||
</div>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="relative w-full" v-click-outside="clickOutsideObj">
|
<div class="relative w-full" v-click-outside="clickOutsideObj">
|
||||||
<p v-if="label" class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
<p v-if="label" class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
||||||
<button type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
<button type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left sm:text-sm" :class="buttonClass" aria-haspopup="menu" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small }">{{ selectedText }}</span>
|
<span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small }">{{ selectedText }}</span>
|
||||||
<span v-if="selectedSubtext">: </span>
|
<span v-if="selectedSubtext">: </span>
|
||||||
|
|
@ -13,9 +13,9 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<transition name="menu">
|
<transition name="menu">
|
||||||
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="listbox" :style="{ maxHeight: menuMaxHeight }">
|
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="menu" :style="{ maxHeight: menuMaxHeight }">
|
||||||
<template v-for="item in itemsToShow">
|
<template v-for="item in itemsToShow">
|
||||||
<li :key="item.value" class="text-gray-100 relative py-2 cursor-pointer hover:bg-black-400" :id="'listbox-option-' + item.value" role="option" tabindex="0" @keyup.enter="clickedOption(item.value)" @click="clickedOption(item.value)">
|
<li :key="item.value" class="text-gray-100 relative py-2 cursor-pointer hover:bg-black-400" role="menuitem" tabindex="0" @keyup.enter="clickedOption(item.value)" @click="clickedOption(item.value)">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="ml-3 block truncate font-sans text-sm" :class="{ 'font-semibold': item.subtext }">{{ item.text }}</span>
|
<span class="ml-3 block truncate font-sans text-sm" :class="{ 'font-semibold': item.subtext }">{{ item.text }}</span>
|
||||||
<span v-if="item.subtext">: </span>
|
<span v-if="item.subtext">: </span>
|
||||||
|
|
@ -119,4 +119,4 @@ export default {
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<button class="icon-btn rounded-md flex items-center justify-center relative" @mousedown.prevent :disabled="disabled || loading" :class="className" @click="clickBtn">
|
<button :aria-label="ariaLabel" class="icon-btn rounded-md flex items-center justify-center relative" @mousedown.prevent :disabled="disabled || loading" :class="className" @click="clickBtn">
|
||||||
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
||||||
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||||
|
|
@ -28,7 +28,8 @@ export default {
|
||||||
size: {
|
size: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 9
|
default: 9
|
||||||
}
|
},
|
||||||
|
ariaLabel: String
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
type="button"
|
type="button"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
class="w-10 sm:w-full relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200"
|
class="w-10 sm:w-full relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200"
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="menu"
|
||||||
:aria-expanded="showMenu"
|
:aria-expanded="showMenu"
|
||||||
:aria-label="$strings.ButtonLibrary + ': ' + currentLibrary.name"
|
:aria-label="$strings.ButtonLibrary + ': ' + currentLibrary.name"
|
||||||
@click.stop.prevent="clickShowMenu"
|
@click.stop.prevent="clickShowMenu"
|
||||||
|
|
@ -16,9 +16,9 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<transition name="menu">
|
<transition name="menu">
|
||||||
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full min-w-48 bg-primary border border-black-200 shadow-lg rounded-b-md py-1 overflow-auto focus:outline-none sm:text-sm librariesDropdownMenu" tabindex="-1" role="listbox">
|
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full min-w-48 bg-primary border border-black-200 shadow-lg rounded-b-md py-1 overflow-auto focus:outline-none sm:text-sm librariesDropdownMenu" tabindex="-1" role="menu">
|
||||||
<template v-for="library in librariesFiltered">
|
<template v-for="library in librariesFiltered">
|
||||||
<li :key="library.id" class="text-gray-400 hover:text-white relative py-2 cursor-pointer hover:bg-black-400" role="option" tabindex="0" @keydown.enter="selectLibrary(library)" @click="selectLibrary(library)">
|
<li :key="library.id" class="text-gray-400 hover:text-white relative py-2 cursor-pointer hover:bg-black-400" role="menuitem" tabindex="0" @keydown.enter="selectLibrary(library)" @click="selectLibrary(library)">
|
||||||
<div class="flex items-center px-2">
|
<div class="flex items-center px-2">
|
||||||
<ui-library-icon :icon="library.icon" class="mr-1.5" />
|
<ui-library-icon :icon="library.icon" class="mr-1.5" />
|
||||||
<span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span>
|
<span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span>
|
||||||
|
|
|
||||||
|
|
@ -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 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>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -17,7 +17,12 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
text: {
|
text: {
|
||||||
type: String,
|
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);
|
transform: translate(24px, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p>
|
<label :for="identifier" class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</label>
|
||||||
<div ref="wrapper" class="relative">
|
<div ref="wrapper" class="relative">
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
<div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-1" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
<div ref="inputWrapper" role="list" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-1" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
||||||
<div v-for="item in selected" :key="item" class="rounded-full px-2 py-1 mx-0.5 my-0.5 text-xs bg-bg flex flex-nowrap break-all items-center relative">
|
<div v-for="item in selected" :key="item" role="listitem" class="rounded-full px-2 py-1 mx-0.5 my-0.5 text-xs bg-bg flex flex-nowrap break-all items-center relative">
|
||||||
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 px-1 bg-bg bg-opacity-75 flex items-center justify-end opacity-0 hover:opacity-100">
|
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 px-1 bg-bg bg-opacity-75 flex items-center justify-end opacity-0 hover:opacity-100" :class="{ 'opacity-100': inputFocused }">
|
||||||
<span v-if="showEdit" class="material-symbols text-white hover:text-warning cursor-pointer" style="font-size: 1.1rem" @click.stop="editItem(item)">edit</span>
|
<button v-if="showEdit" type="button" :aria-label="$strings.ButtonEdit" class="material-symbols text-white hover:text-warning cursor-pointer" style="font-size: 1.1rem" @click.stop="editItem(item)">edit</button>
|
||||||
<span class="material-symbols text-white hover:text-error cursor-pointer" style="font-size: 1.1rem" @click.stop="removeItem(item)">close</span>
|
<button type="button" :aria-label="$strings.ButtonRemove" class="material-symbols text-white hover:text-error focus:text-error cursor-pointer" style="font-size: 1.1rem" @click.stop="removeItem(item)" @keydown.enter.stop.prevent="removeItem(item)" @focus="setInputFocused(true)" @blur="setInputFocused(false)" tabindex="0">close</button>
|
||||||
</div>
|
</div>
|
||||||
{{ item }}
|
{{ item }}
|
||||||
</div>
|
</div>
|
||||||
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
|
<input v-show="!readonly" v-model="textInput" ref="input" :id="identifier" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
@ -66,7 +66,8 @@ export default {
|
||||||
typingTimeout: null,
|
typingTimeout: null,
|
||||||
isFocused: false,
|
isFocused: false,
|
||||||
menu: null,
|
menu: null,
|
||||||
filteredItems: null
|
filteredItems: null,
|
||||||
|
inputFocused: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
@ -100,6 +101,9 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.filteredItems
|
return this.filteredItems
|
||||||
|
},
|
||||||
|
identifier() {
|
||||||
|
return Math.random().toString(36).substring(2)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -129,6 +133,9 @@ export default {
|
||||||
}, 100)
|
}, 100)
|
||||||
this.setInputWidth()
|
this.setInputWidth()
|
||||||
},
|
},
|
||||||
|
setInputFocused(focused) {
|
||||||
|
this.inputFocused = focused
|
||||||
|
},
|
||||||
setInputWidth() {
|
setInputWidth() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
var value = this.$refs.input.value
|
var value = this.$refs.input.value
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p>
|
<label :for="identifier" class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</label>
|
||||||
<div ref="wrapper" class="relative">
|
<div ref="wrapper" class="relative">
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
<div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-0.5" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
<div ref="inputWrapper" role="list" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-0.5" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
||||||
<div v-for="item in selected" :key="item.id" class="rounded-full px-2 py-0.5 m-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center justify-center relative min-w-12">
|
<div v-for="item in selected" :key="item.id" role="listitem" class="rounded-full px-2 py-0.5 m-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center justify-center relative min-w-12">
|
||||||
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer">
|
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer" :class="{ 'opacity-100': inputFocused }">
|
||||||
<span v-if="showEdit" class="material-symbols text-base text-white hover:text-warning mr-1" @click.stop="editItem(item)">edit</span>
|
<button v-if="showEdit" type="button" :aria-label="$strings.ButtonEdit" class="material-symbols text-base text-white hover:text-warning focus:text-warning mr-1" @click.stop="editItem(item)" @keydown.enter.stop.prevent="editItem(item)" @focus="setInputFocused(true)" @blur="setInputFocused(false)" tabindex="0">edit</button>
|
||||||
<span class="material-symbols text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item.id)">close</span>
|
<button type="button" :aria-label="$strings.ButtonRemove" class="material-symbols text-white hover:text-error focus:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item.id)" @keydown.enter.stop="removeItem(item.id)" @focus="setInputFocused(true)" @blur="setInputFocused(false)" tabindex="0">close</button>
|
||||||
</div>
|
</div>
|
||||||
{{ item[textKey] }}
|
{{ item[textKey] }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showEdit && !disabled" class="rounded-full cursor-pointer w-6 h-6 mx-0.5 bg-bg flex items-center justify-center">
|
<div v-if="showEdit && !disabled" class="rounded-full cursor-pointer w-6 h-6 mx-0.5 bg-bg flex items-center justify-center">
|
||||||
<span class="material-symbols text-white hover:text-success pt-px pr-px" style="font-size: 1.1rem" @click.stop="addItem">add</span>
|
<button type="button" :aria-label="$strings.ButtonAdd" class="material-symbols text-white hover:text-success focus:text-success pt-px pr-px" style="font-size: 1.1rem" @click.stop="addItem" @keydown.enter.stop="addItem" tabindex="0">add</button>
|
||||||
</div>
|
</div>
|
||||||
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
|
<input v-show="!readonly" v-model="textInput" ref="input" :id="identifier" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
@ -65,6 +65,7 @@ export default {
|
||||||
currentSearch: null,
|
currentSearch: null,
|
||||||
typingTimeout: null,
|
typingTimeout: null,
|
||||||
isFocused: false,
|
isFocused: false,
|
||||||
|
inputFocused: false,
|
||||||
menu: null,
|
menu: null,
|
||||||
items: []
|
items: []
|
||||||
}
|
}
|
||||||
|
|
@ -102,6 +103,9 @@ export default {
|
||||||
},
|
},
|
||||||
filterData() {
|
filterData() {
|
||||||
return this.$store.state.libraries.filterData || {}
|
return this.$store.state.libraries.filterData || {}
|
||||||
|
},
|
||||||
|
identifier() {
|
||||||
|
return Math.random().toString(36).substring(2)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -114,6 +118,9 @@ export default {
|
||||||
getIsSelected(itemValue) {
|
getIsSelected(itemValue) {
|
||||||
return !!this.selected.find((i) => i.id === itemValue)
|
return !!this.selected.find((i) => i.id === itemValue)
|
||||||
},
|
},
|
||||||
|
setInputFocused(focused) {
|
||||||
|
this.inputFocused = focused
|
||||||
|
},
|
||||||
search() {
|
search() {
|
||||||
if (!this.textInput) return
|
if (!this.textInput) return
|
||||||
this.currentSearch = this.textInput
|
this.currentSearch = this.textInput
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<button class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" :class="borderless ? '' : 'bg-primary border border-gray-600'" @click="clickBtn">
|
<button :aria-label="isRead ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" :class="borderless ? '' : 'bg-primary border border-gray-600'" @click="clickBtn">
|
||||||
<div class="w-5 h-5 text-white relative">
|
<div class="w-5 h-5 text-white relative">
|
||||||
<svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
|
<svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
|
||||||
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
|
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
|
||||||
|
|
@ -24,12 +24,12 @@ export default {
|
||||||
computed: {},
|
computed: {},
|
||||||
methods: {
|
methods: {
|
||||||
clickBtn(e) {
|
clickBtn(e) {
|
||||||
|
e.stopPropagation()
|
||||||
if (this.disabled) {
|
if (this.disabled) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$emit('click')
|
this.$emit('click')
|
||||||
e.stopPropagation()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|
@ -54,4 +54,4 @@ button.icon-btn:hover:not(:disabled)::before {
|
||||||
button.icon-btn:disabled::before {
|
button.icon-btn:disabled::before {
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@ export default {
|
||||||
inputName: String,
|
inputName: String,
|
||||||
showCopy: Boolean,
|
showCopy: Boolean,
|
||||||
step: [String, Number],
|
step: [String, Number],
|
||||||
min: [String, Number]
|
min: [String, Number],
|
||||||
|
customInputClass: String
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -82,6 +83,7 @@ export default {
|
||||||
_list.push(`py-${this.paddingY}`)
|
_list.push(`py-${this.paddingY}`)
|
||||||
if (this.noSpinner) _list.push('no-spinner')
|
if (this.noSpinner) _list.push('no-spinner')
|
||||||
if (this.textCenter) _list.push('text-center')
|
if (this.textCenter) _list.push('text-center')
|
||||||
|
if (this.customInputClass) _list.push(this.customInputClass)
|
||||||
return _list.join(' ')
|
return _list.join(' ')
|
||||||
},
|
},
|
||||||
actualType() {
|
actualType() {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<button :aria-labelledby="labeledBy" role="checkbox" type="button" class="border rounded-full border-black-100 flex items-center cursor-pointer w-10 justify-start" :aria-checked="toggleValue" :class="className" @click="clickToggle">
|
<button :aria-labelledby="labeledBy" :aria-label="label" role="checkbox" type="button" class="border rounded-full border-black-100 flex items-center cursor-pointer justify-start" :style="{ width: buttonWidth + 'px' }" :aria-checked="toggleValue" :class="className" @click="clickToggle">
|
||||||
<span class="rounded-full border w-5 h-5 border-black-50 shadow transform transition-transform duration-100" :class="switchClassName"></span>
|
<span class="rounded-full border border-black-50 shadow transform transition-transform duration-100" :style="{ width: cursorHeightWidth + 'px', height: cursorHeightWidth + 'px' }" :class="switchClassName"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -19,7 +19,12 @@ export default {
|
||||||
default: 'primary'
|
default: 'primary'
|
||||||
},
|
},
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
labeledBy: String
|
labeledBy: String,
|
||||||
|
label: String,
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'md'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
toggleValue: {
|
toggleValue: {
|
||||||
|
|
@ -37,6 +42,13 @@ export default {
|
||||||
switchClassName() {
|
switchClassName() {
|
||||||
var bgColor = this.disabled ? 'bg-gray-300' : 'bg-white'
|
var bgColor = this.disabled ? 'bg-gray-300' : 'bg-white'
|
||||||
return this.toggleValue ? 'translate-x-5 ' + bgColor : bgColor
|
return this.toggleValue ? 'translate-x-5 ' + bgColor : bgColor
|
||||||
|
},
|
||||||
|
cursorHeightWidth() {
|
||||||
|
if (this.size === 'sm') return 16
|
||||||
|
return 20
|
||||||
|
},
|
||||||
|
buttonWidth() {
|
||||||
|
return this.cursorHeightWidth * 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<ui-tooltip v-if="alreadyInLibrary" :text="$strings.LabelAlreadyInYourLibrary" direction="top">
|
<ui-tooltip :text="$strings.LabelAlreadyInYourLibrary" direction="top" class="inline-flex">
|
||||||
<span class="material-symbols ml-1 text-success" style="font-size: 0.8rem">check_circle</span>
|
<span class="material-symbols ml-1 text-sm text-success">check_circle</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
alreadyInLibrary: Boolean
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,67 +3,67 @@
|
||||||
<form class="w-full h-full px-2 md:px-4 py-6" @submit.prevent="submitForm">
|
<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="flex flex-wrap -mx-1">
|
||||||
<div class="w-full md:w-1/2 px-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>
|
||||||
<div class="flex-grow px-1 mt-2 md:mt-0">
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap mt-2 -mx-1">
|
<div class="flex flex-wrap mt-2 -mx-1">
|
||||||
<div class="w-full md:w-3/4 px-1">
|
<div class="w-full md:w-3/4 px-1">
|
||||||
<!-- Authors filter only contains authors in this library, uses filter data -->
|
<!-- 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>
|
||||||
<div class="flex-grow px-1 mt-2 md:mt-0">
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="flex mt-2 -mx-1">
|
<div class="flex mt-2 -mx-1">
|
||||||
<div class="flex-grow px-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>
|
||||||
</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="flex flex-wrap mt-2 -mx-1">
|
||||||
<div class="w-full md:w-1/2 px-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>
|
||||||
<div class="flex-grow px-1 mt-2 md:mt-0">
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap mt-2 -mx-1">
|
<div class="flex flex-wrap mt-2 -mx-1">
|
||||||
<div class="w-full md:w-1/2 px-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>
|
||||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
<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>
|
||||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap mt-2 -mx-1">
|
<div class="flex flex-wrap mt-2 -mx-1">
|
||||||
<div class="w-full md:w-1/4 px-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>
|
||||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
<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>
|
||||||
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
|
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
|
||||||
<div class="flex justify-center">
|
<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>
|
||||||
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
|
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
|
||||||
<div class="flex justify-center">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -132,6 +132,12 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleInputChange() {
|
||||||
|
this.$emit('change', {
|
||||||
|
libraryItemId: this.libraryItem.id,
|
||||||
|
hasChanges: this.checkForChanges().hasChanges
|
||||||
|
})
|
||||||
|
},
|
||||||
getDetails() {
|
getDetails() {
|
||||||
this.forceBlur()
|
this.forceBlur()
|
||||||
return this.checkForChanges()
|
return this.checkForChanges()
|
||||||
|
|
@ -172,6 +178,7 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.handleInputChange()
|
||||||
},
|
},
|
||||||
forceBlur() {
|
forceBlur() {
|
||||||
if (this.$refs.titleInput) this.$refs.titleInput.blur()
|
if (this.$refs.titleInput) this.$refs.titleInput.blur()
|
||||||
|
|
@ -286,4 +293,4 @@ export default {
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent>
|
<div aria-hidden="true" class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent>
|
||||||
<span class="material-symbols" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize" aria-label="Decrease Cover Size" role="button"></span>
|
<span class="material-symbols" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize" aria-label="Decrease Cover Size" role="button"></span>
|
||||||
<p class="px-2 font-mono" style="font-size: 1rem">{{ bookCoverWidth }}</p>
|
<p class="px-2 font-mono" style="font-size: 1rem">{{ bookCoverWidth }}</p>
|
||||||
<span class="material-symbols" :class="selectedSizeIndex === availableSizes.length - 1 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="increaseSize" aria-label="Increase Cover Size" role="button"></span>
|
<span class="material-symbols" :class="selectedSizeIndex === availableSizes.length - 1 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="increaseSize" aria-label="Increase Cover Size" role="button"></span>
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@
|
||||||
<div class="flex items-center py-3e">
|
<div class="flex items-center py-3e">
|
||||||
<slot />
|
<slot />
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<button cy-id="leftScrollButton" v-if="isScrollable" class="w-8e h-8e mx-1e flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
|
<button cy-id="leftScrollButton" v-if="isScrollable" :aria-label="$strings.ButtonScrollLeft" class="w-8e h-8e mx-1e flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
|
||||||
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">chevron_left</span>
|
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">chevron_left</span>
|
||||||
</button>
|
</button>
|
||||||
<button cy-id="rightScrollButton" v-if="isScrollable" class="w-8e h-8e mx-1e flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
|
<button cy-id="rightScrollButton" v-if="isScrollable" :aria-label="$strings.ButtonScrollRight" class="w-8e h-8e mx-1e flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
|
||||||
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">chevron_right</span>
|
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">chevron_right</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -65,7 +65,7 @@ export default {
|
||||||
},
|
},
|
||||||
authors: {
|
authors: {
|
||||||
component: 'cards-author-card',
|
component: 'cards-author-card',
|
||||||
itemPropName: 'author',
|
itemPropName: 'author-mount',
|
||||||
itemIdFunc: (item) => item.id
|
itemIdFunc: (item) => item.id
|
||||||
},
|
},
|
||||||
narrators: {
|
narrators: {
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue