mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-29 15:14:35 +02:00
Add global search, update filters for new filter model
This commit is contained in:
parent
4420375f2a
commit
2c27fb3108
10 changed files with 164 additions and 33 deletions
|
@ -10,8 +10,8 @@ android {
|
|||
applicationId "com.audiobookshelf.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 6
|
||||
versionName "0.2.2-beta"
|
||||
versionCode 7
|
||||
versionName "0.3.0-beta"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
|
|
@ -31,13 +31,13 @@
|
|||
</li>
|
||||
<li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="option">
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="font-normal block truncate py-3">No {{ sublist }}</span>
|
||||
<span class="font-normal block truncate py-5 text-lg">No {{ sublist }} items</span>
|
||||
</div>
|
||||
</li>
|
||||
<template v-for="item in sublistItems">
|
||||
<li :key="item" class="text-gray-50 select-none relative px-2 cursor-pointer hover:bg-black-400" :class="`${sublist}.${item}` === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedSublistOption(item)">
|
||||
<li :key="item.value" class="text-gray-50 select-none relative px-4 cursor-pointer hover:bg-black-400" :class="`${sublist}.${item.value}` === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedSublistOption(item.value)">
|
||||
<div class="flex items-center">
|
||||
<span class="font-normal truncate py-3 text-base">{{ snakeToNormal(item) }}</span>
|
||||
<span class="font-normal truncate py-3 text-base">{{ item.text }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
@ -75,6 +75,11 @@ export default {
|
|||
text: 'Series',
|
||||
value: 'series',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Authors',
|
||||
value: 'authors',
|
||||
sublist: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -108,7 +113,7 @@ export default {
|
|||
return this.selected && this.selected.includes('.') ? this.selected.split('.')[0] : false
|
||||
},
|
||||
genres() {
|
||||
return this.$store.state.audiobooks.genres
|
||||
return this.$store.getters['audiobooks/getGenresUsed']
|
||||
},
|
||||
tags() {
|
||||
return this.$store.state.audiobooks.tags
|
||||
|
@ -116,8 +121,16 @@ export default {
|
|||
series() {
|
||||
return this.$store.state.audiobooks.series
|
||||
},
|
||||
authors() {
|
||||
return this.$store.getters['audiobooks/getUniqueAuthors']
|
||||
},
|
||||
sublistItems() {
|
||||
return this[this.sublist] || []
|
||||
return (this[this.sublist] || []).map((item) => {
|
||||
return {
|
||||
text: item,
|
||||
value: this.$encode(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -126,15 +139,6 @@ export default {
|
|||
this.show = false
|
||||
this.$nextTick(() => this.$emit('change', 'all'))
|
||||
},
|
||||
snakeToNormal(kebab) {
|
||||
if (!kebab) {
|
||||
return 'err'
|
||||
}
|
||||
return String(kebab)
|
||||
.split('_')
|
||||
.map((t) => t.slice(0, 1).toUpperCase() + t.slice(1))
|
||||
.join(' ')
|
||||
},
|
||||
clickedSublistOption(item) {
|
||||
this.clickedOption({ value: `${this.sublist}.${item}` })
|
||||
},
|
||||
|
|
100
components/modals/SearchModal.vue
Normal file
100
components/modals/SearchModal.vue
Normal file
|
@ -0,0 +1,100 @@
|
|||
<template>
|
||||
<modals-modal v-model="show" width="90%" height="100%">
|
||||
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
||||
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20 p-8" style="max-height: 75%" @click.stop>
|
||||
<ui-text-input ref="input" v-model="search" @input="updateSearch" placeholder="Search" class="w-full text-lg" />
|
||||
<div v-show="isFetching" class="w-full py-8 flex justify-center">
|
||||
<p class="text-lg text-gray-400">Fetching...</p>
|
||||
</div>
|
||||
<div v-if="!isFetching && lastSearch && !items.length" class="w-full py-8 flex justify-center">
|
||||
<p class="text-lg text-gray-400">Nothing found</p>
|
||||
</div>
|
||||
<template v-for="item in items">
|
||||
<div class="py-2 border-b border-bg flex" :key="item.id" @click="clickItem(item)">
|
||||
<cards-book-cover :audiobook="item.data" :width="50" />
|
||||
<div class="flex-grow px-4 h-full">
|
||||
<div class="w-full h-full">
|
||||
<p class="text-base truncate">{{ item.data.book.title }}</p>
|
||||
<p class="text-sm text-gray-400 truncate">{{ item.data.book.author }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
search: null,
|
||||
searchTimeout: null,
|
||||
lastSearch: null,
|
||||
isFetching: false,
|
||||
items: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
if (newVal) {
|
||||
this.$nextTick(this.setFocus())
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickItem(item) {
|
||||
this.show = false
|
||||
this.$router.push(`/audiobook/${item.id}`)
|
||||
},
|
||||
async runSearch(value) {
|
||||
this.lastSearch = value
|
||||
if (!this.lastSearch) {
|
||||
this.items = []
|
||||
return
|
||||
}
|
||||
this.isFetching = true
|
||||
var results = await this.$axios.$get(`/api/audiobooks?q=${value}`).catch((error) => {
|
||||
console.error('Search error', error)
|
||||
return []
|
||||
})
|
||||
this.isFetching = false
|
||||
this.items = results.map((res) => {
|
||||
return {
|
||||
id: res.id,
|
||||
data: res,
|
||||
type: 'audiobook'
|
||||
}
|
||||
})
|
||||
},
|
||||
updateSearch(val) {
|
||||
clearTimeout(this.searchTimeout)
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
this.runSearch(val)
|
||||
}, 500)
|
||||
},
|
||||
setFocus() {
|
||||
setTimeout(() => {
|
||||
if (this.$refs.input) {
|
||||
this.$refs.input.focus()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<input v-model="input" :type="type" :disabled="disabled" autocorrect="off" autocapitalize="none" autocomplete="off" :placeholder="placeholder" class="px-2 py-1 bg-bg border border-gray-600 outline-none rounded-sm" :class="disabled ? 'text-gray-300' : 'text-white'" />
|
||||
<input v-model="input" ref="input" autofocus :type="type" :disabled="disabled" autocorrect="off" autocapitalize="none" autocomplete="off" :placeholder="placeholder" class="px-2 py-1 bg-bg border border-gray-600 outline-none rounded-sm" :class="disabled ? 'text-gray-300' : 'text-white'" @keyup="keyup" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -23,7 +23,19 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
methods: {
|
||||
focus() {
|
||||
if (this.$refs.input) {
|
||||
this.$refs.input.focus()
|
||||
this.$refs.input.click()
|
||||
}
|
||||
},
|
||||
keyup() {
|
||||
if (this.$refs.input) {
|
||||
this.input = this.$refs.input.value
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
|
@ -67,11 +67,11 @@ export default {
|
|||
}
|
||||
this.$store.commit('setAppUpdateInfo', result)
|
||||
|
||||
if (result.updateAvailability === 2) {
|
||||
setTimeout(() => {
|
||||
this.showUpdateToast(result.availableVersion, !!result.immediateUpdateAllowed)
|
||||
}, 5000)
|
||||
}
|
||||
// if (result.updateAvailability === 2) {
|
||||
// setTimeout(() => {
|
||||
// this.showUpdateToast(result.availableVersion, !!result.immediateUpdateAllowed)
|
||||
// }, 5000)
|
||||
// }
|
||||
}
|
||||
// parseSemver(ver) {
|
||||
// if (!ver) return null
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "audiobookshelf-app",
|
||||
"version": "v0.2.2-beta",
|
||||
"version": "v0.3.0-beta",
|
||||
"author": "advplyr",
|
||||
"scripts": {
|
||||
"dev": "nuxt --hostname localhost --port 1337",
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
<p class="font-mono pt-1 pb-4">{{ $config.version }}</p>
|
||||
|
||||
<ui-btn v-if="isUpdateAvailable" class="w-full my-4" color="success" @click="clickUpdate"> Version {{ availableVersion }} is available! {{ immediateUpdateAllowed ? 'Update now' : 'Get update from app store' }} </ui-btn>
|
||||
<ui-btn v-if="isUpdateAvailable" class="w-full my-4" color="success" @click="clickUpdate"> Version {{ availableVersion }} is available! Open App Store</ui-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -68,12 +68,8 @@ export default {
|
|||
this.$router.push('/connect')
|
||||
},
|
||||
async clickUpdate() {
|
||||
if (this.immediateUpdateAllowed) {
|
||||
await AppUpdate.performImmediateUpdate()
|
||||
} else {
|
||||
await AppUpdate.openAppStore()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
<div class="w-full h-full">
|
||||
<div class="w-full h-12 relative z-20">
|
||||
<div id="toolbar" class="asolute top-0 left-0 w-full h-full bg-bg flex items-center px-2">
|
||||
<span class="material-icons px-2" @click="showSearchModal = true">search</span>
|
||||
<p class="font-book">{{ numAudiobooks }} Audiobooks</p>
|
||||
|
||||
<div class="flex-grow" />
|
||||
<span class="material-icons px-2" @click="showFilterModal = true">filter_alt</span>
|
||||
<span class="material-icons px-2" @click="showSortModal = true">sort</span>
|
||||
|
@ -12,6 +14,7 @@
|
|||
|
||||
<modals-order-modal v-model="showSortModal" :order-by.sync="settings.orderBy" :descending.sync="settings.orderDesc" @change="updateOrder" />
|
||||
<modals-filter-modal v-model="showFilterModal" :filter-by.sync="settings.filterBy" @change="updateFilter" />
|
||||
<modals-search-modal v-model="showSearchModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -21,6 +24,7 @@ export default {
|
|||
return {
|
||||
showSortModal: false,
|
||||
showFilterModal: false,
|
||||
showSearchModal: false,
|
||||
settings: {}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -38,7 +38,6 @@ Vue.prototype.$secondsToTimestamp = (seconds) => {
|
|||
return `${_hours}:${_minutes.toString().padStart(2, '0')}:${_seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
|
||||
function isClickedOutsideEl(clickEvent, elToCheckOutside, ignoreSelectors = [], ignoreElems = []) {
|
||||
const isDOMElement = (element) => {
|
||||
return element instanceof Element || element instanceof HTMLDocument
|
||||
|
@ -75,3 +74,13 @@ Vue.directive('click-outside', {
|
|||
delete el['__click_outside__']
|
||||
}
|
||||
})
|
||||
|
||||
const encode = (text) => encodeURIComponent(Buffer.from(text).toString('base64'))
|
||||
Vue.prototype.$encode = encode
|
||||
const decode = (text) => Buffer.from(decodeURIComponent(text), 'base64').toString()
|
||||
Vue.prototype.$decode = decode
|
||||
|
||||
export {
|
||||
encode,
|
||||
decode
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { sort } from '@/assets/fastSort'
|
||||
import { decode } 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']
|
||||
|
||||
|
@ -19,7 +20,7 @@ export const getters = {
|
|||
var searchGroups = ['genres', 'tags', 'series', 'authors']
|
||||
var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
||||
if (group) {
|
||||
var filter = filterBy.replace(`${group}.`, '')
|
||||
var filter = decode(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)
|
||||
|
@ -40,6 +41,11 @@ export const getters = {
|
|||
getUniqueAuthors: (state) => {
|
||||
var _authors = state.audiobooks.filter(ab => !!(ab.book && ab.book.author)).map(ab => ab.book.author)
|
||||
return [...new Set(_authors)]
|
||||
},
|
||||
getGenresUsed: (state) => {
|
||||
var _genres = []
|
||||
state.audiobooks.filter(ab => !!(ab.book && ab.book.genres)).forEach(ab => _genres = _genres.concat(ab.book.genres))
|
||||
return [...new Set(_genres)].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue