mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-08-04 10:14:36 +02:00
Clean and parse author name from directory, sort by author last name, scan for covers
This commit is contained in:
parent
759be593b6
commit
51357195e2
28 changed files with 783 additions and 59 deletions
|
@ -3,9 +3,9 @@
|
|||
<div id="toolbar" class="absolute top-0 left-0 w-full h-full z-20 flex items-center px-8">
|
||||
<p class="font-book">{{ numShowing }} Audiobooks</p>
|
||||
<div class="flex-grow" />
|
||||
<controls-filter-select v-model="settings.filterBy" class="w-40 h-7.5" @change="updateFilter" />
|
||||
<controls-filter-select v-model="settings.filterBy" class="w-48 h-7.5" @change="updateFilter" />
|
||||
<span class="px-4 text-sm">by</span>
|
||||
<controls-order-select v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-40 h-7.5" @change="updateOrder" />
|
||||
<controls-order-select v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-48 h-7.5" @change="updateOrder" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<nuxt-link :to="`/audiobook/${audiobookId}`" :style="{ height: height + 32 + 'px', width: width + 32 + 'px' }" class="cursor-pointer p-4">
|
||||
<div class="rounded-sm h-full overflow-hidden relative bookCard" @mouseover="isHovering = true" @mouseleave="isHovering = false">
|
||||
<div class="w-full relative" :style="{ height: width * 1.6 + 'px' }">
|
||||
<cards-book-cover :audiobook="audiobook" />
|
||||
<cards-book-cover :audiobook="audiobook" :author-override="authorFormat" />
|
||||
|
||||
<div v-show="isHovering" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-40">
|
||||
<div class="h-full flex items-center justify-center">
|
||||
|
@ -14,7 +14,6 @@
|
|||
<span class="material-icons" style="font-size: 16px">edit</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 left-0 h-1 bg-yellow-400 shadow-sm" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
||||
</div>
|
||||
<ui-tooltip v-if="showError" :text="errorText" class="absolute top-4 left-0">
|
||||
|
@ -62,6 +61,25 @@ export default {
|
|||
author() {
|
||||
return this.book.author
|
||||
},
|
||||
authorFL() {
|
||||
return this.book.authorFL || this.author
|
||||
},
|
||||
authorLF() {
|
||||
return this.book.authorLF || this.author
|
||||
},
|
||||
authorFormat() {
|
||||
if (!this.orderBy || !this.orderBy.startsWith('book.author')) return null
|
||||
return this.orderBy === 'book.authorLF' ? this.authorLF : this.authorFL
|
||||
},
|
||||
volumeNumber() {
|
||||
return this.book.volumeNumber || null
|
||||
},
|
||||
orderBy() {
|
||||
return this.$store.getters['user/getUserSetting']('orderBy')
|
||||
},
|
||||
filterBy() {
|
||||
return this.$store.getters['user/getUserSetting']('filterBy')
|
||||
},
|
||||
userProgressPercent() {
|
||||
return this.userProgress ? this.userProgress.progress || 0 : 0
|
||||
},
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
<p class="text-center font-book text-error" :style="{ fontSize: titleFontSize + 'rem' }">Invalid Cover</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||
<div>
|
||||
<p class="text-center font-book" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p>
|
||||
|
@ -26,6 +27,7 @@ export default {
|
|||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
authorOverride: String,
|
||||
width: {
|
||||
type: Number,
|
||||
default: 120
|
||||
|
@ -36,6 +38,11 @@ export default {
|
|||
imageFailed: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
cover() {
|
||||
this.imageFailed = false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
book() {
|
||||
return this.audiobook.book || {}
|
||||
|
@ -50,6 +57,7 @@ export default {
|
|||
return this.title
|
||||
},
|
||||
author() {
|
||||
if (this.authorOverride) return this.authorOverride
|
||||
return this.book.author || 'Unknown'
|
||||
},
|
||||
authorCleaned() {
|
||||
|
|
|
@ -115,6 +115,9 @@ export default {
|
|||
if (!_sel) return ''
|
||||
return _sel.text
|
||||
},
|
||||
authors() {
|
||||
return this.$store.getters['audiobooks/getUniqueAuthors']
|
||||
},
|
||||
genres() {
|
||||
return this.$store.state.audiobooks.genres
|
||||
},
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<template v-for="item in items">
|
||||
<li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="item.value === selected ? 'bg-primary bg-opacity-50' : ''" role="option" @click="clickedOption(item.value)">
|
||||
<div class="flex items-center">
|
||||
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
|
||||
<span class="font-normal ml-3 block truncate text-xs">{{ item.text }}</span>
|
||||
</div>
|
||||
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
||||
<span class="material-icons text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||
|
@ -37,8 +37,12 @@ export default {
|
|||
value: 'book.title'
|
||||
},
|
||||
{
|
||||
text: 'Author',
|
||||
value: 'book.author'
|
||||
text: 'Author (First Last)',
|
||||
value: 'book.authorFL'
|
||||
},
|
||||
{
|
||||
text: 'Author (Last, First)',
|
||||
value: 'book.authorLF'
|
||||
},
|
||||
{
|
||||
text: 'Added At',
|
||||
|
@ -73,7 +77,8 @@ export default {
|
|||
}
|
||||
},
|
||||
selectedText() {
|
||||
var _sel = this.items.find((i) => i.value === this.selected)
|
||||
var _selected = this.selected === 'book.author' ? 'book.authorFL' : this.selected
|
||||
var _sel = this.items.find((i) => i.value === _selected)
|
||||
if (!_sel) return ''
|
||||
return _sel.text
|
||||
}
|
||||
|
|
|
@ -21,9 +21,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <ui-text-input-with-label v-model="details.series" label="Series" class="mt-2" /> -->
|
||||
|
||||
<ui-input-dropdown v-model="details.series" label="Series" class="mt-2" :items="series" />
|
||||
<div class="flex mt-2 -mx-1">
|
||||
<div class="w-3/4 px-1">
|
||||
<ui-input-dropdown v-model="details.series" label="Series" :items="series" />
|
||||
</div>
|
||||
<div class="flex-grow px-1">
|
||||
<ui-text-input-with-label v-model="details.volumeNumber" label="Volume #" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ui-textarea-with-label v-model="details.description" :rows="3" label="Description" class="mt-2" />
|
||||
|
||||
|
@ -61,6 +66,7 @@ export default {
|
|||
description: null,
|
||||
author: null,
|
||||
series: null,
|
||||
volumeNumber: null,
|
||||
publishYear: null,
|
||||
genres: []
|
||||
},
|
||||
|
@ -132,6 +138,7 @@ export default {
|
|||
this.details.author = this.book.author
|
||||
this.details.genres = this.book.genres || []
|
||||
this.details.series = this.book.series
|
||||
this.details.volumeNumber = this.book.volumeNumber
|
||||
this.details.publishYear = this.book.publishYear
|
||||
|
||||
this.newTags = this.audiobook.tags || []
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
<p class="px-1 text-sm font-semibold">{{ label }}</p>
|
||||
<div ref="wrapper" class="relative">
|
||||
<form @submit.prevent="submitForm">
|
||||
<div ref="inputWrapper" style="min-height: 40px" class="flex-wrap relative w-full shadow-sm flex items-center bg-primary border border-gray-600 rounded-md px-2 py-1 cursor-text">
|
||||
<div ref="inputWrapper" class="flex-wrap relative w-full shadow-sm flex items-center bg-primary border border-gray-600 rounded px-2 py-2">
|
||||
<input ref="input" v-model="textInput" class="h-full w-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ul ref="menu" v-show="isFocused && items.length && (itemsToShow.length || currentSearch)" class="absolute z-50 mt-0 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
|
||||
<ul ref="menu" v-show="isFocused && items.length && (itemsToShow.length || currentSearch)" class="absolute z-50 mt-0 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
|
||||
<template v-for="item in itemsToShow">
|
||||
<li :key="item" class="text-gray-50 select-none relative py-2 pr-3 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
|
||||
<div class="flex items-center">
|
||||
|
|
|
@ -4,7 +4,12 @@
|
|||
<div ref="wrapper" class="relative">
|
||||
<form @submit.prevent="submitForm">
|
||||
<div ref="inputWrapper" style="min-height: 40px" class="flex-wrap relative w-full shadow-sm flex items-center bg-primary border border-gray-600 rounded-md px-2 py-1 cursor-text" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
||||
<div v-for="item in selected" :key="item" class="rounded-full px-2 py-1 ma-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center">{{ $snakeToNormal(item) }}</div>
|
||||
<div v-for="item in selected" :key="item" class="rounded-full px-2 py-1 ma-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center relative">
|
||||
<div class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer">
|
||||
<span class="material-icons text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item)">close</span>
|
||||
</div>
|
||||
{{ $snakeToNormal(item) }}
|
||||
</div>
|
||||
<input ref="input" v-model="textInput" style="min-width: 40px; width: 40px" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" />
|
||||
</div>
|
||||
</form>
|
||||
|
@ -156,6 +161,13 @@ export default {
|
|||
}
|
||||
this.focus()
|
||||
},
|
||||
removeItem(item) {
|
||||
var remaining = this.selected.filter((i) => i !== item)
|
||||
this.$emit('input', remaining)
|
||||
this.$nextTick(() => {
|
||||
this.recalcMenuPos()
|
||||
})
|
||||
},
|
||||
insertNewItem(item) {
|
||||
var kebabItem = this.$normalToSnake(item)
|
||||
this.selected.push(kebabItem)
|
||||
|
|
|
@ -1,25 +1,47 @@
|
|||
<template>
|
||||
<div v-show="isScanning" class="fixed bottom-0 left-0 right-0 mx-auto z-20 max-w-lg">
|
||||
<div v-show="isScanning" class="fixed bottom-4 left-0 right-0 mx-auto z-20 max-w-lg">
|
||||
<div class="w-full my-1 rounded-lg drop-shadow-lg px-4 py-2 flex items-center justify-center text-center transition-all border border-white border-opacity-40 shadow-md bg-warning">
|
||||
<p class="text-lg font-sans" v-html="text" />
|
||||
</div>
|
||||
<div v-show="!hasCanceled" class="absolute right-0 top-3 bottom-0 px-2">
|
||||
<ui-btn color="red-600" small :padding-x="1" @click="cancelScan">Cancel</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
return {
|
||||
hasCanceled: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isScanning(newVal) {
|
||||
if (newVal) {
|
||||
this.hasCanceled = false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
text() {
|
||||
return `Scanning... <span class="font-mono">${this.scanNum}</span> of <span class="font-mono">${this.scanTotal}</span> <strong class='font-mono px-2'>${this.scanPercent}</strong>`
|
||||
var scanText = this.isScanningFiles ? 'Scanning...' : 'Scanning Covers...'
|
||||
return `${scanText} <span class="font-mono">${this.scanNum}</span> of <span class="font-mono">${this.scanTotal}</span> <strong class='font-mono px-2'>${this.scanPercent}</strong>`
|
||||
},
|
||||
isScanning() {
|
||||
return this.isScanningFiles || this.isScanningCovers
|
||||
},
|
||||
isScanningFiles() {
|
||||
return this.$store.state.isScanning
|
||||
},
|
||||
isScanningCovers() {
|
||||
return this.$store.state.isScanningCovers
|
||||
},
|
||||
scanProgressKey() {
|
||||
return this.isScanningFiles ? 'scanProgress' : 'coverScanProgress'
|
||||
},
|
||||
scanProgress() {
|
||||
return this.$store.state.scanProgress
|
||||
return this.$store.state[this.scanProgressKey]
|
||||
},
|
||||
scanPercent() {
|
||||
return this.scanProgress ? this.scanProgress.progress + '%' : '0%'
|
||||
|
@ -31,7 +53,12 @@ export default {
|
|||
return this.scanProgress ? this.scanProgress.total : 0
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
methods: {
|
||||
cancelScan() {
|
||||
this.hasCanceled = true
|
||||
this.$root.socket.emit('cancel_scan')
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
Loading…
Add table
Add a link
Reference in a new issue