Merge branch 'advplyr:master' into master

This commit is contained in:
Rasmus Krämer 2022-04-14 09:25:53 +02:00 committed by GitHub
commit 4abd1c8b49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 436 additions and 91 deletions

View file

@ -5,10 +5,7 @@ import com.audiobookshelf.app.MainActivity
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.server.ApiHandler import com.audiobookshelf.app.server.ApiHandler
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.JSObject import com.getcapacitor.*
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin import com.getcapacitor.annotation.CapacitorPlugin
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -202,6 +199,27 @@ class AbsDatabase : Plugin() {
} }
} }
@PluginMethod
fun updateLocalTrackOrder(call:PluginCall) {
var localLibraryItemId = call.getString("localLibraryItemId", "") ?: ""
var localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
if (localLibraryItem == null) {
call.resolve()
return
}
var tracks:JSArray = call.getArray("tracks") ?: JSArray()
Log.d(tag, "updateLocalTrackOrder $tracks")
for (i in 0..tracks.length()) {
var track = tracks.getJSONObject(i)
var localFileId = track.getString("localFileId")
Log.d(tag, "LOCAL FILE ID $localFileId")
}
call.resolve()
}
// //
// Generic Webview calls to db // Generic Webview calls to db
// //

View file

@ -100,8 +100,9 @@ class AbsDownloader : Plugin() {
fun downloadLibraryItem(call: PluginCall) { fun downloadLibraryItem(call: PluginCall) {
var libraryItemId = call.data.getString("libraryItemId").toString() var libraryItemId = call.data.getString("libraryItemId").toString()
var episodeId = call.data.getString("episodeId").toString() var episodeId = call.data.getString("episodeId").toString()
if (episodeId == "null") episodeId = ""
var localFolderId = call.data.getString("localFolderId").toString() var localFolderId = call.data.getString("localFolderId").toString()
Log.d(tag, "Download library item $libraryItemId to folder $localFolderId") Log.d(tag, "Download library item $libraryItemId to folder $localFolderId / episode: $episodeId")
var downloadId = if (episodeId.isNullOrEmpty()) libraryItemId else "$libraryItemId-$episodeId" var downloadId = if (episodeId.isNullOrEmpty()) libraryItemId else "$libraryItemId-$episodeId"
if (downloadQueue.find { it.id == downloadId } != null) { if (downloadQueue.find { it.id == downloadId } != null) {

View file

@ -8,11 +8,9 @@
<span class="material-icons text-3xl text-white">arrow_back</span> <span class="material-icons text-3xl text-white">arrow_back</span>
</a> </a>
<div v-if="user"> <div v-if="user">
<div class="px-4 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal"> <div class="pl-3 pr-4 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <widgets-library-icon :icon="currentLibraryIcon" :size="4" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" /> <p class="text-base font-book leading-4 ml-2 mt-0.5">{{ currentLibraryName }}</p>
</svg>
<p class="text-base font-book leading-4 ml-2">{{ currentLibraryName }}</p>
</div> </div>
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
@ -45,6 +43,9 @@ export default {
currentLibraryName() { currentLibraryName() {
return this.currentLibrary ? this.currentLibrary.name : 'Main' return this.currentLibrary ? this.currentLibrary.name : 'Main'
}, },
currentLibraryIcon() {
return this.currentLibrary ? this.currentLibrary.icon : 'database'
},
showBack() { showBack() {
if (!this.$route.name) return true if (!this.$route.name) return true
return this.$route.name !== 'index' && !this.$route.name.startsWith('bookshelf') return this.$route.name !== 'index' && !this.$route.name.startsWith('bookshelf')

View file

@ -1,5 +1,5 @@
<template> <template>
<div @mouseover="mouseover" @mouseout="mouseout"> <div>
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden"> <div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
<!-- Image or placeholder --> <!-- Image or placeholder -->
<covers-author-image :author="author" /> <covers-author-image :author="author" />
@ -10,14 +10,6 @@
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p> <p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
</div> </div>
<!-- Search icon btn -->
<div v-show="!searching && isHovering" class="absolute top-0 left-0 p-2 cursor-pointer hover:text-white text-gray-200 transform transition-transform hover:scale-125" @click.prevent.stop="searchAuthor">
<span class="material-icons text-lg">search</span>
</div>
<div v-show="isHovering && !searching" class="absolute top-0 right-0 p-2 cursor-pointer hover:text-white text-gray-200 transform transition-transform hover:scale-125" @click.prevent.stop="$emit('edit', author)">
<span class="material-icons text-lg">edit</span>
</div>
<!-- Loading spinner --> <!-- Loading spinner -->
<div v-show="searching" class="absolute top-0 left-0 z-10 w-full h-full bg-black bg-opacity-50 flex items-center justify-center"> <div v-show="searching" class="absolute top-0 left-0 z-10 w-full h-full bg-black bg-opacity-50 flex items-center justify-center">
<widgets-loading-spinner size="" /> <widgets-loading-spinner size="" />
@ -46,8 +38,7 @@ export default {
}, },
data() { data() {
return { return {
searching: false, searching: false
isHovering: false
} }
}, },
computed: { computed: {
@ -68,12 +59,6 @@ export default {
} }
}, },
methods: { methods: {
mouseover() {
this.isHovering = true
},
mouseout() {
this.isHovering = false
},
async searchAuthor() { async searchAuthor() {
this.searching = true this.searching = true
var response = await this.$axios.$post(`/api/authors/${this.authorId}/match`, { q: this.name }).catch((error) => { var response = await this.$axios.$post(`/api/authors/${this.authorId}/match`, { q: this.name }).catch((error) => {

View file

@ -1,6 +1,13 @@
<template> <template>
<div class="flex h-full px-1 overflow-hidden shadow-sm"> <div class="flex h-full px-1 overflow-hidden">
<!-- <img src="/icons/NoUserPhoto.png" class="w-40 h-40 max-h-40 object-contain" style="max-height: 40px; max-width: 40px" /> --> <div class="overflow-hidden bg-primary rounded" style="height: 50px; width: 40px">
<covers-author-image :author="author" />
</div>
<div class="flex-grow px-2 authorSearchCardContent h-full">
<p class="truncate text-sm">{{ name }}</p>
</div>
</div>
<!-- <div class="flex h-full px-1 overflow-hidden shadow-sm">
<div style="max-height: 48px; max-width: 48px" class="w-12 h-12 bg-primary overflow-hidden rounded"> <div style="max-height: 48px; max-width: 48px" class="w-12 h-12 bg-primary overflow-hidden rounded">
<svg width="140%" height="140%" style="margin-left: -20%; margin-top: -20%; opacity: 0.6" viewBox="0 0 177 266" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="140%" height="140%" style="margin-left: -20%; margin-top: -20%; opacity: 0.6" viewBox="0 0 177 266" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill="white" d="M40.7156 165.47C10.2694 150.865 -31.5407 148.629 -38.0532 155.529L63.3191 204.159L76.9443 190.899C66.828 181.394 54.006 171.846 40.7156 165.47Z" stroke="white" stroke-width="4" transform="translate(-2 -1)" /> <path fill="white" d="M40.7156 165.47C10.2694 150.865 -31.5407 148.629 -38.0532 155.529L63.3191 204.159L76.9443 190.899C66.828 181.394 54.006 171.846 40.7156 165.47Z" stroke="white" stroke-width="4" transform="translate(-2 -1)" />
@ -19,18 +26,25 @@
<div class="flex-grow px-2 authorSearchCardContent h-full"> <div class="flex-grow px-2 authorSearchCardContent h-full">
<p class="truncate text-sm">{{ author }}</p> <p class="truncate text-sm">{{ author }}</p>
</div> </div>
</div> </div> -->
</template> </template>
<script> <script>
export default { export default {
props: { props: {
author: String author: {
type: Object,
default: () => {}
}
}, },
data() { data() {
return {} return {}
}, },
computed: {}, computed: {
name() {
return this.author.name
}
},
methods: {}, methods: {},
mounted() {} mounted() {}
} }

View file

@ -1,16 +1,16 @@
<template> <template>
<div class="flex h-full px-1 overflow-hidden"> <div class="flex h-full px-1 overflow-hidden">
<covers-book-cover :audiobook="audiobook" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div class="flex-grow px-2 h-full audiobookSearchCardContent"> <div class="flex-grow px-2 audiobookSearchCardContent">
<p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p> <p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p>
<p v-else class="truncate text-sm" v-html="matchHtml" /> <p v-else class="truncate text-sm" v-html="matchHtml" />
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-gray-300">{{ matchHtml }}</p> <p v-if="matchKey === 'subtitle'" class="truncate text-xs text-gray-300">{{ matchHtml }}</p>
<p v-if="matchKey !== 'authorFL'" class="text-xs text-gray-200 truncate">by {{ authorFL }}</p> <p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p>
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" /> <p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
<div v-if="matchKey === 'series' || matchKey === 'tags'" class="m-0 p-0 truncate" v-html="matchHtml" /> <div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
</div> </div>
</div> </div>
</template> </template>
@ -18,7 +18,7 @@
<script> <script>
export default { export default {
props: { props: {
audiobook: { libraryItem: {
type: Object, type: Object,
default: () => {} default: () => {}
}, },
@ -37,17 +37,27 @@ export default {
if (this.bookCoverAspectRatio === 1) return 50 * 1.2 if (this.bookCoverAspectRatio === 1) return 50 * 1.2
return 50 return 50
}, },
book() { media() {
return this.audiobook ? this.audiobook.book || {} : {} return this.libraryItem ? this.libraryItem.media || {} : {}
},
mediaMetadata() {
return this.media.metadata || {}
},
mediaType() {
return this.libraryItem ? this.libraryItem.mediaType : null
},
isPodcast() {
return this.mediaType === 'podcast'
}, },
title() { title() {
return this.book ? this.book.title : 'No Title' return this.mediaMetadata.title || 'No Title'
}, },
subtitle() { subtitle() {
return this.book ? this.book.subtitle : '' return this.mediaMetadata.subtitle
}, },
authorFL() { authorName() {
return this.book ? this.book.authorFL : 'Unknown' if (this.isPodcast) return this.mediaMetadata.author
return this.mediaMetadata.authorName
}, },
matchHtml() { matchHtml() {
if (!this.matchText || !this.search) return '' if (!this.matchText || !this.search) return ''
@ -69,7 +79,9 @@ export default {
html += lastPart html += lastPart
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>` if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
if (this.matchKey === 'authorFL') return `by ${html}` if (this.matchKey === 'authors') return `by ${html}`
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
if (this.matchKey === 'series') return `<p class="truncate">Series: ${html}</p>` if (this.matchKey === 'series') return `<p class="truncate">Series: ${html}</p>`
return `${html}` return `${html}`
} }

View file

@ -42,11 +42,9 @@
</div> </div>
<!-- Error widget --> <!-- Error widget -->
<ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0 z-10"> <div v-if="showError" :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
<div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300"> <span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span> </div>
</div>
</ui-tooltip>
<!-- Volume number --> <!-- Volume number -->
<div v-if="seriesSequence && showSequence && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }"> <div v-if="seriesSequence && showSequence && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">

View file

@ -7,7 +7,7 @@
<div class="absolute cover-bg" ref="coverBg" /> <div class="absolute cover-bg" ref="coverBg" />
</div> </div>
<div class="w-full h-full absolute top-0 left-0"> <div class="w-full h-full absolute top-0 left-0 bg-primary">
<img v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" /> <img v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
</div> </div>
@ -22,7 +22,7 @@
<p class="whitespace-normal" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }"> <p class="whitespace-normal" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">
{{ displayTitle }}<span v-if="seriesSequence">&nbsp;#{{ seriesSequence }}</span> {{ displayTitle }}<span v-if="seriesSequence">&nbsp;#{{ seriesSequence }}</span>
</p> </p>
<p class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p> <p class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">by {{ displayAuthor }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p> <p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
</div> </div>
</div> </div>
@ -150,10 +150,11 @@ export default {
return Math.max(2, 3 * this.sizeMultiplier) return Math.max(2, 3 * this.sizeMultiplier)
}, },
author() { author() {
return this.mediaMetadata.authorName || '' if (this.isPodcast) return this.mediaMetadata.author || 'Unknown'
return this.mediaMetadata.authorName || 'Unknown'
}, },
authorLF() { authorLF() {
return this.mediaMetadata.authorNameLF || '' return this.mediaMetadata.authorNameLF || 'Unknown'
}, },
series() { series() {
// Only included when filtering by series or collapse series // Only included when filtering by series or collapse series
@ -177,6 +178,7 @@ export default {
return this.title return this.title
}, },
displayAuthor() { displayAuthor() {
if (this.isPodcast) return this.author
if (this.orderBy === 'media.metadata.authorNameLF') return this.authorLF if (this.orderBy === 'media.metadata.authorNameLF') return this.authorLF
return this.author return this.author
}, },
@ -184,7 +186,6 @@ export default {
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs) if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs) if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt) if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt)
if (this.orderBy === 'duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size) if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
return null return null
}, },

View file

@ -1,8 +1,8 @@
<template> <template>
<div class="flex h-full px-1 overflow-hidden"> <div class="flex h-full px-1 overflow-hidden">
<covers-group-cover :name="series" :book-items="bookItems" :width="80" :height="60" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-group-cover :name="name" :book-items="bookItems" :width="80" :height="60" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div class="flex-grow px-2 seriesSearchCardContent h-full"> <div class="flex-grow px-2 seriesSearchCardContent h-full">
<p class="truncate text-sm">{{ series }}</p> <p class="truncate text-sm">{{ name }}</p>
</div> </div>
</div> </div>
</template> </template>
@ -10,7 +10,10 @@
<script> <script>
export default { export default {
props: { props: {
series: String, series: {
type: Object,
default: () => {}
},
bookItems: { bookItems: {
type: Array, type: Array,
default: () => [] default: () => []
@ -22,6 +25,9 @@ export default {
computed: { computed: {
bookCoverAspectRatio() { bookCoverAspectRatio() {
return this.$store.getters['getBookCoverAspectRatio'] return this.$store.getters['getBookCoverAspectRatio']
},
name() {
return this.series.name
} }
}, },
methods: {}, methods: {},

View file

@ -58,6 +58,7 @@ export default {
return this.$route.query || {} return this.$route.query || {}
}, },
entityTitle() { entityTitle() {
if (this.isPodcast) return 'Podcasts'
if (this.page === 'library') return 'Books' if (this.page === 'library') return 'Books'
else if (this.page === 'series') { else if (this.page === 'series') {
return 'Series' return 'Series'
@ -71,6 +72,9 @@ export default {
return this.$store.state.globals.series.name return this.$store.state.globals.series.name
} }
return null return null
},
isPodcast() {
return this.$store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast'
} }
}, },
methods: { methods: {

View file

@ -0,0 +1,23 @@
<template>
<svg fill="currentColor" class="h-full w-full p-px" viewBox="0 0 1978.03 2349.44">
<path
d="M2519.5,1438.39c-12.13-10.1-31-25-56.57-42.62V1197.31c0-505.94-410.15-916.09-916.1-916.09h0c-505.94,0-916.09,410.15-916.09,916.09v198.46c-25.57,17.66-44.44,32.52-56.57,42.62a45.45,45.45,0,0,0-16.35,34.95v237.74a45.45,45.45,0,0,0,16.35,35c28.28,23.54,93.18,72.92,194.22,123.55v23.11c0,62.32,40.21,112.85,89.8,112.85h0c49.59,0,89.8-50.53,89.8-112.85V1322.51c0-62.33-40.21-112.86-89.8-112.86h0c-47.51,0-86.4,46.38-89.58,105.07l-.22.11V1197.31c0-429.92,348.52-778.43,778.44-778.43h0c429.92,0,778.44,348.51,778.44,778.43v117.52l-.22-.11c-3.18-58.69-42.06-105.07-89.58-105.07h0c-49.59,0-89.79,50.53-89.79,112.86v570.18c0,62.32,40.2,112.85,89.79,112.85h0c49.6,0,89.8-50.53,89.8-112.85v-23.11c101.05-50.63,165.95-100,194.23-123.55a45.48,45.48,0,0,0,16.35-35V1473.34A45.48,45.48,0,0,0,2519.5,1438.39Z"
transform="translate(-557.82 -281.22)"
/>
<path d="M1227.4,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56A108.47,108.47,0,0,0,1227.4,998.08H1115.33a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1047.75,1289.38H1295v25.83H1047.75Z" transform="translate(-557.82 -281.22)" />
<path d="M1602.87,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56a108.47,108.47,0,0,0-108.47-108.48H1490.8a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1423.22,1289.38h247.22v25.83H1423.22Z" transform="translate(-557.82 -281.22)" />
<path d="M1978.34,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56a108.47,108.47,0,0,0-108.47-108.48H1866.27a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1798.69,1289.38h247.22v25.83H1798.69Z" transform="translate(-557.82 -281.22)" />
<rect x="180.05" y="2185.95" width="1617.93" height="163.49" rx="81.74" />
</svg>
</template>
<script>
export default {
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>

View file

@ -0,0 +1,16 @@
<template>
<svg fill="currentColor" viewBox="0 0 24 24">
<path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" />
</svg>
</template>
<script>
export default {
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>

View file

@ -0,0 +1,16 @@
<template>
<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M6,19L9,15.14L11.14,17.72L14.14,13.86L18,19H6M6,4H11V12L8.5,10.5L6,12M18,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V4A2,2 0 0,0 18,2Z" />
</svg>
</template>
<script>
export default {
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>

View file

@ -0,0 +1,16 @@
<template>
<svg class="p-px" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
</svg>
</template>
<script>
export default {
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>

View file

@ -0,0 +1,19 @@
<template>
<svg class="p-px" viewBox="0 0 122.877 120.596">
<path
fill="currentColor"
d="M68.925,69.906v50.689H53.953V69.906c-4.918-2.662-8.259-7.867-8.259-13.854 c0-8.694,7.05-15.744,15.745-15.744c8.694,0,15.745,7.05,15.745,15.744C77.184,62.039,73.843,67.244,68.925,69.906L68.925,69.906z M39.32,11.165c2.916-1.438,4.111-4.969,2.673-7.882c-1.438-2.914-4.966-4.111-7.88-2.674C22.213,6.479,12.958,16.19,7.11,27.625 c-4.32,8.445-6.783,17.842-7.08,27.325c-0.299,9.563,1.587,19.223,5.973,28.114c5.401,10.953,14.558,20.695,28.039,27.592 c2.889,1.477,6.429,0.33,7.905-2.559c1.477-2.889,0.331-6.428-2.558-7.904c-11.037-5.645-18.486-13.525-22.833-22.334 c-3.506-7.111-5.014-14.857-4.774-22.539c0.243-7.757,2.256-15.442,5.79-22.348C22.304,23.721,29.76,15.879,39.32,11.165 L39.32,11.165z M88.765,0.608c-2.914-1.438-6.443-0.24-7.881,2.674c-1.438,2.914-0.242,6.445,2.674,7.882 c9.561,4.715,17.017,12.556,21.747,21.808c3.533,6.905,5.547,14.59,5.789,22.348c0.24,7.682-1.268,15.428-4.773,22.539 c-4.347,8.809-11.796,16.689-22.833,22.334c-2.889,1.477-4.034,5.016-2.558,7.904c1.476,2.889,5.016,4.035,7.905,2.559 c13.48-6.896,22.638-16.639,28.039-27.592c4.386-8.891,6.272-18.551,5.973-28.114c-0.297-9.483-2.76-18.88-7.079-27.325 C109.919,16.19,100.665,6.479,88.765,0.608L88.765,0.608z M82.791,26.505c-2.195-1.581-5.256-1.082-6.837,1.113 c-1.58,2.195-1.082,5.256,1.113,6.837c0.885,0.637,1.753,1.352,2.604,2.134c4.971,4.583,7.919,10.694,8.538,17.16 c0.626,6.524-1.111,13.437-5.518,19.552c-0.748,1.039-1.61,2.092-2.585,3.15c-1.835,1.992-1.708,5.098,0.287,6.932 c1.994,1.834,5.099,1.705,6.933-0.287c1.18-1.279,2.286-2.641,3.315-4.072c5.862-8.139,8.166-17.4,7.322-26.197 c-0.848-8.853-4.871-17.208-11.648-23.457C85.249,28.387,84.074,27.431,82.791,26.505L82.791,26.505z M45.81,34.458 c2.195-1.581,2.694-4.642,1.113-6.837c-1.581-2.195-4.642-2.694-6.837-1.114c-1.284,0.926-2.458,1.882-3.524,2.864 c-6.778,6.25-10.801,14.604-11.649,23.457c-0.844,8.796,1.46,18.06,7.323,26.199c1.031,1.43,2.136,2.791,3.315,4.07 c1.834,1.992,4.939,2.121,6.932,0.287c1.996-1.834,2.123-4.939,0.288-6.932c-0.975-1.059-1.837-2.111-2.585-3.15 c-4.406-6.115-6.144-13.027-5.518-19.551c0.619-6.465,3.567-12.577,8.538-17.16C44.058,35.81,44.926,35.095,45.81,34.458 L45.81,34.458z"
/>
</svg>
</template>
<script>
export default {
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>

View file

@ -56,7 +56,7 @@ export default {
data() { data() {
return { return {
sublist: null, sublist: null,
items: [ bookItems: [
{ {
text: 'All', text: 'All',
value: 'all' value: 'all'
@ -96,6 +96,22 @@ export default {
value: 'issues', value: 'issues',
sublist: false sublist: false
} }
],
podcastItems: [
{
text: 'All',
value: 'all'
},
{
text: 'Genre',
value: 'genres',
sublist: true
},
{
text: 'Tag',
value: 'tags',
sublist: true
}
] ]
} }
}, },
@ -124,6 +140,13 @@ export default {
this.$emit('update:filterBy', val) this.$emit('update:filterBy', val)
} }
}, },
isPodcast() {
return this.$store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast'
},
items() {
if (this.isPodcast) return this.podcastItems
return this.bookItems
},
selectedItemSublist() { selectedItemSublist() {
return this.selected && this.selected.includes('.') ? this.selected.split('.')[0] : false return this.selected && this.selected.includes('.') ? this.selected.split('.')[0] : false
}, },

View file

@ -13,10 +13,7 @@
<li :key="library.id" class="text-gray-50 select-none relative py-3 cursor-pointer hover:bg-black-400" :class="currentLibraryId === library.id ? 'bg-bg bg-opacity-80' : ''" role="option" @click="clickedOption(library)"> <li :key="library.id" class="text-gray-50 select-none relative py-3 cursor-pointer hover:bg-black-400" :class="currentLibraryId === library.id ? 'bg-bg bg-opacity-80' : ''" role="option" @click="clickedOption(library)">
<div v-show="currentLibraryId === library.id" class="absolute top-0 left-0 w-0.5 bg-warning h-full" /> <div v-show="currentLibraryId === library.id" class="absolute top-0 left-0 w-0.5 bg-warning h-full" />
<div class="flex items-center px-3"> <div class="flex items-center px-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <widgets-library-icon :icon="library.icon" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
</svg>
<span class="font-normal block truncate text-lg ml-4">{{ library.name }}</span> <span class="font-normal block truncate text-lg ml-4">{{ library.name }}</span>
</div> </div>
</li> </li>

View file

@ -26,7 +26,7 @@ export default {
}, },
data() { data() {
return { return {
items: [ bookItems: [
{ {
text: 'Title', text: 'Title',
value: 'media.metadata.title' value: 'media.metadata.title'
@ -47,6 +47,32 @@ export default {
text: 'Size', text: 'Size',
value: 'size' value: 'size'
} }
],
podcastItems: [
{
text: 'Title',
value: 'media.metadata.title'
},
{
text: 'Author',
value: 'media.metadata.author'
},
{
text: 'Added At',
value: 'addedAt'
},
{
text: 'Size',
value: 'size'
},
{
text: 'File Birthtime',
value: 'birthtimeMs'
},
{
text: 'File Modified',
value: 'mtimeMs'
}
] ]
} }
}, },
@ -74,6 +100,13 @@ export default {
set(val) { set(val) {
this.$emit('update:descending', val) this.$emit('update:descending', val)
} }
},
isPodcast() {
return this.$store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast'
},
items() {
if (this.isPodcast) return this.podcastItems
return this.bookItems
} }
}, },
methods: { methods: {

View file

@ -1,12 +1,14 @@
<template> <template>
<div class="w-full px-2 py-3 overflow-hidden relative border-b border-white border-opacity-10"> <div class="w-full px-0 py-4 overflow-hidden relative border-b border-white border-opacity-10">
<div v-if="episode" class="flex items-center h-24"> <div v-if="episode" class="flex items-center">
<!-- <div class="w-12 min-w-12 max-w-16 h-full"> <!-- <div class="w-12 min-w-12 max-w-16 h-full">
<div class="flex h-full items-center justify-center"> <div class="flex h-full items-center justify-center">
<span class="material-icons drag-handle text-lg text-white text-opacity-50 hover:text-opacity-100">menu</span> <span class="material-icons drag-handle text-lg text-white text-opacity-50 hover:text-opacity-100">menu</span>
</div> </div>
</div> --> </div> -->
<div class="flex-grow px-2"> <div class="flex-grow px-1">
<p v-if="publishedAt" class="text-xs text-gray-400 mb-1">Published {{ $formatDate(publishedAt, 'MMM do, yyyy') }}</p>
<p class="text-sm font-semibold"> <p class="text-sm font-semibold">
{{ title }} {{ title }}
</p> </p>
@ -19,10 +21,8 @@
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p> <p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
</div> </div>
<span class="material-icons px-2" :class="downloadItem ? 'animate-bounce text-warning text-opacity-75' : ''" @click="downloadClick">{{ downloadItem ? 'downloading' : 'download' }}</span>
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" /> <ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
<p v-if="publishedAt" class="px-4 text-sm text-gray-300">Published {{ $formatDate(publishedAt, 'MMM do, yyyy') }}</p> <span class="material-icons px-2" :class="downloadItem ? 'animate-bounce text-warning text-opacity-75 text-xl' : 'text-gray-300 text-xl'" @click="downloadClick">{{ downloadItem ? 'downloading' : 'download' }}</span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,5 +1,7 @@
<template> <template>
<div class="w-full"> <div class="w-full">
<p class="text-lg mb-1 font-semibold">Episodes ({{ episodes.length }})</p>
<template v-for="episode in episodes"> <template v-for="episode in episodes">
<tables-podcast-episode-row :episode="episode" :library-item-id="libraryItemId" :key="episode.id" /> <tables-podcast-episode-row :episode="episode" :library-item-id="libraryItemId" :key="episode.id" />
</template> </template>

View file

@ -0,0 +1,28 @@
<template>
<div :class="`h-${size} w-${size}`">
<component :is="iconComponentName" />
</div>
</template>
<script>
export default {
props: {
icon: String,
size: {
type: Number,
default: 5
}
},
data() {
return {}
},
computed: {
iconComponentName() {
if (this.icon === 'default') return `icons-database-svg`
return `icons-${this.icon}-svg`
}
},
methods: {},
mounted() {}
}
</script>

View file

@ -48,6 +48,11 @@ export default {
bookCoverAspectRatio: this.bookCoverAspectRatio bookCoverAspectRatio: this.bookCoverAspectRatio
} }
if (this.entityName === 'series-books') props.showSequence = true if (this.entityName === 'series-books') props.showSequence = true
if (this.entityName === 'books') {
props.filterBy = this.filterBy
props.orderBy = this.orderBy
props.sortingIgnorePrefix = !!this.sortingIgnorePrefix
}
var _this = this var _this = this
var instance = new ComponentClass({ var instance = new ComponentClass({

View file

@ -24,6 +24,11 @@ export default {
{ hid: 'description', name: 'description', content: '' }, { hid: 'description', name: 'description', content: '' },
{ name: 'format-detection', content: 'telephone=no' } { name: 'format-detection', content: 'telephone=no' }
], ],
script: [
{
src: '/libs/sortable.js'
}
],
link: [ link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }, { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Ubuntu+Mono&family=Source+Sans+Pro:wght@300;400;600' }, { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Ubuntu+Mono&family=Source+Sans+Pro:wght@300;400;600' },

13
package-lock.json generated
View file

@ -30494,6 +30494,11 @@
} }
} }
}, },
"sortablejs": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz",
"integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A=="
},
"source-list-map": { "source-list-map": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
@ -31910,6 +31915,14 @@
"integrity": "sha512-khZR8t3NWZ/JJ2MZxXLbesHrRJ8AKa75PY5Zq8yMifF9x8lHq8ljYkC0d2PD9yahooygQB5tcFyRDkbbIPx8hw==", "integrity": "sha512-khZR8t3NWZ/JJ2MZxXLbesHrRJ8AKa75PY5Zq8yMifF9x8lHq8ljYkC0d2PD9yahooygQB5tcFyRDkbbIPx8hw==",
"requires": {} "requires": {}
}, },
"vuedraggable": {
"version": "2.24.3",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.3.tgz",
"integrity": "sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==",
"requires": {
"sortablejs": "1.10.2"
}
},
"vuex": { "vuex": {
"version": "3.6.2", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz", "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz",

View file

@ -30,7 +30,8 @@
"libarchive.js": "^1.3.0", "libarchive.js": "^1.3.0",
"nuxt": "^2.15.7", "nuxt": "^2.15.7",
"socket.io-client": "^4.1.3", "socket.io-client": "^4.1.3",
"vue-toastification": "^1.7.11" "vue-toastification": "^1.7.11",
"vuedraggable": "^2.24.3"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.13.15", "@babel/core": "7.13.15",

View file

@ -4,7 +4,7 @@
<script> <script>
export default { export default {
async asyncData({ params, app, store }) { async asyncData({ params, app, store, redirect }) {
var series = await app.$axios.$get(`/api/series/${params.id}`).catch((error) => { var series = await app.$axios.$get(`/api/series/${params.id}`).catch((error) => {
console.error('Failed', error) console.error('Failed', error)
return false return false

View file

@ -6,9 +6,6 @@
<covers-book-cover :library-item="libraryItem" :width="128" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-book-cover :library-item="libraryItem" :width="128" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm z-10" :style="{ width: 128 * progressPercent + 'px' }"></div> <div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm z-10" :style="{ width: 128 * progressPercent + 'px' }"></div>
</div> </div>
<div class="flex my-4">
<p v-if="numTracks" class="text-sm">{{ numTracks }} Tracks</p>
</div>
</div> </div>
<div class="flex-grow px-3"> <div class="flex-grow px-3">
<h1 class="text-lg">{{ title }}</h1> <h1 class="text-lg">{{ title }}</h1>
@ -58,7 +55,7 @@
</div> </div>
<div class="w-full py-4"> <div class="w-full py-4">
<p>{{ description }}</p> <p class="text-sm">{{ description }}</p>
</div> </div>
<tables-podcast-episodes-table v-if="isPodcast" :library-item-id="libraryItemId" :episodes="episodes" /> <tables-podcast-episodes-table v-if="isPodcast" :library-item-id="libraryItemId" :episodes="episodes" />

View file

@ -18,10 +18,36 @@
<div v-if="isScanning" class="w-full text-center p-4"> <div v-if="isScanning" class="w-full text-center p-4">
<p>Scanning...</p> <p>Scanning...</p>
</div> </div>
<div v-else class="w-full media-item-container overflow-y-auto"> <div v-else class="w-full max-w-full media-item-container overflow-y-auto overflow-x-hidden relative">
<div v-if="!isPodcast" class="w-full"> <div v-if="!isPodcast" class="w-full">
<p class="text-base mb-2">Audio Tracks ({{ audioTracks.length }})</p> <p class="text-base mb-2">Audio Tracks ({{ audioTracks.length }})</p>
<template v-for="track in audioTracks">
<draggable v-model="audioTracksCopy" v-bind="dragOptions" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate">
<transition-group type="transition" :name="!drag ? 'dragtrack' : null">
<template v-for="track in audioTracksCopy">
<div :key="track.localFileId" class="flex items-center my-1 item">
<div class="w-8 h-12 flex items-center justify-center" style="min-width: 32px">
<span class="material-icons drag-handle text-lg text-white text-opacity-50 hover:text-opacity-100">menu</span>
</div>
<div class="w-8 h-12 flex items-center justify-center" style="min-width: 32px">
<p class="font-mono font-bold text-xl">{{ track.index }}</p>
</div>
<div class="flex-grow px-2">
<p class="text-xs">{{ track.title }}</p>
</div>
<div class="w-20 text-center text-gray-300" style="min-width: 80px">
<p class="text-xs">{{ track.mimeType }}</p>
<p class="text-sm">{{ $elapsedPretty(track.duration) }}</p>
</div>
<div class="w-12 h-12 flex items-center justify-center" style="min-width: 48px">
<span class="material-icons" @click="showTrackDialog(track)">more_vert</span>
</div>
</div>
</template>
</transition-group>
</draggable>
<!-- <template v-for="track in audioTracks">
<div :key="track.localFileId" class="flex items-center my-1"> <div :key="track.localFileId" class="flex items-center my-1">
<div class="w-10 h-12 flex items-center justify-center" style="min-width: 48px"> <div class="w-10 h-12 flex items-center justify-center" style="min-width: 48px">
<p class="font-mono font-bold text-xl">{{ track.index }}</p> <p class="font-mono font-bold text-xl">{{ track.index }}</p>
@ -37,7 +63,7 @@
<span class="material-icons" @click="showTrackDialog(track)">more_vert</span> <span class="material-icons" @click="showTrackDialog(track)">more_vert</span>
</div> </div>
</div> </div>
</template> </template> -->
</div> </div>
<div v-else class="w-full"> <div v-else class="w-full">
<p class="text-base mb-2">Episodes ({{ audioTracks.length }})</p> <p class="text-base mb-2">Episodes ({{ audioTracks.length }})</p>
@ -87,11 +113,16 @@
</template> </template>
<script> <script>
import draggable from 'vuedraggable'
import { Capacitor } from '@capacitor/core' import { Capacitor } from '@capacitor/core'
import { Dialog } from '@capacitor/dialog' import { Dialog } from '@capacitor/dialog'
import { AbsFileSystem } from '@/plugins/capacitor' import { AbsFileSystem } from '@/plugins/capacitor'
export default { export default {
components: {
draggable
},
asyncData({ params }) { asyncData({ params }) {
return { return {
localLibraryItemId: params.id localLibraryItemId: params.id
@ -99,8 +130,16 @@ export default {
}, },
data() { data() {
return { return {
drag: false,
dragOptions: {
animation: 200,
group: 'description',
delay: 40,
delayOnTouchOnly: true
},
failed: false, failed: false,
localLibraryItem: null, localLibraryItem: null,
audioTracksCopy: [],
removingItem: false, removingItem: false,
folderId: null, folderId: null,
folder: null, folder: null,
@ -187,6 +226,15 @@ export default {
} }
}, },
methods: { methods: {
draggableUpdate() {
console.log('Draggable update', this.audioTracksCopy)
// var copyOfCopy = this.audioTracksCopy.map((at) => ({ ...at }))
// const payload = {
// localLibraryItemId: this.localLibraryItemId,
// tracks: copyOfCopy
// }
// this.$db.updateLocalTrackOrder(payload)
},
showItemDialog() { showItemDialog() {
this.selectedAudioTrack = null this.selectedAudioTrack = null
this.showDialog = true this.showDialog = true
@ -322,6 +370,8 @@ export default {
return return
} }
this.audioTracksCopy = this.audioTracks.map((at) => ({ ...at }))
this.folderId = this.localLibraryItem.folderId this.folderId = this.localLibraryItem.folderId
this.folder = await this.$db.getLocalFolder(this.folderId) this.folder = await this.$db.getLocalFolder(this.folderId)
} }
@ -332,9 +382,21 @@ export default {
} }
</script> </script>
<style scoped> <style>
.media-item-container { .media-item-container {
height: calc(100vh - 200px); height: calc(100vh - 200px);
max-height: calc(100vh - 200px); max-height: calc(100vh - 200px);
} }
.sortable-ghost {
opacity: 0.5;
}
.dragtrack-enter-from,
.dragtrack-leave-to {
opacity: 0;
transform: translateX(30px);
}
.dragtrack-leave-active {
position: absolute;
}
</style> </style>

View file

@ -11,28 +11,37 @@
<p class="text-lg text-gray-400">Nothing found</p> <p class="text-lg text-gray-400">Nothing found</p>
</div> </div>
<p v-if="bookResults.length" class="font-semibold text-sm mb-1">Books</p> <p v-if="bookResults.length" class="font-semibold text-sm mb-1">Books</p>
<template v-for="bookResult in bookResults"> <template v-for="item in bookResults">
<div :key="bookResult.audiobook.id" class="w-full h-16 py-1"> <div :key="item.id" class="w-full h-16 py-1">
<nuxt-link :to="`/item/${bookResult.audiobook.id}`"> <nuxt-link :to="`/item/${item.id}`">
<cards-book-search-card :audiobook="bookResult.audiobook" :search="lastSearch" :match-key="bookResult.matchKey" :match-text="bookResult.matchText" /> <cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
</nuxt-link>
</div>
</template>
<p v-if="podcastResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Podcasts</p>
<template v-for="item in podcastResults">
<div :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1">
<nuxt-link :to="`/item/${item.id}`">
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
</nuxt-link> </nuxt-link>
</div> </div>
</template> </template>
<p v-if="seriesResults.length" class="font-semibold text-sm mb-1 mt-2">Series</p> <p v-if="seriesResults.length" class="font-semibold text-sm mb-1 mt-2">Series</p>
<template v-for="seriesResult in seriesResults"> <template v-for="seriesResult in seriesResults">
<div :key="seriesResult.series" class="w-full h-16 py-1"> <div :key="seriesResult.series.id" class="w-full h-16 py-1">
<nuxt-link :to="`/bookshelf/series/${$encode(seriesResult.series)}`"> <nuxt-link :to="`/bookshelf/series/${seriesResult.series.id}`">
<cards-series-search-card :series="seriesResult.series" :book-items="seriesResult.audiobooks" /> <cards-series-search-card :series="seriesResult.series" :book-items="seriesResult.books" />
</nuxt-link> </nuxt-link>
</div> </div>
</template> </template>
<p v-if="authorResults.length" class="font-semibold text-sm mb-1 mt-2">Authors</p> <p v-if="authorResults.length" class="font-semibold text-sm mb-1 mt-2">Authors</p>
<template v-for="authorResult in authorResults"> <template v-for="authorResult in authorResults">
<div :key="authorResult.author" class="w-full h-14 py-1"> <div :key="authorResult.id" class="w-full h-14 py-1">
<nuxt-link :to="`/bookshelf/library?filter=authors.${$encode(authorResult.author)}`"> <nuxt-link :to="`/bookshelf/library?filter=authors.${$encode(authorResult.id)}`">
<cards-author-search-card :key="authorResult.author" :author="authorResult.author" /> <cards-author-search-card :key="authorResult.id" :author="authorResult" />
</nuxt-link> </nuxt-link>
</div> </div>
</template> </template>
@ -49,6 +58,7 @@ export default {
lastSearch: null, lastSearch: null,
isFetching: false, isFetching: false,
bookResults: [], bookResults: [],
podcastResults: [],
seriesResults: [], seriesResults: [],
authorResults: [] authorResults: []
} }
@ -61,7 +71,7 @@ export default {
return this.$store.getters['getBookCoverAspectRatio'] return this.$store.getters['getBookCoverAspectRatio']
}, },
totalResults() { totalResults() {
return this.bookResults.length + this.seriesResults.length + this.authorResults.length return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.podcastResults.length
} }
}, },
methods: { methods: {
@ -69,6 +79,7 @@ export default {
this.lastSearch = value this.lastSearch = value
if (!this.lastSearch) { if (!this.lastSearch) {
this.bookResults = [] this.bookResults = []
this.podcastResults = []
this.seriesResults = [] this.seriesResults = []
this.authorResults = [] this.authorResults = []
return return
@ -82,7 +93,8 @@ export default {
this.isFetching = false this.isFetching = false
this.bookResults = results ? results.audiobooks || [] : [] this.bookResults = results ? results.book || [] : []
this.podcastResults = results ? results.podcast || [] : []
this.seriesResults = results ? results.series || [] : [] this.seriesResults = results ? results.series || [] : []
this.authorResults = results ? results.authors || [] : [] this.authorResults = results ? results.authors || [] : []
}, },

View file

@ -111,6 +111,30 @@ class AbsDatabaseWeb extends WebPlugin {
isLocal: true, isLocal: true,
localFileId: 'lf1', localFileId: 'lf1',
audioProbeResult: {} audioProbeResult: {}
},
{
index: 2,
startOffset: 0,
duration: 15000,
title: 'Track Title 2',
contentUrl: 'test2',
mimeType: 'audio/mpeg',
metadata: null,
isLocal: true,
localFileId: 'lf2',
audioProbeResult: {}
},
{
index: 3,
startOffset: 0,
duration: 20000,
title: 'Track Title 3',
contentUrl: 'test3',
mimeType: 'audio/mpeg',
metadata: null,
isLocal: true,
localFileId: 'lf3',
audioProbeResult: {}
} }
] ]
}, },
@ -170,6 +194,10 @@ class AbsDatabaseWeb extends WebPlugin {
async syncLocalMediaProgressWithServer() { async syncLocalMediaProgressWithServer() {
return null return null
} }
async updateLocalTrackOrder({ localLibraryItemId, tracks }) {
return []
}
} }
const AbsDatabase = registerPlugin('AbsDatabase', { const AbsDatabase = registerPlugin('AbsDatabase', {

View file

@ -94,6 +94,10 @@ class DbService {
syncLocalMediaProgressWithServer() { syncLocalMediaProgressWithServer() {
return AbsDatabase.syncLocalMediaProgressWithServer() return AbsDatabase.syncLocalMediaProgressWithServer()
} }
updateLocalTrackOrder(payload) {
return AbsDatabase.updateLocalTrackOrder(payload)
}
} }
export default ({ app, store }, inject) => { export default ({ app, store }, inject) => {

2
static/lib/sortable.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long