Clean and parse author name from directory, sort by author last name, scan for covers

This commit is contained in:
Mark Cooper 2021-08-24 20:24:40 -05:00
parent 9300a0bfb6
commit e230cb47e8
28 changed files with 783 additions and 59 deletions

View file

@ -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>

View file

@ -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
},

View file

@ -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() {

View file

@ -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
},

View file

@ -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
}

View file

@ -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 || []

View file

@ -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">

View file

@ -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)

View file

@ -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>