mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-01-01 16:49:38 +00:00
Clean and parse author name from directory, sort by author last name, scan for covers
This commit is contained in:
parent
9300a0bfb6
commit
e230cb47e8
28 changed files with 783 additions and 59 deletions
|
|
@ -65,4 +65,8 @@
|
|||
|
||||
.icon-text {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
#ab-page-wrapper {
|
||||
background-image: linear-gradient(to right bottom, #2e2e2e, #303030, #313131, #333333, #353535, #343434, #323232, #313131, #2c2c2c, #282828, #232323, #1f1f1f);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -83,21 +83,37 @@ export default {
|
|||
}
|
||||
this.$store.commit('audiobooks/remove', audiobook)
|
||||
},
|
||||
scanComplete(results) {
|
||||
if (!results) results = {}
|
||||
this.$store.commit('setIsScanning', false)
|
||||
var scanResultMsgs = []
|
||||
if (results.added) scanResultMsgs.push(`${results.added} added`)
|
||||
if (results.updated) scanResultMsgs.push(`${results.updated} updated`)
|
||||
if (results.removed) scanResultMsgs.push(`${results.removed} removed`)
|
||||
if (!scanResultMsgs.length) this.$toast.success('Scan Finished\nEverything was up to date')
|
||||
else this.$toast.success('Scan Finished\n' + scanResultMsgs.join('\n'))
|
||||
scanComplete({ scanType, results }) {
|
||||
if (scanType === 'covers') {
|
||||
this.$store.commit('setIsScanningCovers', false)
|
||||
if (results) {
|
||||
this.$toast.success(`Scan Finished\nUpdated ${results.found} covers`)
|
||||
}
|
||||
} else {
|
||||
this.$store.commit('setIsScanning', false)
|
||||
if (results) {
|
||||
var scanResultMsgs = []
|
||||
if (results.added) scanResultMsgs.push(`${results.added} added`)
|
||||
if (results.updated) scanResultMsgs.push(`${results.updated} updated`)
|
||||
if (results.removed) scanResultMsgs.push(`${results.removed} removed`)
|
||||
if (!scanResultMsgs.length) this.$toast.success('Scan Finished\nEverything was up to date')
|
||||
else this.$toast.success('Scan Finished\n' + scanResultMsgs.join('\n'))
|
||||
}
|
||||
}
|
||||
},
|
||||
scanStart() {
|
||||
this.$store.commit('setIsScanning', true)
|
||||
scanStart(scanType) {
|
||||
if (scanType === 'covers') {
|
||||
this.$store.commit('setIsScanningCovers', true)
|
||||
} else {
|
||||
this.$store.commit('setIsScanning', true)
|
||||
}
|
||||
},
|
||||
scanProgress(progress) {
|
||||
this.$store.commit('setScanProgress', progress)
|
||||
scanProgress({ scanType, progress }) {
|
||||
if (scanType === 'covers') {
|
||||
this.$store.commit('setCoverScanProgress', progress)
|
||||
} else {
|
||||
this.$store.commit('setScanProgress', progress)
|
||||
}
|
||||
},
|
||||
userUpdated(user) {
|
||||
if (this.$store.state.user.user.id === user.id) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "0.9.72-beta",
|
||||
"version": "0.9.73-beta",
|
||||
"description": "Audiobook manager and player",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="bg-bg page overflow-hidden relative" :class="streamAudiobook ? 'streaming' : ''">
|
||||
<div id="ab-page-wrapper" class="bg-bg page overflow-hidden relative" :class="streamAudiobook ? 'streaming' : ''">
|
||||
<div v-show="saving" class="absolute z-20 w-full h-full flex items-center justify-center">
|
||||
<ui-loading-indicator />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="bg-bg page overflow-hidden" :class="streamAudiobook ? 'streaming' : ''">
|
||||
<div id="ab-page-wrapper" class="bg-bg page overflow-hidden" :class="streamAudiobook ? 'streaming' : ''">
|
||||
<div class="w-full h-full overflow-y-auto p-8">
|
||||
<div class="flex max-w-6xl mx-auto">
|
||||
<div class="w-52" style="min-width: 208px">
|
||||
|
|
@ -10,7 +10,11 @@
|
|||
</div>
|
||||
<div class="flex-grow px-10">
|
||||
<div class="flex">
|
||||
<h1 class="text-2xl">{{ title }}</h1>
|
||||
<div class="mb-2">
|
||||
<h1 class="text-2xl font-book leading-7">{{ title }}</h1>
|
||||
<h3 v-if="series" class="font-book text-gray-300 text-lg leading-7">{{ seriesText }}</h3>
|
||||
<p class="text-sm text-gray-100 leading-7">by {{ author }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
</div>
|
||||
<p class="text-gray-300 text-sm my-1">
|
||||
|
|
@ -133,6 +137,17 @@ export default {
|
|||
author() {
|
||||
return this.book.author || 'Unknown'
|
||||
},
|
||||
series() {
|
||||
return this.book.series || null
|
||||
},
|
||||
volumeNumber() {
|
||||
return this.book.volumeNumber || null
|
||||
},
|
||||
seriesText() {
|
||||
if (!this.series) return ''
|
||||
if (!this.volumeNumber) return this.series
|
||||
return `${this.series} #${this.volumeNumber}`
|
||||
},
|
||||
durationPretty() {
|
||||
return this.audiobook.durationPretty
|
||||
},
|
||||
|
|
|
|||
|
|
@ -26,10 +26,15 @@
|
|||
</table>
|
||||
</div>
|
||||
<div class="h-0.5 bg-primary bg-opacity-50 w-full" />
|
||||
<div class="flex items-center py-4 mb-8">
|
||||
<p class="text-2xl">Scanner</p>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" @click="scan">Scan</ui-btn>
|
||||
<div class="py-4 mb-8">
|
||||
<div class="flex items-start py-2">
|
||||
<p class="text-2xl">Scanner</p>
|
||||
<div class="flex-grow" />
|
||||
<div class="w-40 flex flex-col">
|
||||
<ui-btn color="success" class="mb-4" :loading="isScanning" :disabled="isScanningCovers" @click="scan">Scan</ui-btn>
|
||||
<ui-btn color="primary" small :padding-x="2" :loading="isScanningCovers" :disabled="isScanning" @click="scanCovers">Scan for Covers</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-0.5 bg-primary bg-opacity-50 w-full" />
|
||||
|
|
@ -68,6 +73,12 @@ export default {
|
|||
computed: {
|
||||
streamAudiobook() {
|
||||
return this.$store.state.streamAudiobook
|
||||
},
|
||||
isScanning() {
|
||||
return this.$store.state.isScanning
|
||||
},
|
||||
isScanningCovers() {
|
||||
return this.$store.state.isScanningCovers
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -79,6 +90,9 @@ export default {
|
|||
scan() {
|
||||
this.$root.socket.emit('scan')
|
||||
},
|
||||
scanCovers() {
|
||||
this.$root.socket.emit('scan_covers')
|
||||
},
|
||||
clickAddUser() {
|
||||
this.$toast.info('Under Construction: User management coming soon.')
|
||||
},
|
||||
|
|
|
|||
|
|
@ -109,6 +109,21 @@ Vue.prototype.$codeToString = (code) => {
|
|||
return finalform
|
||||
}
|
||||
|
||||
function cleanString(str, availableChars) {
|
||||
var _str = str.normalize('NFD').replace(/[\u0300-\u036f]/g, "")
|
||||
var cleaned = ''
|
||||
for (let i = 0; i < _str.length; i++) {
|
||||
cleaned += availableChars.indexOf(str[i]) < 0 ? '' : str[i]
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
|
||||
export const cleanFilterString = (str) => {
|
||||
var _str = str.toLowerCase().replace(/ /g, '_')
|
||||
_str = cleanString(_str, "0123456789abcdefghijklmnopqrstuvwxyz")
|
||||
return _str
|
||||
}
|
||||
|
||||
function loadImageBlob(uri) {
|
||||
return new Promise((resolve) => {
|
||||
const img = document.createElement('img')
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { sort } from '@/assets/fastSort'
|
||||
import { cleanFilterString } from '@/plugins/init.client'
|
||||
|
||||
const STANDARD_GENRES = ['adventure', 'autobiography', 'biography', 'childrens', 'comedy', 'crime', 'dystopian', 'fantasy', 'fiction', 'health', 'history', 'horror', 'mystery', 'new_adult', 'nonfiction', 'philosophy', 'politics', 'religion', 'romance', 'sci-fi', 'self-help', 'short_story', 'technology', 'thriller', 'true_crime', 'western', 'young_adult']
|
||||
|
||||
|
|
@ -16,13 +17,14 @@ export const getters = {
|
|||
var settings = rootState.user.settings || {}
|
||||
var filterBy = settings.filterBy || ''
|
||||
|
||||
var searchGroups = ['genres', 'tags', 'series']
|
||||
var searchGroups = ['genres', 'tags', 'series', 'authors']
|
||||
var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
||||
if (group) {
|
||||
var filter = filterBy.replace(`${group}.`, '')
|
||||
if (group === 'genres') filtered = filtered.filter(ab => ab.book && ab.book.genres.includes(filter))
|
||||
else if (group === 'tags') filtered = filtered.filter(ab => ab.tags.includes(filter))
|
||||
else if (group === 'series') filtered = filtered.filter(ab => ab.book && ab.book.series === filter)
|
||||
else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.author === filter)
|
||||
}
|
||||
return filtered
|
||||
},
|
||||
|
|
@ -35,6 +37,10 @@ export const getters = {
|
|||
// Supports dot notation strings i.e. "book.title"
|
||||
return settings.orderBy.split('.').reduce((a, b) => a[b], ab)
|
||||
})
|
||||
},
|
||||
getUniqueAuthors: (state) => {
|
||||
var _authors = state.audiobooks.filter(ab => !!(ab.book && ab.book.author)).map(ab => ab.book.author)
|
||||
return [...new Set(_authors)]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ export const state = () => ({
|
|||
selectedAudiobook: null,
|
||||
playOnLoad: false,
|
||||
isScanning: false,
|
||||
isScanningCovers: false,
|
||||
scanProgress: null,
|
||||
coverScanProgress: null,
|
||||
developerMode: false
|
||||
})
|
||||
|
||||
|
|
@ -41,9 +43,16 @@ export const mutations = {
|
|||
setIsScanning(state, isScanning) {
|
||||
state.isScanning = isScanning
|
||||
},
|
||||
setScanProgress(state, progress) {
|
||||
if (progress > 0) state.isScanning = true
|
||||
state.scanProgress = progress
|
||||
setScanProgress(state, scanProgress) {
|
||||
if (scanProgress && scanProgress.progress > 0) state.isScanning = true
|
||||
state.scanProgress = scanProgress
|
||||
},
|
||||
setIsScanningCovers(state, isScanningCovers) {
|
||||
state.isScanningCovers = isScanningCovers
|
||||
},
|
||||
setCoverScanProgress(state, coverScanProgress) {
|
||||
if (coverScanProgress && coverScanProgress.progress > 0) state.isScanningCovers = true
|
||||
state.coverScanProgress = coverScanProgress
|
||||
},
|
||||
setDeveloperMode(state, val) {
|
||||
state.developerMode = val
|
||||
|
|
|
|||
|
|
@ -16,12 +16,11 @@ module.exports = {
|
|||
},
|
||||
colors: {
|
||||
bg: '#373838',
|
||||
primary: '#262626',
|
||||
primary: '#232323',
|
||||
accent: '#1ad691',
|
||||
error: '#FF5252',
|
||||
info: '#2196F3',
|
||||
success: '#4CAF50',
|
||||
successDark: '#3b8a3e',
|
||||
warning: '#FB8C00',
|
||||
'black-50': '#bbbbbb',
|
||||
'black-100': '#666666',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue