mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-23 20:25:44 +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"
|
applicationId "com.audiobookshelf.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 6
|
versionCode 7
|
||||||
versionName "0.2.2-beta"
|
versionName "0.3.0-beta"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|
|
@ -31,13 +31,13 @@
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="option">
|
<li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="option">
|
||||||
<div class="flex items-center justify-center">
|
<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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<template v-for="item in sublistItems">
|
<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">
|
<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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
@ -75,6 +75,11 @@ export default {
|
||||||
text: 'Series',
|
text: 'Series',
|
||||||
value: 'series',
|
value: 'series',
|
||||||
sublist: true
|
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
|
return this.selected && this.selected.includes('.') ? this.selected.split('.')[0] : false
|
||||||
},
|
},
|
||||||
genres() {
|
genres() {
|
||||||
return this.$store.state.audiobooks.genres
|
return this.$store.getters['audiobooks/getGenresUsed']
|
||||||
},
|
},
|
||||||
tags() {
|
tags() {
|
||||||
return this.$store.state.audiobooks.tags
|
return this.$store.state.audiobooks.tags
|
||||||
|
@ -116,8 +121,16 @@ export default {
|
||||||
series() {
|
series() {
|
||||||
return this.$store.state.audiobooks.series
|
return this.$store.state.audiobooks.series
|
||||||
},
|
},
|
||||||
|
authors() {
|
||||||
|
return this.$store.getters['audiobooks/getUniqueAuthors']
|
||||||
|
},
|
||||||
sublistItems() {
|
sublistItems() {
|
||||||
return this[this.sublist] || []
|
return (this[this.sublist] || []).map((item) => {
|
||||||
|
return {
|
||||||
|
text: item,
|
||||||
|
value: this.$encode(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -126,15 +139,6 @@ export default {
|
||||||
this.show = false
|
this.show = false
|
||||||
this.$nextTick(() => this.$emit('change', 'all'))
|
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) {
|
clickedSublistOption(item) {
|
||||||
this.clickedOption({ value: `${this.sublist}.${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>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<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() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -67,11 +67,11 @@ export default {
|
||||||
}
|
}
|
||||||
this.$store.commit('setAppUpdateInfo', result)
|
this.$store.commit('setAppUpdateInfo', result)
|
||||||
|
|
||||||
if (result.updateAvailability === 2) {
|
// if (result.updateAvailability === 2) {
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
this.showUpdateToast(result.availableVersion, !!result.immediateUpdateAllowed)
|
// this.showUpdateToast(result.availableVersion, !!result.immediateUpdateAllowed)
|
||||||
}, 5000)
|
// }, 5000)
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
// parseSemver(ver) {
|
// parseSemver(ver) {
|
||||||
// if (!ver) return null
|
// if (!ver) return null
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "v0.2.2-beta",
|
"version": "v0.3.0-beta",
|
||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt --hostname localhost --port 1337",
|
"dev": "nuxt --hostname localhost --port 1337",
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
|
|
||||||
<p class="font-mono pt-1 pb-4">{{ $config.version }}</p>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -68,11 +68,7 @@ export default {
|
||||||
this.$router.push('/connect')
|
this.$router.push('/connect')
|
||||||
},
|
},
|
||||||
async clickUpdate() {
|
async clickUpdate() {
|
||||||
if (this.immediateUpdateAllowed) {
|
await AppUpdate.openAppStore()
|
||||||
await AppUpdate.performImmediateUpdate()
|
|
||||||
} else {
|
|
||||||
await AppUpdate.openAppStore()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
<div class="w-full h-full">
|
<div class="w-full h-full">
|
||||||
<div class="w-full h-12 relative z-20">
|
<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">
|
<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>
|
<p class="font-book">{{ numAudiobooks }} Audiobooks</p>
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<span class="material-icons px-2" @click="showFilterModal = true">filter_alt</span>
|
<span class="material-icons px-2" @click="showFilterModal = true">filter_alt</span>
|
||||||
<span class="material-icons px-2" @click="showSortModal = true">sort</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-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-filter-modal v-model="showFilterModal" :filter-by.sync="settings.filterBy" @change="updateFilter" />
|
||||||
|
<modals-search-modal v-model="showSearchModal" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -21,6 +24,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
showSortModal: false,
|
showSortModal: false,
|
||||||
showFilterModal: false,
|
showFilterModal: false,
|
||||||
|
showSearchModal: false,
|
||||||
settings: {}
|
settings: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -38,7 +38,6 @@ Vue.prototype.$secondsToTimestamp = (seconds) => {
|
||||||
return `${_hours}:${_minutes.toString().padStart(2, '0')}:${_seconds.toString().padStart(2, '0')}`
|
return `${_hours}:${_minutes.toString().padStart(2, '0')}:${_seconds.toString().padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function isClickedOutsideEl(clickEvent, elToCheckOutside, ignoreSelectors = [], ignoreElems = []) {
|
function isClickedOutsideEl(clickEvent, elToCheckOutside, ignoreSelectors = [], ignoreElems = []) {
|
||||||
const isDOMElement = (element) => {
|
const isDOMElement = (element) => {
|
||||||
return element instanceof Element || element instanceof HTMLDocument
|
return element instanceof Element || element instanceof HTMLDocument
|
||||||
|
@ -74,4 +73,14 @@ Vue.directive('click-outside', {
|
||||||
document.removeEventListener('click', el['__click_outside__'], false)
|
document.removeEventListener('click', el['__click_outside__'], false)
|
||||||
delete el['__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 { 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']
|
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 searchGroups = ['genres', 'tags', 'series', 'authors']
|
||||||
var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
||||||
if (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))
|
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 === '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 === 'series') filtered = filtered.filter(ab => ab.book && ab.book.series === filter)
|
||||||
|
@ -40,6 +41,11 @@ export const getters = {
|
||||||
getUniqueAuthors: (state) => {
|
getUniqueAuthors: (state) => {
|
||||||
var _authors = state.audiobooks.filter(ab => !!(ab.book && ab.book.author)).map(ab => ab.book.author)
|
var _authors = state.audiobooks.filter(ab => !!(ab.book && ab.book.author)).map(ab => ab.book.author)
|
||||||
return [...new Set(_authors)]
|
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