Update chapters modal, search page, fix version check, ignore matching audio file paths on rescan

This commit is contained in:
Mark Cooper 2021-09-24 16:14:33 -05:00
parent fcd664c16e
commit b35997e8be
12 changed files with 146 additions and 32 deletions

View File

@ -30,8 +30,10 @@
</div> </div>
</template> </template>
<div v-show="!shelves.length" class="w-full py-16 text-center text-xl"> <div v-show="!shelves.length" class="w-full py-16 text-center text-xl">
<div class="py-4">No {{ showGroups ? 'Series' : 'Audiobooks' }}</div> <div v-if="page === 'search'" class="py-4 mb-6"><p class="text-2xl">No Results</p></div>
<div v-else class="py-4">No {{ showGroups ? 'Series' : 'Audiobooks' }}</div>
<ui-btn v-if="!showGroups && (filterBy !== 'all' || keywordFilter)" @click="clearFilter">Clear Filter</ui-btn> <ui-btn v-if="!showGroups && (filterBy !== 'all' || keywordFilter)" @click="clearFilter">Clear Filter</ui-btn>
<ui-btn v-else-if="page === 'search'" to="/library">Back to Library</ui-btn>
</div> </div>
</div> </div>
</div> </div>
@ -41,7 +43,12 @@
export default { export default {
props: { props: {
page: String, page: String,
selectedSeries: String selectedSeries: String,
searchResults: {
type: Array,
default: () => []
},
searchQuery: String
}, },
data() { data() {
return { return {
@ -62,6 +69,11 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
this.setBookshelfEntities() this.setBookshelfEntities()
}) })
},
searchResults() {
this.$nextTick(() => {
this.setBookshelfEntities()
})
} }
}, },
computed: { computed: {
@ -96,11 +108,13 @@ export default {
return this.$store.getters['user/getUserSetting']('filterBy') return this.$store.getters['user/getUserSetting']('filterBy')
}, },
showGroups() { showGroups() {
return this.page !== '' && !this.selectedSeries return this.page !== '' && this.page !== 'search' && !this.selectedSeries
}, },
entities() { entities() {
if (this.page === '') { if (this.page === '') {
return this.$store.getters['audiobooks/getFilteredAndSorted']() return this.$store.getters['audiobooks/getFilteredAndSorted']()
} else if (this.page === 'search') {
return this.searchResults || []
} else { } else {
var seriesGroups = this.$store.getters['audiobooks/getSeriesGroups']() var seriesGroups = this.$store.getters['audiobooks/getSeriesGroups']()
if (this.selectedSeries) { if (this.selectedSeries) {

View File

@ -1,21 +1,32 @@
<template> <template>
<div class="w-full h-10 relative"> <div class="w-full h-10 relative">
<div id="toolbar" class="absolute top-0 left-0 w-full h-full z-20 flex items-center px-8"> <div id="toolbar" class="absolute top-0 left-0 w-full h-full z-20 flex items-center px-8">
<p v-if="!selectedSeries" class="font-book">{{ numShowing }} {{ entityName }}</p> <template v-if="page !== 'search'">
<div v-else class="flex items-center"> <p v-if="!selectedSeries" class="font-book">{{ numShowing }} {{ entityName }}</p>
<div @click="seriesBackArrow" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 cursor-pointer"> <div v-else class="flex items-center">
<div @click="seriesBackArrow" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 cursor-pointer">
<span class="material-icons text-3xl text-white">west</span>
</div>
<!-- <span class="material-icons text-2xl cursor-pointer" @click="seriesBackArrow">west</span> -->
<p class="pl-4 font-book text-lg">
{{ selectedSeries }} <span class="ml-3 font-mono text-lg bg-black bg-opacity-30 rounded-lg px-1 py-0.5">{{ numShowing }}</span>
</p>
</div>
<div class="flex-grow" />
<ui-text-input v-show="showSortFilters" v-model="_keywordFilter" placeholder="Keyword Filter" :padding-y="1.5" class="text-xs w-40" />
<controls-filter-select v-show="showSortFilters" v-model="settings.filterBy" class="w-48 h-7.5 ml-4" @change="updateFilter" />
<controls-order-select v-show="showSortFilters" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-48 h-7.5 ml-4" @change="updateOrder" />
</template>
<template v-else>
<div @click="searchBackArrow" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 cursor-pointer">
<span class="material-icons text-3xl text-white">west</span> <span class="material-icons text-3xl text-white">west</span>
</div> </div>
<!-- <span class="material-icons text-2xl cursor-pointer" @click="seriesBackArrow">west</span> --> <!-- <p class="font-book pl-4">{{ numShowing }} showing</p> -->
<p class="pl-4 font-book text-lg"> <div class="flex-grow" />
{{ selectedSeries }} <span class="ml-3 font-mono text-lg bg-black bg-opacity-30 rounded-lg px-1 py-0.5">{{ numShowing }}</span> <p>Search results for "{{ searchQuery }}"</p>
</p> <div class="flex-grow" />
</div> </template>
<div class="flex-grow" />
<ui-text-input v-show="showSortFilters" v-model="_keywordFilter" placeholder="Keyword Filter" :padding-y="1.5" class="text-xs w-40" />
<controls-filter-select v-show="showSortFilters" v-model="settings.filterBy" class="w-48 h-7.5 ml-4" @change="updateFilter" />
<controls-order-select v-show="showSortFilters" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-48 h-7.5 ml-4" @change="updateOrder" />
</div> </div>
</div> </div>
</template> </template>
@ -24,7 +35,12 @@
export default { export default {
props: { props: {
page: String, page: String,
selectedSeries: String selectedSeries: String,
searchResults: {
type: Array,
default: () => []
},
searchQuery: String
}, },
data() { data() {
return { return {
@ -39,6 +55,8 @@ export default {
numShowing() { numShowing() {
if (this.page === '') { if (this.page === '') {
return this.$store.getters['audiobooks/getFiltered']().length return this.$store.getters['audiobooks/getFiltered']().length
} else if (this.page === 'search') {
return (this.searchResults || []).length
} else { } else {
var groups = this.$store.getters['audiobooks/getSeriesGroups']() var groups = this.$store.getters['audiobooks/getSeriesGroups']()
if (this.selectedSeries) { if (this.selectedSeries) {
@ -65,6 +83,9 @@ export default {
} }
}, },
methods: { methods: {
searchBackArrow() {
this.$router.replace('/library')
},
seriesBackArrow() { seriesBackArrow() {
this.$router.replace('/library/series') this.$router.replace('/library/series')
this.$emit('update:selectedSeries', null) this.$emit('update:selectedSeries', null)

View File

@ -29,7 +29,7 @@
</div> </div>
</div> </div>
<div v-show="!isSelectionMode" class="absolute bottom-0 left-0 h-1 shadow-sm" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div> <div v-show="!isSelectionMode" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
<ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0"> <ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0">
<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"> <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">

View File

@ -1,6 +1,8 @@
<template> <template>
<div class="w-64 ml-8 relative"> <div class="w-64 ml-8 relative">
<ui-text-input v-model="search" placeholder="Search.." @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" /> <form @submit.prevent="submitSearch">
<ui-text-input ref="input" v-model="search" placeholder="Search.." @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
</form>
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear"> <div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span> <span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
<span v-else class="material-icons" style="font-size: 1.2rem">close</span> <span v-else class="material-icons" style="font-size: 1.2rem">close</span>
@ -51,6 +53,19 @@ export default {
} }
}, },
methods: { methods: {
submitSearch() {
if (!this.search) return
this.$router.push(`/library/search?query=${this.search}`)
this.search = null
this.items = []
this.showMenu = false
this.$nextTick(() => {
if (this.$refs.input) {
this.$refs.input.blur()
}
})
},
focussed() { focussed() {
this.isFocused = true this.isFocused = true
this.showMenu = true this.showMenu = true
@ -73,6 +88,10 @@ export default {
return [] return []
}) })
this.isFetching = false this.isFetching = false
if (!this.showMenu) {
return
}
this.items = results.map((res) => { this.items = results.map((res) => {
return { return {
id: res.id, id: res.id,

View File

@ -1,11 +1,13 @@
<template> <template>
<modals-modal v-model="show" :width="500" :height="'unset'"> <modals-modal v-model="show" :width="500" :height="'unset'">
<div class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 500px"> <div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<template v-for="chap in chapters"> <template v-for="chap in chapters">
<div :key="chap.id" class="flex items-center px-6 py-3 justify-start cursor-pointer hover:bg-bg relative" :class="chap.id === currentChapterId ? 'bg-bg bg-opacity-80' : 'bg-opacity-20'" @click="clickChapter(chap)"> <div :key="chap.id" :id="`chapter-row-${chap.id}`" class="flex items-center px-6 py-3 justify-start cursor-pointer hover:bg-bg relative" :class="chap.id === currentChapterId ? 'bg-bg bg-opacity-80' : 'bg-opacity-20'" @click="clickChapter(chap)">
{{ chap.title }} {{ chap.title }}
<span class="flex-grow" /> <span class="flex-grow" />
<span class="font-mono text-sm text-gray-300">{{ $secondsToTimestamp(chap.start) }}</span> <span class="font-mono text-sm text-gray-300">{{ $secondsToTimestamp(chap.start) }}</span>
<div v-show="chap.id === currentChapterId" class="w-0.5 h-full absolute top-0 left-0 bg-yellow-400" />
</div> </div>
</template> </template>
</div> </div>
@ -28,6 +30,11 @@ export default {
data() { data() {
return {} return {}
}, },
watch: {
value(newVal) {
this.$nextTick(this.scrollToChapter)
}
},
computed: { computed: {
show: { show: {
get() { get() {
@ -44,8 +51,20 @@ export default {
methods: { methods: {
clickChapter(chap) { clickChapter(chap) {
this.$emit('select', chap) this.$emit('select', chap)
},
scrollToChapter() {
if (!this.currentChapterId) return
var container = this.$refs.container
if (container) {
var currChapterEl = document.getElementById(`chapter-row-${this.currentChapterId}`)
if (currChapterEl) {
var offsetTop = currChapterEl.offsetTop
var containerHeight = container.clientHeight
container.scrollTo({ top: offsetTop - containerHeight / 2 })
}
}
} }
}, }
mounted() {}
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<input v-model="inputValue" :type="type" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="rounded bg-primary text-gray-200 focus:border-gray-500 focus:outline-none border border-gray-600" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" /> <input ref="input" v-model="inputValue" :type="type" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="rounded bg-primary text-gray-200 focus:border-gray-500 focus:outline-none border border-gray-600" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
</template> </template>
<script> <script>
@ -53,6 +53,9 @@ export default {
}, },
keyup(e) { keyup(e) {
this.$emit('keyup', e) this.$emit('keyup', e)
},
blur() {
if (this.$refs.input) this.$refs.input.blur()
} }
}, },
mounted() {} mounted() {}

View File

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "1.2.0", "version": "1.2.1",
"description": "Audiobook manager and player", "description": "Audiobook manager and player",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -3,8 +3,8 @@
<div class="flex h-full"> <div class="flex h-full">
<app-side-rail /> <app-side-rail />
<div class="flex-grow"> <div class="flex-grow">
<app-book-shelf-toolbar :page="id || ''" :selected-series.sync="selectedSeries" /> <app-book-shelf-toolbar :page="id || ''" :search-results="searchResults" :search-query="searchQuery" :selected-series.sync="selectedSeries" />
<app-book-shelf :page="id || ''" :selected-series.sync="selectedSeries" /> <app-book-shelf :page="id || ''" :search-results="searchResults" :search-query="searchQuery" :selected-series.sync="selectedSeries" />
</div> </div>
</div> </div>
</div> </div>
@ -12,24 +12,53 @@
<script> <script>
export default { export default {
asyncData({ params, query, store, app }) { async asyncData({ params, query, store, app }) {
if (query.filter) { if (query.filter) {
store.dispatch('user/updateUserSettings', { filterBy: query.filter }) store.dispatch('user/updateUserSettings', { filterBy: query.filter })
} }
var searchResults = []
var searchQuery = null
if (params.id === 'search' && query.query) {
searchQuery = query.query
searchResults = await app.$axios.$get(`/api/audiobooks?q=${query.query}`).catch((error) => {
console.error('Search error', error)
return []
})
}
return { return {
id: params.id, id: params.id,
searchQuery,
searchResults,
selectedSeries: query.series ? app.$decode(query.series) : null selectedSeries: query.series ? app.$decode(query.series) : null
} }
}, },
data() { data() {
return {} return {}
}, },
watch: {
'$route.query'(newVal) {
if (this.id === 'search' && this.$route.query.query) {
if (this.$route.query.query !== this.searchQuery) {
this.newQuery()
}
}
}
},
computed: { computed: {
streamAudiobook() { streamAudiobook() {
return this.$store.state.streamAudiobook return this.$store.state.streamAudiobook
} }
}, },
methods: {}, methods: {
async newQuery() {
var query = this.$route.query.query
this.searchResults = await this.$axios.$get(`/api/audiobooks?q=${query}`).catch((error) => {
console.error('Search error', error)
return []
})
this.searchQuery = query
}
},
mounted() {} mounted() {}
} }
</script> </script>

View File

@ -5,7 +5,7 @@ function parseSemver(ver) {
if (!ver) return null if (!ver) return null
var groups = ver.match(/^v((([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)$/) var groups = ver.match(/^v((([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)$/)
if (groups && groups.length > 6) { if (groups && groups.length > 6) {
var total = Number(groups[3]) * 100 + Number(groups[4]) * 10 + Number(groups[5]) var total = Number(groups[3]) * 10000 + Number(groups[4]) * 100 + Number(groups[5])
if (isNaN(total)) { if (isNaN(total)) {
console.warn('Invalid version total', groups[3], groups[4], groups[5]) console.warn('Invalid version total', groups[3], groups[4], groups[5])
return null return null

View File

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "1.2.0", "version": "1.2.1",
"description": "Self-hosted audiobook server for managing and playing audiobooks.", "description": "Self-hosted audiobook server for managing and playing audiobooks.",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -100,7 +100,12 @@ class Scanner {
hasUpdatedAudioFiles = true hasUpdatedAudioFiles = true
} }
} else { } else {
newAudioFiles.push(file) var audioFileWithMatchingPath = existingAudiobook.getAudioFileByPath(file.fullPath)
if (audioFileWithMatchingPath) {
Logger.warn(`[Scanner] Audio file with path already exists with different inode, New: "${file.filename}" (${file.ino}) | Existing: ${audioFileWithMatchingPath.filename} (${audioFileWithMatchingPath.ino})`)
} else {
newAudioFiles.push(file)
}
} }
}) })
if (newAudioFiles.length) { if (newAudioFiles.length) {

View File

@ -425,6 +425,10 @@ class Audiobook {
return this.audioFiles.find(af => af.ino === ino) return this.audioFiles.find(af => af.ino === ino)
} }
getAudioFileByPath(fullPath) {
return this.audioFiles.find(af => af.fullPath === fullPath)
}
setChapters() { setChapters() {
// If 1 audio file without chapters, then no chapters will be set // If 1 audio file without chapters, then no chapters will be set