mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-15 22:01:37 +00:00
Remove sortablejs
This commit is contained in:
commit
634167aea6
25 changed files with 530 additions and 173 deletions
|
|
@ -112,7 +112,7 @@ input[type=number] {
|
|||
background-color: #373838;
|
||||
}
|
||||
|
||||
.tracksTable tr:hover {
|
||||
.tracksTable tr:hover:not(:has(th)) {
|
||||
background-color: #474747;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,14 @@
|
|||
<div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-icons text-2.5xl">queue_music</span>
|
||||
|
||||
<p class="pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
|
||||
|
||||
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/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">
|
||||
<path
|
||||
|
|
@ -62,6 +70,14 @@
|
|||
<div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/narrators`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isNarratorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-icons text-2xl">record_voice_over</span>
|
||||
|
||||
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.LabelNarrators }}</p>
|
||||
|
||||
<div v-show="isNarratorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="abs-icons icon-podcast text-xl"></span>
|
||||
|
||||
|
|
@ -78,14 +94,6 @@
|
|||
<div v-show="isMusicAlbumsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-icons text-2.5xl">queue_music</span>
|
||||
|
||||
<p class="pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
|
||||
|
||||
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-icons text-2xl">file_download</span>
|
||||
|
||||
|
|
@ -178,6 +186,9 @@ export default {
|
|||
isAuthorsPage() {
|
||||
return this.$route.name === 'library-library-authors'
|
||||
},
|
||||
isNarratorsPage() {
|
||||
return this.$route.name === 'library-library-narrators'
|
||||
},
|
||||
isPlaylistsPage() {
|
||||
return this.paramId === 'playlists'
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(narrator.name)}`">
|
||||
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
||||
<div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-20">
|
||||
<span class="material-icons-outlined text-8xl">record_voice_over</span>
|
||||
<div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-40">
|
||||
<span class="material-icons-outlined text-[10rem]">record_voice_over</span>
|
||||
</div>
|
||||
|
||||
<!-- Narrator name & num books overlay -->
|
||||
|
|
|
|||
201
client/components/content/LibraryItemDetails.vue
Normal file
201
client/components/content/LibraryItemDetails.vue
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="narrators?.length" class="flex py-0.5 mt-4">
|
||||
<div class="w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNarrators }}</span>
|
||||
</div>
|
||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||
<template v-for="(narrator, index) in narrators">
|
||||
<nuxt-link :key="narrator" :to="`/library/${libraryId}/bookshelf?filter=narrators.${$encode(narrator)}`" class="hover:underline">{{ narrator }}</nuxt-link
|
||||
><span :key="index" v-if="index < narrators.length - 1">, </span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="publishedYear" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublishYear }}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ publishedYear }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="musicAlbum" class="flex py-0.5">
|
||||
<div class="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-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-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-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">Disc</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ musicDiscPretty }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="podcastType" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span>
|
||||
</div>
|
||||
<div class="capitalize">
|
||||
{{ podcastType }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex py-0.5" v-if="genres.length">
|
||||
<div class="w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
|
||||
</div>
|
||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||
<template v-for="(genre, index) in genres">
|
||||
<nuxt-link :key="genre" :to="`/library/${libraryId}/bookshelf?filter=genres.${$encode(genre)}`" class="hover:underline">{{ genre }}</nuxt-link
|
||||
><span :key="index" v-if="index < genres.length - 1">, </span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex py-0.5" v-if="tags.length">
|
||||
<div class="w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelTags }}</span>
|
||||
</div>
|
||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||
<template v-for="(tag, index) in tags">
|
||||
<nuxt-link :key="tag" :to="`/library/${libraryId}/bookshelf?filter=tags.${$encode(tag)}`" class="hover:underline">{{ tag }}</nuxt-link
|
||||
><span :key="index" v-if="index < tags.length - 1">, </span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ durationPretty }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelSize }}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ sizePretty }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
libraryItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
libraryId() {
|
||||
return this.libraryItem.libraryId
|
||||
},
|
||||
isPodcast() {
|
||||
return this.libraryItem.mediaType === 'podcast'
|
||||
},
|
||||
audioFile() {
|
||||
// Music track
|
||||
return this.media.audioFile
|
||||
},
|
||||
media() {
|
||||
return this.libraryItem.media || {}
|
||||
},
|
||||
tracks() {
|
||||
return this.media.tracks || []
|
||||
},
|
||||
podcastEpisodes() {
|
||||
return this.media.episodes || []
|
||||
},
|
||||
mediaMetadata() {
|
||||
return this.media.metadata || {}
|
||||
},
|
||||
publishedYear() {
|
||||
return this.mediaMetadata.publishedYear
|
||||
},
|
||||
genres() {
|
||||
return this.mediaMetadata.genres || []
|
||||
},
|
||||
tags() {
|
||||
return this.media.tags || []
|
||||
},
|
||||
podcastAuthor() {
|
||||
return this.mediaMetadata.author || ''
|
||||
},
|
||||
authors() {
|
||||
return this.mediaMetadata.authors || []
|
||||
},
|
||||
musicArtists() {
|
||||
return this.mediaMetadata.artists || []
|
||||
},
|
||||
musicAlbum() {
|
||||
return this.mediaMetadata.album || ''
|
||||
},
|
||||
musicAlbumArtist() {
|
||||
return this.mediaMetadata.albumArtist || ''
|
||||
},
|
||||
musicTrackPretty() {
|
||||
if (!this.mediaMetadata.trackNumber) return null
|
||||
if (!this.mediaMetadata.trackTotal) return this.mediaMetadata.trackNumber
|
||||
return `${this.mediaMetadata.trackNumber} / ${this.mediaMetadata.trackTotal}`
|
||||
},
|
||||
musicDiscPretty() {
|
||||
if (!this.mediaMetadata.discNumber) return null
|
||||
if (!this.mediaMetadata.discTotal) return this.mediaMetadata.discNumber
|
||||
return `${this.mediaMetadata.discNumber} / ${this.mediaMetadata.discTotal}`
|
||||
},
|
||||
narrators() {
|
||||
return this.mediaMetadata.narrators || []
|
||||
},
|
||||
durationPretty() {
|
||||
if (this.isPodcast) return this.$elapsedPrettyExtended(this.totalPodcastDuration)
|
||||
|
||||
if (!this.tracks.length && !this.audioFile) return 'N/A'
|
||||
if (this.audioFile) return this.$elapsedPrettyExtended(this.duration)
|
||||
return this.$elapsedPretty(this.duration)
|
||||
},
|
||||
duration() {
|
||||
if (!this.tracks.length && !this.audioFile) return 0
|
||||
return this.media.duration
|
||||
},
|
||||
totalPodcastDuration() {
|
||||
if (!this.podcastEpisodes.length) return 0
|
||||
let totalDuration = 0
|
||||
this.podcastEpisodes.forEach((ep) => (totalDuration += ep.duration || 0))
|
||||
return totalDuration
|
||||
},
|
||||
sizePretty() {
|
||||
return this.$bytesPretty(this.media.size)
|
||||
},
|
||||
podcastType() {
|
||||
return this.mediaMetadata.type
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -70,6 +70,9 @@ export default {
|
|||
return this.libraryItem.libraryFiles || []
|
||||
},
|
||||
audioFiles() {
|
||||
if (this.libraryItem.mediaType === 'podcast') {
|
||||
return this.libraryItem.media?.episodes.map((ep) => ep.audioFile) || []
|
||||
}
|
||||
return this.libraryItem.media?.audioFiles || []
|
||||
},
|
||||
filesWithAudioFile() {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ module.exports = ({ command }) => ({
|
|||
plugins: [
|
||||
'@/plugins/constants.js',
|
||||
'@/plugins/init.client.js',
|
||||
'@/plugins/sortable.js',
|
||||
'@/plugins/favicon.js',
|
||||
'@/plugins/axios.js',
|
||||
'@/plugins/toast.js',
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
<ui-text-input v-else v-model="newTagName" />
|
||||
<div class="flex-grow" />
|
||||
<template v-if="editingTag !== tag">
|
||||
<ui-icon-btn v-if="editingTag !== tag" icon="edit" borderless :size="8" icon-font-size="1.1rem" class="mx-1" @click="editTagClick(tag)" />
|
||||
<ui-icon-btn v-if="editingTag !== tag" icon="delete" borderless :size="8" icon-font-size="1.1rem" @click="removeTagClick(tag)" />
|
||||
<ui-icon-btn icon="edit" borderless :size="8" icon-font-size="1.1rem" class="mx-1" @click="editTagClick(tag)" />
|
||||
<ui-icon-btn icon="delete" borderless :size="8" icon-font-size="1.1rem" @click="removeTagClick(tag)" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ui-btn color="success" small class="mx-2" @click.stop="saveTagClick">{{ $strings.ButtonSave }}</ui-btn>
|
||||
|
|
|
|||
|
|
@ -47,84 +47,7 @@
|
|||
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
||||
</template>
|
||||
|
||||
<div v-if="narrator" class="flex py-0.5 mt-4">
|
||||
<div class="w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNarrators }}</span>
|
||||
</div>
|
||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||
<template v-for="(narrator, index) in narrators">
|
||||
<nuxt-link :key="narrator" :to="`/library/${libraryId}/bookshelf?filter=narrators.${$encode(narrator)}`" class="hover:underline">{{ narrator }}</nuxt-link
|
||||
><span :key="index" v-if="index < narrators.length - 1">, </span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="publishedYear" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublishYear }}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ publishedYear }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="musicAlbum" class="flex py-0.5">
|
||||
<div class="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-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-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-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">Disc</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ musicDiscPretty }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex py-0.5" v-if="genres.length">
|
||||
<div class="w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
|
||||
</div>
|
||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||
<template v-for="(genre, index) in genres">
|
||||
<nuxt-link :key="genre" :to="`/library/${libraryId}/bookshelf?filter=genres.${$encode(genre)}`" class="hover:underline">{{ genre }}</nuxt-link
|
||||
><span :key="index" v-if="index < genres.length - 1">, </span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ durationPretty }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelSize }}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ sizePretty }}
|
||||
</div>
|
||||
</div>
|
||||
<content-library-item-details :library-item="libraryItem" />
|
||||
</div>
|
||||
<div class="hidden md:block flex-grow" />
|
||||
</div>
|
||||
|
|
@ -339,9 +262,6 @@ export default {
|
|||
libraryId() {
|
||||
return this.libraryItem.libraryId
|
||||
},
|
||||
folderId() {
|
||||
return this.libraryItem.folderId
|
||||
},
|
||||
libraryItemId() {
|
||||
return this.libraryItem.id
|
||||
},
|
||||
|
|
@ -367,19 +287,10 @@ export default {
|
|||
title() {
|
||||
return this.mediaMetadata.title || 'No Title'
|
||||
},
|
||||
publishedYear() {
|
||||
return this.mediaMetadata.publishedYear
|
||||
},
|
||||
narrator() {
|
||||
return this.mediaMetadata.narratorName
|
||||
},
|
||||
bookSubtitle() {
|
||||
if (this.isPodcast) return null
|
||||
return this.mediaMetadata.subtitle
|
||||
},
|
||||
genres() {
|
||||
return this.mediaMetadata.genres || []
|
||||
},
|
||||
podcastAuthor() {
|
||||
return this.mediaMetadata.author || ''
|
||||
},
|
||||
|
|
@ -389,25 +300,6 @@ export default {
|
|||
musicArtists() {
|
||||
return this.mediaMetadata.artists || []
|
||||
},
|
||||
musicAlbum() {
|
||||
return this.mediaMetadata.album || ''
|
||||
},
|
||||
musicAlbumArtist() {
|
||||
return this.mediaMetadata.albumArtist || ''
|
||||
},
|
||||
musicTrackPretty() {
|
||||
if (!this.mediaMetadata.trackNumber) return null
|
||||
if (!this.mediaMetadata.trackTotal) return this.mediaMetadata.trackNumber
|
||||
return `${this.mediaMetadata.trackNumber} / ${this.mediaMetadata.trackTotal}`
|
||||
},
|
||||
musicDiscPretty() {
|
||||
if (!this.mediaMetadata.discNumber) return null
|
||||
if (!this.mediaMetadata.discTotal) return this.mediaMetadata.discNumber
|
||||
return `${this.mediaMetadata.discNumber} / ${this.mediaMetadata.discTotal}`
|
||||
},
|
||||
narrators() {
|
||||
return this.mediaMetadata.narrators || []
|
||||
},
|
||||
series() {
|
||||
return this.mediaMetadata.series || []
|
||||
},
|
||||
|
|
@ -421,26 +313,10 @@ export default {
|
|||
}
|
||||
})
|
||||
},
|
||||
durationPretty() {
|
||||
if (this.isPodcast) return this.$elapsedPrettyExtended(this.totalPodcastDuration)
|
||||
|
||||
if (!this.tracks.length && !this.audioFile) return 'N/A'
|
||||
if (this.audioFile) return this.$elapsedPrettyExtended(this.duration)
|
||||
return this.$elapsedPretty(this.duration)
|
||||
},
|
||||
duration() {
|
||||
if (!this.tracks.length && !this.audioFile) return 0
|
||||
return this.media.duration
|
||||
},
|
||||
totalPodcastDuration() {
|
||||
if (!this.podcastEpisodes.length) return 0
|
||||
let totalDuration = 0
|
||||
this.podcastEpisodes.forEach((ep) => (totalDuration += ep.duration || 0))
|
||||
return totalDuration
|
||||
},
|
||||
sizePretty() {
|
||||
return this.$bytesPretty(this.media.size)
|
||||
},
|
||||
libraryFiles() {
|
||||
return this.libraryItem.libraryFiles || []
|
||||
},
|
||||
|
|
|
|||
161
client/pages/library/_library/narrators.vue
Normal file
161
client/pages/library/_library/narrators.vue
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
<template>
|
||||
<div class="page relative" :class="streamLibraryItem ? 'streaming' : ''">
|
||||
<app-book-shelf-toolbar page="narrators" is-home />
|
||||
<div id="bookshelf" class="w-full h-full px-1 py-4 md:p-8 relative overflow-y-auto">
|
||||
<table class="tracksTable max-w-2xl mx-auto">
|
||||
<tr>
|
||||
<th class="text-left">{{ $strings.LabelName }}</th>
|
||||
<th class="text-center w-24">{{ $strings.LabelBooks }}</th>
|
||||
<th v-if="userCanUpdate" class="w-40"></th>
|
||||
</tr>
|
||||
<tr v-for="narrator in narrators" :key="narrator.id">
|
||||
<td>
|
||||
<p v-if="selectedNarrator?.id !== narrator.id" class="text-sm md:text-base text-gray-100">{{ narrator.name }}</p>
|
||||
<form v-else @submit.prevent="saveClick">
|
||||
<ui-text-input v-model="newNarratorName" />
|
||||
</form>
|
||||
</td>
|
||||
<td class="text-center w-24">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${narrator.id}`" class="hover:underline">{{ narrator.numBooks }}</nuxt-link>
|
||||
</td>
|
||||
<td v-if="userCanUpdate" class="w-40">
|
||||
<div class="flex justify-end items-center h-10">
|
||||
<template v-if="selectedNarrator?.id !== narrator.id">
|
||||
<ui-icon-btn icon="edit" borderless :size="8" icon-font-size="1.1rem" class="mx-1" @click="editClick(narrator)" />
|
||||
<ui-icon-btn icon="delete" borderless :size="8" icon-font-size="1.1rem" @click="removeClick(narrator)" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ui-btn color="success" small class="mr-2" @click.stop="saveClick">{{ $strings.ButtonSave }}</ui-btn>
|
||||
<ui-btn small @click.stop="cancelEditClick">{{ $strings.ButtonCancel }}</ui-btn>
|
||||
</template>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="absolute top-0 left-0 w-full h-[calc(100%-40px)] mt-10 flex items-center justify-center bg-black/25">
|
||||
<ui-loading-indicator />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
async asyncData({ store, params, redirect, query, app }) {
|
||||
const libraryId = params.library
|
||||
const libraryData = await store.dispatch('libraries/fetch', libraryId)
|
||||
if (!libraryData) {
|
||||
return redirect('/oops?message=Library not found')
|
||||
}
|
||||
|
||||
const library = libraryData.library
|
||||
if (library.mediaType === 'podcast') {
|
||||
return redirect(`/library/${libraryId}`)
|
||||
}
|
||||
|
||||
return {
|
||||
libraryId
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
narrators: [],
|
||||
selectedNarrator: null,
|
||||
newNarratorName: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
streamLibraryItem() {
|
||||
return this.$store.state.streamLibraryItem
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
userCanUpdate() {
|
||||
return this.$store.getters['user/getUserCanUpdate']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
removeClick(narrator) {
|
||||
const payload = {
|
||||
message: this.$getString('MessageConfirmRemoveNarrator', [narrator.name]),
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.removeNarrator(narrator.id)
|
||||
}
|
||||
},
|
||||
type: 'yesNo'
|
||||
}
|
||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||
},
|
||||
editClick(narrator) {
|
||||
this.selectedNarrator = narrator
|
||||
this.newNarratorName = narrator.name
|
||||
},
|
||||
cancelEditClick() {
|
||||
this.selectedNarrator = null
|
||||
this.newNarratorName = null
|
||||
},
|
||||
saveClick() {
|
||||
if (!this.selectedNarrator) return
|
||||
this.newNarratorName = this.newNarratorName?.trim() || ''
|
||||
if (!this.newNarratorName || this.newNarratorName === this.selectedNarrator.name) {
|
||||
this.cancelEditClick()
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
this.$axios
|
||||
.$patch(`/api/libraries/${this.currentLibraryId}/narrators/${this.selectedNarrator.id}`, { name: this.newNarratorName })
|
||||
.then((data) => {
|
||||
if (data.updated) {
|
||||
this.$toast.success(this.$getString('MessageItemsUpdated', [data.updated]))
|
||||
} else {
|
||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||
}
|
||||
this.cancelEditClick()
|
||||
this.init()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to updated narrator', error)
|
||||
this.$toast.error('Failed to update narrator')
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
removeNarrator(id) {
|
||||
this.loading = true
|
||||
this.$axios
|
||||
.$delete(`/api/libraries/${this.currentLibraryId}/narrators/${id}`)
|
||||
.then((data) => {
|
||||
if (data.updated) {
|
||||
this.$toast.success(this.$getString('MessageItemsUpdated', [data.updated]))
|
||||
} else {
|
||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||
}
|
||||
this.init()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove narrator', error)
|
||||
this.$toast.error('Failed to remove narrator')
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
async init() {
|
||||
this.narrators = await this.$axios
|
||||
.$get(`/api/libraries/${this.currentLibraryId}/narrators`)
|
||||
.then((response) => response.narrators)
|
||||
.catch((error) => {
|
||||
console.error('Failed to load narrators', error)
|
||||
return []
|
||||
})
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
},
|
||||
beforeDestroy() {}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
export default function ({ $config }) {
|
||||
const script = document.createElement('script')
|
||||
script.type = 'text/javascript'
|
||||
script.src = `${$config.routerBasePath || ''}/libs/sortable.js`
|
||||
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -483,6 +483,7 @@
|
|||
"MessageConfirmRemoveCollection": "Sind Sie sicher, dass Sie die Sammlung \"{0}\" löschen wollen?",
|
||||
"MessageConfirmRemoveEpisode": "Sind Sie sicher, dass Sie die Episode \"{0}\" löschen möchten?",
|
||||
"MessageConfirmRemoveEpisodes": "Sind Sie sicher, dass Sie {0} Episoden löschen wollen?",
|
||||
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "Sind Sie sicher, dass Sie die Wiedergabeliste \"{0}\" entfernen möchten?",
|
||||
"MessageConfirmRenameGenre": "Sind Sie sicher, dass Sie die Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts umbenennen wollen?",
|
||||
"MessageConfirmRenameGenreMergeNote": "Hinweis: Kategorie existiert bereits -> Kategorien werden zusammengelegt.",
|
||||
|
|
@ -653,4 +654,4 @@
|
|||
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
|
||||
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
|
||||
"ToastUserDeleteSuccess": "Benutzer gelöscht"
|
||||
}
|
||||
}
|
||||
|
|
@ -483,6 +483,7 @@
|
|||
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
|
||||
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
|
||||
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||
|
|
|
|||
|
|
@ -483,6 +483,7 @@
|
|||
"MessageConfirmRemoveCollection": "Esta seguro que desea remover la colección \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Esta seguro que desea remover el episodio \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Esta seguro que desea remover {0} episodios?",
|
||||
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "Esta seguro que desea remover su lista de reproducción \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "Esta seguro que desea renombrar el genero \"{0}\" a \"{1}\" de todos los elementos?",
|
||||
"MessageConfirmRenameGenreMergeNote": "Nota: Este genero ya existe por lo que se fusionarán.",
|
||||
|
|
|
|||
|
|
@ -483,6 +483,7 @@
|
|||
"MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection « {0} » ?",
|
||||
"MessageConfirmRemoveEpisode": "Êtes-vous sûr de vouloir supprimer l’épisode « {0} » ?",
|
||||
"MessageConfirmRemoveEpisodes": "Êtes-vous sûr de vouloir supprimer {0} épisodes ?",
|
||||
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "Êtes-vous sûr de vouloir supprimer la liste de lecture « {0} » ?",
|
||||
"MessageConfirmRenameGenre": "Êtes-vous sûr de vouloir renommer le genre « {0} » vers « {1} » pour tous les articles ?",
|
||||
"MessageConfirmRenameGenreMergeNote": "Information: Ce genre existe déjà et sera fusionné.",
|
||||
|
|
|
|||
|
|
@ -483,6 +483,7 @@
|
|||
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
|
||||
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
|
||||
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||
|
|
|
|||
|
|
@ -483,6 +483,7 @@
|
|||
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
|
||||
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
|
||||
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||
|
|
|
|||
|
|
@ -483,6 +483,7 @@
|
|||
"MessageConfirmRemoveCollection": "AJeste li sigurni da želite obrisati kolekciju \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Jeste li sigurni da želite obrisati epizodu \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Jeste li sigurni da želite obrisati {0} epizoda/-u?",
|
||||
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
|
||||
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||
|
|
|
|||
|
|
@ -483,6 +483,7 @@
|
|||
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?",
|
||||
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "Sei sicuro di voler rinominare il genere \"{0}\" in \"{1}\" per tutti gli oggetti?",
|
||||
"MessageConfirmRenameGenreMergeNote": "Note: Questo genere esiste già quindi verra unito.",
|
||||
|
|
|
|||
|
|
@ -483,6 +483,7 @@
|
|||
"MessageConfirmRemoveCollection": "Czy na pewno chcesz usunąć kolekcję \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Czy na pewno chcesz usunąć odcinek \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Czy na pewno chcesz usunąć {0} odcinki?",
|
||||
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
|
||||
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||
|
|
|
|||
|
|
@ -483,6 +483,7 @@
|
|||
"MessageConfirmRemoveCollection": "Вы уверены, что хотите удалить коллекцию \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Вы уверены, что хотите удалить эпизод \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Вы уверены, что хотите удалить {0} эпизодов?",
|
||||
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "Вы уверены, что хотите удалить плейлист \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "Вы уверены, что хотите переименовать жанр \"{0}\" в \"{1}\" для всех элементов?",
|
||||
"MessageConfirmRenameGenreMergeNote": "Примечание: Этот жанр уже существует, поэтому они будут объединены.",
|
||||
|
|
|
|||
|
|
@ -155,13 +155,13 @@
|
|||
"HeaderUpdateLibrary": "更新媒体库",
|
||||
"HeaderUsers": "用户",
|
||||
"HeaderYourStats": "你的统计数据",
|
||||
"LabelAbridged": "Abridged",
|
||||
"LabelAbridged": "概要",
|
||||
"LabelAccountType": "帐户类型",
|
||||
"LabelAccountTypeAdmin": "管理员",
|
||||
"LabelAccountTypeGuest": "来宾",
|
||||
"LabelAccountTypeUser": "用户",
|
||||
"LabelActivity": "活动",
|
||||
"LabelAdded": "Added",
|
||||
"LabelAdded": "添加",
|
||||
"LabelAddedAt": "添加于",
|
||||
"LabelAddToCollection": "添加到收藏",
|
||||
"LabelAddToCollectionBatch": "批量添加 {0} 个媒体到收藏",
|
||||
|
|
@ -183,15 +183,15 @@
|
|||
"LabelBackupsMaxBackupSizeHelp": "为了防止错误配置, 如果备份超过配置的大小, 备份将失败.",
|
||||
"LabelBackupsNumberToKeep": "要保留的备份个数",
|
||||
"LabelBackupsNumberToKeepHelp": "一次只能删除一个备份, 因此如果你已经有超过此数量的备份, 则应手动删除它们.",
|
||||
"LabelBitrate": "Bitrate",
|
||||
"LabelBitrate": "比特率",
|
||||
"LabelBooks": "图书",
|
||||
"LabelChangePassword": "修改密码",
|
||||
"LabelChannels": "Channels",
|
||||
"LabelChapters": "Chapters",
|
||||
"LabelChannels": "声道",
|
||||
"LabelChapters": "章节",
|
||||
"LabelChaptersFound": "找到的章节",
|
||||
"LabelChapterTitle": "章节标题",
|
||||
"LabelClosePlayer": "关闭播放器",
|
||||
"LabelCodec": "Codec",
|
||||
"LabelCodec": "编解码",
|
||||
"LabelCollapseSeries": "折叠系列",
|
||||
"LabelCollections": "收藏",
|
||||
"LabelComplete": "已完成",
|
||||
|
|
@ -217,7 +217,7 @@
|
|||
"LabelDuration": "持续时间",
|
||||
"LabelDurationFound": "找到持续时间:",
|
||||
"LabelEdit": "编辑",
|
||||
"LabelEmbeddedCover": "Embedded Cover",
|
||||
"LabelEmbeddedCover": "嵌入封面",
|
||||
"LabelEnable": "启用",
|
||||
"LabelEnd": "结束",
|
||||
"LabelEpisode": "剧集",
|
||||
|
|
@ -235,7 +235,7 @@
|
|||
"LabelFinished": "已听完",
|
||||
"LabelFolder": "文件夹",
|
||||
"LabelFolders": "文件夹",
|
||||
"LabelFormat": "Format",
|
||||
"LabelFormat": "编码格式",
|
||||
"LabelGenre": "流派",
|
||||
"LabelGenres": "流派",
|
||||
"LabelHardDeleteFile": "完全删除文件",
|
||||
|
|
@ -254,12 +254,12 @@
|
|||
"LabelIntervalEveryDay": "每天",
|
||||
"LabelIntervalEveryHour": "每小时",
|
||||
"LabelInvalidParts": "无效部件",
|
||||
"LabelInvert": "Invert",
|
||||
"LabelInvert": "倒转",
|
||||
"LabelItem": "项目",
|
||||
"LabelLanguage": "语言",
|
||||
"LabelLanguageDefaultServer": "默认服务器语言",
|
||||
"LabelLastBookAdded": "Last Book Added",
|
||||
"LabelLastBookUpdated": "Last Book Updated",
|
||||
"LabelLastBookAdded": "最后添加的书",
|
||||
"LabelLastBookUpdated": "最后更新的书",
|
||||
"LabelLastSeen": "上次查看时间",
|
||||
"LabelLastTime": "最近一次",
|
||||
"LabelLastUpdate": "最近更新",
|
||||
|
|
@ -278,12 +278,12 @@
|
|||
"LabelMediaType": "媒体类型",
|
||||
"LabelMetadataProvider": "元数据提供者",
|
||||
"LabelMetaTag": "元数据标签",
|
||||
"LabelMetaTags": "Meta Tags",
|
||||
"LabelMetaTags": "元标签",
|
||||
"LabelMinute": "分钟",
|
||||
"LabelMissing": "丢失",
|
||||
"LabelMissingParts": "丢失的部分",
|
||||
"LabelMore": "更多",
|
||||
"LabelMoreInfo": "More Info",
|
||||
"LabelMoreInfo": "更多..",
|
||||
"LabelName": "名称",
|
||||
"LabelNarrator": "演播者",
|
||||
"LabelNarrators": "演播者",
|
||||
|
|
@ -413,9 +413,9 @@
|
|||
"LabelTag": "标签",
|
||||
"LabelTags": "标签",
|
||||
"LabelTagsAccessibleToUser": "用户可访问的标签",
|
||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||
"LabelTagsNotAccessibleToUser": "用户无法访问标签",
|
||||
"LabelTasks": "正在运行的任务",
|
||||
"LabelTimeBase": "Time Base",
|
||||
"LabelTimeBase": "时间基准",
|
||||
"LabelTimeListened": "收听时间",
|
||||
"LabelTimeListenedToday": "今日收听的时间",
|
||||
"LabelTimeRemaining": "剩余 {0}",
|
||||
|
|
@ -435,7 +435,7 @@
|
|||
"LabelTracksMultiTrack": "多轨",
|
||||
"LabelTracksSingleTrack": "单轨",
|
||||
"LabelType": "类型",
|
||||
"LabelUnabridged": "Unabridged",
|
||||
"LabelUnabridged": "未删节",
|
||||
"LabelUnknown": "未知",
|
||||
"LabelUpdateCover": "更新封面",
|
||||
"LabelUpdateCoverHelp": "找到匹配项时允许覆盖所选书籍存在的封面",
|
||||
|
|
@ -479,10 +479,11 @@
|
|||
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
|
||||
"MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
"MessageConfirmRemoveAllChapters": "你确定要移除所有章节吗?",
|
||||
"MessageConfirmRemoveCollection": "您确定要移除收藏 \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "您确定要移除剧集 \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
|
||||
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "你确定要移除播放列表 \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "你确定要将所有项目流派 \"{0}\" 重命名到 \"{1}\"?",
|
||||
"MessageConfirmRenameGenreMergeNote": "注意: 该流派已经存在, 因此它们将被合并.",
|
||||
|
|
@ -583,7 +584,7 @@
|
|||
"PlaceholderNewFolderPath": "输入文件夹路径",
|
||||
"PlaceholderNewPlaylist": "输入播放列表名称",
|
||||
"PlaceholderSearch": "查找..",
|
||||
"PlaceholderSearchEpisode": "Search episode..",
|
||||
"PlaceholderSearchEpisode": "搜索剧集..",
|
||||
"ToastAccountUpdateFailed": "账户更新失败",
|
||||
"ToastAccountUpdateSuccess": "帐户已更新",
|
||||
"ToastAuthorImageRemoveFailed": "作者图像删除失败",
|
||||
|
|
|
|||
|
|
@ -684,13 +684,12 @@ class LibraryController {
|
|||
}
|
||||
|
||||
async getAuthors(req, res) {
|
||||
var libraryItems = req.libraryItems
|
||||
var authors = {}
|
||||
libraryItems.forEach((li) => {
|
||||
const authors = {}
|
||||
req.libraryItems.forEach((li) => {
|
||||
if (li.media.metadata.authors && li.media.metadata.authors.length) {
|
||||
li.media.metadata.authors.forEach((au) => {
|
||||
if (!authors[au.id]) {
|
||||
var _author = this.db.authors.find(_au => _au.id === au.id)
|
||||
const _author = this.db.authors.find(_au => _au.id === au.id)
|
||||
if (_author) {
|
||||
authors[au.id] = _author.toJSON()
|
||||
authors[au.id].numBooks = 1
|
||||
|
|
@ -707,6 +706,83 @@ class LibraryController {
|
|||
})
|
||||
}
|
||||
|
||||
async getNarrators(req, res) {
|
||||
const narrators = {}
|
||||
req.libraryItems.forEach((li) => {
|
||||
if (li.media.metadata.narrators && li.media.metadata.narrators.length) {
|
||||
li.media.metadata.narrators.forEach((n) => {
|
||||
if (!narrators[n]) {
|
||||
narrators[n] = {
|
||||
id: encodeURIComponent(Buffer.from(n).toString('base64')),
|
||||
name: n,
|
||||
numBooks: 1
|
||||
}
|
||||
} else {
|
||||
narrators[n].numBooks++
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
res.json({
|
||||
narrators: naturalSort(Object.values(narrators)).asc(n => n.name)
|
||||
})
|
||||
}
|
||||
|
||||
async updateNarrator(req, res) {
|
||||
if (!req.user.canUpdate) {
|
||||
Logger.error(`[LibraryController] Unauthorized user "${req.user.username}" attempted to update narrator`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
const narratorName = libraryHelpers.decode(req.params.narratorId)
|
||||
const updatedName = req.body.name
|
||||
if (!updatedName) {
|
||||
return res.status(400).send('Invalid request payload. Name not specified.')
|
||||
}
|
||||
|
||||
const itemsUpdated = []
|
||||
for (const libraryItem of req.libraryItems) {
|
||||
if (libraryItem.media.metadata.updateNarrator(narratorName, updatedName)) {
|
||||
itemsUpdated.push(libraryItem)
|
||||
}
|
||||
}
|
||||
|
||||
if (itemsUpdated.length) {
|
||||
await this.db.updateLibraryItems(itemsUpdated)
|
||||
SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded()))
|
||||
}
|
||||
|
||||
res.json({
|
||||
updated: itemsUpdated.length
|
||||
})
|
||||
}
|
||||
|
||||
async removeNarrator(req, res) {
|
||||
if (!req.user.canUpdate) {
|
||||
Logger.error(`[LibraryController] Unauthorized user "${req.user.username}" attempted to remove narrator`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
const narratorName = libraryHelpers.decode(req.params.narratorId)
|
||||
|
||||
const itemsUpdated = []
|
||||
for (const libraryItem of req.libraryItems) {
|
||||
if (libraryItem.media.metadata.removeNarrator(narratorName)) {
|
||||
itemsUpdated.push(libraryItem)
|
||||
}
|
||||
}
|
||||
|
||||
if (itemsUpdated.length) {
|
||||
await this.db.updateLibraryItems(itemsUpdated)
|
||||
SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded()))
|
||||
}
|
||||
|
||||
res.json({
|
||||
updated: itemsUpdated.length
|
||||
})
|
||||
}
|
||||
|
||||
async matchAll(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[LibraryController] Non-root user attempted to match library items`, req.user)
|
||||
|
|
@ -776,7 +852,7 @@ class LibraryController {
|
|||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
var library = this.db.libraries.find(lib => lib.id === req.params.id)
|
||||
const library = this.db.libraries.find(lib => lib.id === req.params.id)
|
||||
if (!library) {
|
||||
return res.status(404).send('Library not found')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,6 +221,32 @@ class BookMetadata {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update narrator name if narrator is in book
|
||||
* @param {String} oldNarratorName - Narrator name to get updated
|
||||
* @param {String} newNarratorName - Updated narrator name
|
||||
* @return {Boolean} True if narrator was updated
|
||||
*/
|
||||
updateNarrator(oldNarratorName, newNarratorName) {
|
||||
if (!this.hasNarrator(oldNarratorName)) return false
|
||||
this.narrators = this.narrators.filter(n => n !== oldNarratorName)
|
||||
if (!this.hasNarrator(newNarratorName)) {
|
||||
this.narrators.push(newNarratorName)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove narrator name if narrator is in book
|
||||
* @param {String} narratorName - Narrator name to remove
|
||||
* @return {Boolean} True if narrator was updated
|
||||
*/
|
||||
removeNarrator(narratorName) {
|
||||
if (!this.hasNarrator(narratorName)) return false
|
||||
this.narrators = this.narrators.filter(n => n !== narratorName)
|
||||
return true
|
||||
}
|
||||
|
||||
setData(scanMediaData = {}) {
|
||||
this.title = scanMediaData.title || null
|
||||
this.subtitle = scanMediaData.subtitle || null
|
||||
|
|
|
|||
|
|
@ -84,6 +84,9 @@ class ApiRouter {
|
|||
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
|
||||
this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this))
|
||||
this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), LibraryController.getAuthors.bind(this))
|
||||
this.router.get('/libraries/:id/narrators', LibraryController.middleware.bind(this), LibraryController.getNarrators.bind(this))
|
||||
this.router.patch('/libraries/:id/narrators/:narratorId', LibraryController.middleware.bind(this), LibraryController.updateNarrator.bind(this))
|
||||
this.router.delete('/libraries/:id/narrators/:narratorId', LibraryController.middleware.bind(this), LibraryController.removeNarrator.bind(this))
|
||||
this.router.get('/libraries/:id/matchall', LibraryController.middleware.bind(this), LibraryController.matchAll.bind(this))
|
||||
this.router.post('/libraries/:id/scan', LibraryController.middleware.bind(this), LibraryController.scan.bind(this))
|
||||
this.router.get('/libraries/:id/recent-episodes', LibraryController.middleware.bind(this), LibraryController.getRecentEpisodes.bind(this))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue