mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-07 18:01:42 +00:00
Merge branch 'master' into lw526_tourGuide
This commit is contained in:
commit
7592ccf690
83 changed files with 3691 additions and 722 deletions
|
|
@ -93,10 +93,10 @@ export default {
|
|||
editAuthor(author) {
|
||||
this.$store.commit('globals/showEditAuthorModal', author)
|
||||
},
|
||||
editItem(libraryItem) {
|
||||
editItem(libraryItem, tab = 'details') {
|
||||
var itemIds = this.shelf.entities.map((e) => e.id)
|
||||
this.$store.commit('setBookshelfBookIds', itemIds)
|
||||
this.$store.commit('showEditModal', libraryItem)
|
||||
this.$store.commit('showEditModalOnTab', { libraryItem, tab: tab || 'details' })
|
||||
},
|
||||
editEpisode({ libraryItem, episode }) {
|
||||
this.$store.commit('setEpisodeTableEpisodeIds', [episode.id])
|
||||
|
|
|
|||
|
|
@ -3,24 +3,18 @@
|
|||
<div class="flex md:hidden h-10 items-center">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}`" class="grow h-full flex justify-center items-center" :class="isHomePage ? 'bg-primary/80' : 'bg-primary/40'">
|
||||
<p v-if="isHomePage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonHome }}</p>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
<span v-else class="material-symbols text-lg">home</span>
|
||||
</nuxt-link>
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="grow h-full flex justify-center items-center" :class="isLibraryPage ? 'bg-primary/80' : 'bg-primary/40'">
|
||||
<p v-if="isLibraryPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonLibrary }}</p>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
<span v-else class="material-symbols text-lg">import_contacts</span>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="grow h-full flex justify-center items-center" :class="isPodcastLatestPage ? 'bg-primary/80' : 'bg-primary/40'">
|
||||
<p class="text-sm">{{ $strings.ButtonLatest }}</p>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary/80' : 'bg-primary/40'">
|
||||
<p v-if="isSeriesPage" class="text-sm">{{ $strings.ButtonSeries }}</p>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
||||
</svg>
|
||||
<span v-else class="material-symbols text-lg">view_column</span>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="grow h-full flex justify-center items-center" :class="isPlaylistsPage ? 'bg-primary/80' : 'bg-primary/40'">
|
||||
<p v-if="isPlaylistsPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonPlaylists }}</p>
|
||||
|
|
@ -32,12 +26,7 @@
|
|||
</nuxt-link>
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary/80' : 'bg-primary/40'">
|
||||
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
|
||||
<svg v-else class="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z"
|
||||
/>
|
||||
</svg>
|
||||
<span v-else class="material-symbols text-lg">groups</span>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary/80' : 'bg-primary/40'">
|
||||
<p class="text-sm">{{ $strings.ButtonAdd }}</p>
|
||||
|
|
|
|||
|
|
@ -232,11 +232,11 @@ export default {
|
|||
clearFilter() {
|
||||
this.$store.dispatch('user/updateUserSettings', { filterBy: 'all' })
|
||||
},
|
||||
editEntity(entity) {
|
||||
editEntity(entity, tab = 'details') {
|
||||
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
||||
const bookIds = this.entities.map((e) => e.id)
|
||||
this.$store.commit('setBookshelfBookIds', bookIds)
|
||||
this.$store.commit('showEditModal', entity)
|
||||
this.$store.commit('showEditModalOnTab', { libraryItem: entity, tab: tab || 'details' })
|
||||
} else if (this.entityName === 'collections') {
|
||||
this.$store.commit('globals/setEditCollection', entity)
|
||||
} else if (this.entityName === 'playlists') {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
<nuxt-link :to="`/library/${currentLibraryId}`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="homePage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<span class="material-symbols text-2xl">home</span>
|
||||
|
||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonHome }}</p>
|
||||
|
||||
|
|
@ -26,6 +28,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="showLibrary ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<span class="material-symbols text-2xl">import_contacts</span>
|
||||
|
||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLibrary }}</p>
|
||||
|
||||
|
|
@ -36,6 +40,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
||||
</svg>
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isSeriesPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<span class="material-symbols text-2xl">view_column</span>
|
||||
|
||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSeries }}</p>
|
||||
|
||||
|
|
@ -65,6 +71,8 @@
|
|||
d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z"
|
||||
/>
|
||||
</svg>
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary/80' : 'bg-bg/60'">
|
||||
<span class="material-symbols text-2xl">groups</span>
|
||||
|
||||
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAuthors }}</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -353,6 +353,14 @@ export default {
|
|||
if (!this.userProgressLastUpdated) return '\u00A0'
|
||||
return this.$getString('LabelLastProgressDate', [this.$formatDatetime(this.userProgressLastUpdated, this.dateFormat, this.timeFormat)])
|
||||
}
|
||||
if (this.orderBy === 'progress.createdAt') {
|
||||
if (!this.userProgressStartedDate) return '\u00A0'
|
||||
return this.$getString('LabelStartedDate', [this.$formatDatetime(this.userProgressStartedDate, this.dateFormat, this.timeFormat)])
|
||||
}
|
||||
if (this.orderBy === 'progress.finishedAt') {
|
||||
if (!this.userProgressFinishedDate) return '\u00A0'
|
||||
return this.$getString('LabelFinishedDate', [this.$formatDatetime(this.userProgressFinishedDate, this.dateFormat, this.timeFormat)])
|
||||
}
|
||||
return null
|
||||
},
|
||||
episodeProgress() {
|
||||
|
|
@ -389,6 +397,14 @@ export default {
|
|||
if (!this.userProgress) return null
|
||||
return this.userProgress.lastUpdate
|
||||
},
|
||||
userProgressStartedDate() {
|
||||
if (!this.userProgress) return null
|
||||
return this.userProgress.startedAt
|
||||
},
|
||||
userProgressFinishedDate() {
|
||||
if (!this.userProgress) return null
|
||||
return this.userProgress.finishedAt
|
||||
},
|
||||
itemIsFinished() {
|
||||
if (this.booksInSeries) return this.seriesIsFinished
|
||||
return this.userProgress ? !!this.userProgress.isFinished : false
|
||||
|
|
@ -772,11 +788,11 @@ export default {
|
|||
},
|
||||
showEditModalFiles() {
|
||||
// More menu func
|
||||
this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'files' })
|
||||
this.$emit('edit', this.libraryItem, 'files')
|
||||
},
|
||||
showEditModalMatch() {
|
||||
// More menu func
|
||||
this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'match' })
|
||||
this.$emit('edit', this.libraryItem, 'match')
|
||||
},
|
||||
sendToDevice(deviceName) {
|
||||
// More menu func
|
||||
|
|
|
|||
|
|
@ -134,6 +134,14 @@ export default {
|
|||
text: this.$strings.LabelLibrarySortByProgress,
|
||||
value: 'progress'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelLibrarySortByProgressStarted,
|
||||
value: 'progress.createdAt'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelLibrarySortByProgressFinished,
|
||||
value: 'progress.finishedAt'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelRandomly,
|
||||
value: 'random'
|
||||
|
|
@ -200,4 +208,4 @@ export default {
|
|||
.librarySortMenu {
|
||||
max-height: calc(100vh - 125px);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -51,19 +51,21 @@
|
|||
<form @submit.prevent="submitSearchForm">
|
||||
<div class="flex flex-wrap sm:flex-nowrap items-center justify-start -mx-1">
|
||||
<div class="w-48 grow p-1">
|
||||
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small />
|
||||
<ui-dropdown v-model="provider" :items="providers" :disabled="searchInProgress" :label="$strings.LabelProvider" small />
|
||||
</div>
|
||||
<div class="w-72 grow p-1">
|
||||
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" />
|
||||
<ui-text-input-with-label v-model="searchTitle" :disabled="searchInProgress" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" />
|
||||
</div>
|
||||
<div v-show="provider != 'itunes' && provider != 'audiobookcovers'" class="w-72 grow p-1">
|
||||
<ui-text-input-with-label v-model="searchAuthor" :label="$strings.LabelAuthor" />
|
||||
<ui-text-input-with-label v-model="searchAuthor" :disabled="searchInProgress" :label="$strings.LabelAuthor" />
|
||||
</div>
|
||||
<ui-btn class="mt-5 ml-1 md:min-w-24" :padding-x="4" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
|
||||
<ui-btn v-if="!searchInProgress" class="mt-5 ml-1 md:min-w-24" :padding-x="4" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
|
||||
<ui-btn v-else class="mt-5 ml-1 md:min-w-24" :padding-x="4" type="button" color="bg-error" @click.prevent="cancelCurrentSearch">{{ $strings.ButtonCancel }}</ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center sm:max-h-80 sm:overflow-y-scroll mt-2 max-w-full">
|
||||
<p v-if="!coversFound.length">{{ $strings.MessageNoCoversFound }}</p>
|
||||
<p v-if="searchInProgress && !coversFound.length" class="text-gray-300 py-4">{{ $strings.MessageLoading }}</p>
|
||||
<p v-else-if="!searchInProgress && !coversFound.length" class="text-gray-300 py-4">{{ $strings.MessageNoCoversFound }}</p>
|
||||
<template v-for="cover in coversFound">
|
||||
<div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === coverPath ? 'border-yellow-300' : ''" @click="updateCover(cover)">
|
||||
<covers-preview-cover :src="cover" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
|
|
@ -105,7 +107,10 @@ export default {
|
|||
showLocalCovers: false,
|
||||
previewUpload: null,
|
||||
selectedFile: null,
|
||||
provider: 'google'
|
||||
provider: 'google',
|
||||
currentSearchRequestId: null,
|
||||
searchInProgress: false,
|
||||
socketListenersActive: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -129,7 +134,7 @@ export default {
|
|||
},
|
||||
providers() {
|
||||
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
|
||||
return [{ text: 'All', value: 'all' }, ...this.$store.state.scanners.providers, ...this.$store.state.scanners.coverOnlyProviders]
|
||||
return [{ text: 'Best', value: 'best' }, ...this.$store.state.scanners.providers, ...this.$store.state.scanners.coverOnlyProviders, { text: 'All', value: 'all' }]
|
||||
},
|
||||
searchTitleLabel() {
|
||||
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
|
||||
|
|
@ -186,6 +191,9 @@ export default {
|
|||
_file.localPath = `${process.env.serverUrl}/api/items/${this.libraryItemId}/file/${file.ino}?token=${this.userToken}`
|
||||
return _file
|
||||
})
|
||||
},
|
||||
socket() {
|
||||
return this.$root.socket
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -235,7 +243,19 @@ export default {
|
|||
this.searchTitle = this.mediaMetadata.title || ''
|
||||
this.searchAuthor = this.mediaMetadata.authorName || ''
|
||||
if (this.isPodcast) this.provider = 'itunes'
|
||||
else this.provider = localStorage.getItem('book-cover-provider') || localStorage.getItem('book-provider') || 'google'
|
||||
else {
|
||||
// Migrate from 'all' to 'best' (only once)
|
||||
const migrationKey = 'book-cover-provider-migrated'
|
||||
const currentProvider = localStorage.getItem('book-cover-provider') || localStorage.getItem('book-provider') || 'google'
|
||||
|
||||
if (!localStorage.getItem(migrationKey) && currentProvider === 'all') {
|
||||
localStorage.setItem('book-cover-provider', 'best')
|
||||
localStorage.setItem(migrationKey, 'true')
|
||||
this.provider = 'best'
|
||||
} else {
|
||||
this.provider = currentProvider
|
||||
}
|
||||
}
|
||||
},
|
||||
removeCover() {
|
||||
if (!this.coverPath) {
|
||||
|
|
@ -291,22 +311,116 @@ export default {
|
|||
console.error('PersistProvider', error)
|
||||
}
|
||||
},
|
||||
generateRequestId() {
|
||||
return `cover-search-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
},
|
||||
addSocketListeners() {
|
||||
if (!this.socket || this.socketListenersActive) return
|
||||
|
||||
this.socket.on('cover_search_result', this.handleSearchResult)
|
||||
this.socket.on('cover_search_complete', this.handleSearchComplete)
|
||||
this.socket.on('cover_search_error', this.handleSearchError)
|
||||
this.socket.on('cover_search_provider_error', this.handleProviderError)
|
||||
this.socket.on('cover_search_cancelled', this.handleSearchCancelled)
|
||||
this.socket.on('disconnect', this.handleSocketDisconnect)
|
||||
this.socketListenersActive = true
|
||||
},
|
||||
removeSocketListeners() {
|
||||
if (!this.socket || !this.socketListenersActive) return
|
||||
|
||||
this.socket.off('cover_search_result', this.handleSearchResult)
|
||||
this.socket.off('cover_search_complete', this.handleSearchComplete)
|
||||
this.socket.off('cover_search_error', this.handleSearchError)
|
||||
this.socket.off('cover_search_provider_error', this.handleProviderError)
|
||||
this.socket.off('cover_search_cancelled', this.handleSearchCancelled)
|
||||
this.socket.off('disconnect', this.handleSocketDisconnect)
|
||||
this.socketListenersActive = false
|
||||
},
|
||||
handleSearchResult(data) {
|
||||
if (data.requestId !== this.currentSearchRequestId) return
|
||||
|
||||
// Add new covers to the list (avoiding duplicates)
|
||||
const newCovers = data.covers.filter((cover) => !this.coversFound.includes(cover))
|
||||
this.coversFound.push(...newCovers)
|
||||
},
|
||||
handleSearchComplete(data) {
|
||||
if (data.requestId !== this.currentSearchRequestId) return
|
||||
|
||||
this.searchInProgress = false
|
||||
this.currentSearchRequestId = null
|
||||
},
|
||||
handleSearchError(data) {
|
||||
if (data.requestId !== this.currentSearchRequestId) return
|
||||
|
||||
console.error('[Cover Search] Search error:', data.error)
|
||||
this.$toast.error(this.$strings.ToastCoverSearchFailed)
|
||||
this.searchInProgress = false
|
||||
this.currentSearchRequestId = null
|
||||
},
|
||||
handleProviderError(data) {
|
||||
if (data.requestId !== this.currentSearchRequestId) return
|
||||
|
||||
console.warn(`[Cover Search] Provider ${data.provider} failed:`, data.error)
|
||||
},
|
||||
handleSearchCancelled(data) {
|
||||
if (data.requestId !== this.currentSearchRequestId) return
|
||||
|
||||
this.searchInProgress = false
|
||||
this.currentSearchRequestId = null
|
||||
},
|
||||
handleSocketDisconnect() {
|
||||
// If we were in the middle of a search, cancel it (server can't send results anymore)
|
||||
if (this.searchInProgress && this.currentSearchRequestId) {
|
||||
this.searchInProgress = false
|
||||
this.currentSearchRequestId = null
|
||||
}
|
||||
},
|
||||
cancelCurrentSearch() {
|
||||
if (!this.currentSearchRequestId || !this.socket?.connected) {
|
||||
console.error('[Cover Search] Socket not connected')
|
||||
this.$toast.error(this.$strings.ToastConnectionNotAvailable)
|
||||
return
|
||||
}
|
||||
|
||||
this.socket.emit('cancel_cover_search', this.currentSearchRequestId)
|
||||
this.currentSearchRequestId = null
|
||||
this.searchInProgress = false
|
||||
},
|
||||
async submitSearchForm() {
|
||||
if (!this.socket?.connected) {
|
||||
console.error('[Cover Search] Socket not connected')
|
||||
this.$toast.error(this.$strings.ToastConnectionNotAvailable)
|
||||
return
|
||||
}
|
||||
|
||||
// Cancel any existing search
|
||||
if (this.searchInProgress) {
|
||||
this.cancelCurrentSearch()
|
||||
}
|
||||
|
||||
// Store provider in local storage
|
||||
this.persistProvider()
|
||||
|
||||
this.isProcessing = true
|
||||
const searchQuery = this.getSearchQuery()
|
||||
const results = await this.$axios
|
||||
.$get(`/api/search/covers?${searchQuery}`)
|
||||
.then((res) => res.results)
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
return []
|
||||
})
|
||||
this.coversFound = results
|
||||
this.isProcessing = false
|
||||
// Setup socket listeners if not already done
|
||||
this.addSocketListeners()
|
||||
|
||||
// Clear previous results
|
||||
this.coversFound = []
|
||||
this.hasSearched = true
|
||||
this.searchInProgress = true
|
||||
|
||||
// Generate unique request ID
|
||||
const requestId = this.generateRequestId()
|
||||
this.currentSearchRequestId = requestId
|
||||
|
||||
// Emit search request via WebSocket
|
||||
this.socket.emit('search_covers', {
|
||||
requestId,
|
||||
title: this.searchTitle,
|
||||
author: this.searchAuthor || '',
|
||||
provider: this.provider,
|
||||
podcast: this.isPodcast
|
||||
})
|
||||
},
|
||||
setCover(coverFile) {
|
||||
this.isProcessing = true
|
||||
|
|
@ -320,6 +434,18 @@ export default {
|
|||
this.isProcessing = false
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// Setup socket listeners when component is mounted
|
||||
this.addSocketListeners()
|
||||
},
|
||||
beforeDestroy() {
|
||||
// Cancel any ongoing search when component is destroyed
|
||||
if (this.searchInProgress) {
|
||||
this.cancelCurrentSearch()
|
||||
}
|
||||
// Remove socket listeners
|
||||
this.removeSocketListeners()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -77,8 +77,8 @@
|
|||
<ui-checkbox v-model="selectedMatchUsage.author" checkbox-bg="bg" @input="checkboxToggled" />
|
||||
<div class="grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" :label="$strings.LabelAuthor" />
|
||||
<p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white/60">
|
||||
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('author', mediaMetadata.authorName)">{{ mediaMetadata.authorName }}</a>
|
||||
<p v-if="mediaMetadata.authorName || (isPodcast && mediaMetadata.author)" class="text-xs ml-1 text-white/60">
|
||||
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('author', isPodcast ? mediaMetadata.author : mediaMetadata.authorName)">{{ isPodcast ? mediaMetadata.author : mediaMetadata.authorName }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -400,7 +400,9 @@ export default {
|
|||
this.$toast.warning(this.$strings.ToastTitleRequired)
|
||||
return
|
||||
}
|
||||
this.persistProvider()
|
||||
if (!this.isPodcast) {
|
||||
this.persistProvider()
|
||||
}
|
||||
this.runSearch()
|
||||
},
|
||||
async runSearch() {
|
||||
|
|
|
|||
|
|
@ -94,7 +94,6 @@ export default {
|
|||
}
|
||||
|
||||
this.processing = false
|
||||
this.$toast.success(`${this.episodes.length} episode${this.episodes.length > 1 ? 's' : ''} removed`)
|
||||
this.show = false
|
||||
this.$emit('clearSelected')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ export default {
|
|||
.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatePayload)
|
||||
.then(() => {
|
||||
this.isProcessing = false
|
||||
this.$toast.success('Podcast episode updated')
|
||||
this.$toast.success(this.$strings.ToastPodcastEpisodeUpdated)
|
||||
this.$emit('selectTab', 'details')
|
||||
})
|
||||
.catch((error) => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
<template>
|
||||
<div class="flex flex-wrap justify-center mt-6">
|
||||
<div class="flex p-2">
|
||||
<svg class="h-14 w-14" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" />
|
||||
</svg>
|
||||
<span class="material-symbols text-5xl py-1">newsstand</span>
|
||||
<div class="px-1">
|
||||
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalItems) }}</p>
|
||||
<p class="text-xs md:text-sm text-white/80">{{ $strings.LabelStatsItemsInLibrary }}</p>
|
||||
|
|
@ -19,9 +17,7 @@
|
|||
</div>
|
||||
|
||||
<div v-if="isBookLibrary" class="flex p-2">
|
||||
<svg class="h-14 w-14" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" />
|
||||
</svg>
|
||||
<span class="material-symbols text-5xl py-1">person</span>
|
||||
<div class="px-1">
|
||||
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalAuthors) }}</p>
|
||||
<p class="text-xs md:text-sm text-white/80">{{ $strings.LabelStatsAuthors }}</p>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@
|
|||
<div ref="wrapper" class="relative">
|
||||
<form @submit.prevent="submitForm">
|
||||
<div ref="inputWrapper" role="list" style="min-height: 36px" class="flex-wrap relative w-full shadow-xs flex items-center border border-gray-600 rounded-sm px-2 py-1" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
||||
<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">
|
||||
<!-- Use index in v-for and key in case the same key exists multiple times -->
|
||||
<div v-for="(item, idx) in selected" :key="item + '-' + idx" 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/75 flex items-center justify-end opacity-0 hover:opacity-100" :class="{ 'opacity-100': inputFocused }">
|
||||
<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>
|
||||
<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>
|
||||
<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, idx)" @keydown.enter.stop.prevent="removeItem(item, idx)" @focus="setInputFocused(true)" @blur="setInputFocused(false)" tabindex="0">close</button>
|
||||
</div>
|
||||
{{ item }}
|
||||
</div>
|
||||
|
|
@ -259,8 +260,9 @@ export default {
|
|||
}
|
||||
this.focus()
|
||||
},
|
||||
removeItem(item) {
|
||||
var remaining = this.selected.filter((i) => i !== item)
|
||||
removeItem(item, idx) {
|
||||
var remaining = this.selected.slice()
|
||||
remaining.splice(idx, 1)
|
||||
this.$emit('input', remaining)
|
||||
this.$emit('removedItem', item)
|
||||
this.$nextTick(() => {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
<template>
|
||||
<button :aria-label="isRead ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" :class="borderless ? '' : 'bg-primary border border-gray-600'" @click="clickBtn">
|
||||
<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)">
|
||||
<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" />
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-7 19.6l-7-4.66V3h14v12.93l-7 4.67zm-2.01-7.42l-2.58-2.59L6 12l4 4 8-8-1.42-1.42z" />
|
||||
</svg>
|
||||
<div class="w-5 h-5 relative">
|
||||
<span v-if="isRead" class="material-symbols fill text-xl text-success">beenhere</span>
|
||||
<span v-else class="material-symbols text-xl text-white">beenhere</span>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,40 +1,6 @@
|
|||
<template>
|
||||
<ui-tooltip :text="$strings.LabelExplicit" direction="top">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12px" height="12px" viewBox="0 0 512 512" class="ml-1">
|
||||
<path
|
||||
fill="white"
|
||||
d="M 89.00,40.12
|
||||
C 89.00,40.12 127.00,40.12 127.00,40.12
|
||||
127.00,40.12 198.00,40.12 198.00,40.12
|
||||
198.00,40.12 416.00,40.12 416.00,40.12
|
||||
446.58,40.05 472.95,66.42 473.00,97.00
|
||||
473.00,97.00 473.00,303.00 473.00,303.00
|
||||
473.00,303.00 473.00,418.00 473.00,418.00
|
||||
472.65,447.55 445.06,472.95 416.00,473.00
|
||||
416.00,473.00 210.00,473.00 210.00,473.00
|
||||
210.00,473.00 95.00,473.00 95.00,473.00
|
||||
65.45,472.65 40.05,445.06 40.00,416.00
|
||||
40.00,416.00 40.00,136.00 40.00,136.00
|
||||
40.00,136.00 40.00,109.00 40.00,109.00
|
||||
40.00,109.00 40.00,96.00 40.00,96.00
|
||||
40.07,81.58 46.89,67.14 57.01,57.01
|
||||
61.17,52.86 64.86,50.13 70.00,47.31
|
||||
77.25,43.33 81.02,42.18 89.00,40.12 Z
|
||||
M 337.00,121.00
|
||||
C 337.00,121.00 175.00,121.00 175.00,121.00
|
||||
175.00,121.00 175.00,392.00 175.00,392.00
|
||||
175.00,392.00 337.00,392.00 337.00,392.00
|
||||
337.00,392.00 337.00,349.00 337.00,349.00
|
||||
337.00,349.00 226.00,349.00 226.00,349.00
|
||||
226.00,349.00 226.00,274.00 226.00,274.00
|
||||
226.00,274.00 332.00,274.00 332.00,274.00
|
||||
332.00,274.00 332.00,232.00 332.00,232.00
|
||||
332.00,232.00 226.00,232.00 226.00,232.00
|
||||
226.00,232.00 226.00,164.00 226.00,164.00
|
||||
226.00,164.00 337.00,164.00 337.00,164.00
|
||||
337.00,164.00 337.00,121.00 337.00,121.00 Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="material-symbols fill text-sm ml-1 !block">explicit</span>
|
||||
</ui-tooltip>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -132,10 +132,10 @@ export default {
|
|||
editAuthor(author) {
|
||||
this.$store.commit('globals/showEditAuthorModal', author)
|
||||
},
|
||||
editItem(libraryItem) {
|
||||
editItem(libraryItem, tab = 'details') {
|
||||
var itemIds = this.items.map((e) => e.id)
|
||||
this.$store.commit('setBookshelfBookIds', itemIds)
|
||||
this.$store.commit('showEditModal', libraryItem)
|
||||
this.$store.commit('showEditModalOnTab', { libraryItem, tab: tab || 'details' })
|
||||
},
|
||||
selectItem(payload) {
|
||||
this.$emit('selectEntity', payload)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue