mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-03 00:06:46 +01:00
New filters using base64 strings, keyword filter
This commit is contained in:
parent
af05e78cdf
commit
d2a2f3ff6a
@ -15,7 +15,14 @@
|
|||||||
<span class="material-icons">settings</span>
|
<span class="material-icons">settings</span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<ui-menu :label="username" :items="menuItems" @action="menuAction" class="ml-5" />
|
<nuxt-link to="/account" class="relative w-32 bg-fg border border-gray-500 rounded shadow-sm ml-5 pl-3 pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer hover:bg-bg hover:bg-opacity-40" aria-haspopup="listbox" aria-expanded="true">
|
||||||
|
<span class="flex items-center">
|
||||||
|
<span class="block truncate">{{ username }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
|
<span class="material-icons text-gray-100">person</span>
|
||||||
|
</span>
|
||||||
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="numAudiobooksSelected" class="absolute top-0 left-0 w-full h-full px-4 bg-primary flex items-center">
|
<div v-show="numAudiobooksSelected" class="absolute top-0 left-0 w-full h-full px-4 bg-primary flex items-center">
|
||||||
@ -35,17 +42,6 @@
|
|||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
menuItems: [
|
|
||||||
{
|
|
||||||
value: 'account',
|
|
||||||
text: 'Account',
|
|
||||||
to: '/account'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'logout',
|
|
||||||
text: 'Logout'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
processingBatchDelete: false
|
processingBatchDelete: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -83,20 +79,6 @@ export default {
|
|||||||
this.$router.push('/')
|
this.$router.push('/')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
logout() {
|
|
||||||
this.$axios.$post('/logout').catch((error) => {
|
|
||||||
console.error(error)
|
|
||||||
})
|
|
||||||
if (localStorage.getItem('token')) {
|
|
||||||
localStorage.removeItem('token')
|
|
||||||
}
|
|
||||||
this.$router.push('/login')
|
|
||||||
},
|
|
||||||
menuAction(action) {
|
|
||||||
if (action === 'logout') {
|
|
||||||
this.logout()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cancelSelectionMode() {
|
cancelSelectionMode() {
|
||||||
if (this.processingBatchDelete) return
|
if (this.processingBatchDelete) return
|
||||||
this.$store.commit('setSelectedAudiobooks', [])
|
this.$store.commit('setSelectedAudiobooks', [])
|
||||||
|
@ -24,6 +24,10 @@
|
|||||||
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
|
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<div v-show="!groupedBooks.length" class="w-full py-16 text-center text-xl">
|
||||||
|
<div class="py-4">No Audiobooks</div>
|
||||||
|
<ui-btn v-if="filterBy !== 'all' || keywordFilter" @click="clearFilter">Clear Filter</ui-btn>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -38,10 +42,19 @@ export default {
|
|||||||
currFilterOrderKey: null,
|
currFilterOrderKey: null,
|
||||||
availableSizes: [60, 80, 100, 120, 140, 160, 180, 200, 220],
|
availableSizes: [60, 80, 100, 120, 140, 160, 180, 200, 220],
|
||||||
selectedSizeIndex: 3,
|
selectedSizeIndex: 3,
|
||||||
rowPaddingX: 40
|
rowPaddingX: 40,
|
||||||
|
keywordFilterTimeout: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
keywordFilter() {
|
||||||
|
this.checkKeywordFilter()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
keywordFilter() {
|
||||||
|
return this.$store.state.audiobooks.keywordFilter
|
||||||
|
},
|
||||||
userAudiobooks() {
|
userAudiobooks() {
|
||||||
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
||||||
},
|
},
|
||||||
@ -65,9 +78,28 @@ export default {
|
|||||||
},
|
},
|
||||||
isSelectionMode() {
|
isSelectionMode() {
|
||||||
return this.$store.getters['getNumAudiobooksSelected']
|
return this.$store.getters['getNumAudiobooksSelected']
|
||||||
|
},
|
||||||
|
filterBy() {
|
||||||
|
return this.$store.getters['user/getUserSetting']('filterBy')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
clearFilter() {
|
||||||
|
this.$store.commit('audiobooks/setKeywordFilter', null)
|
||||||
|
if (this.filterBy !== 'all') {
|
||||||
|
this.$store.dispatch('user/updateUserSettings', {
|
||||||
|
filterBy: 'all'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.setGroupedBooks()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
checkKeywordFilter() {
|
||||||
|
clearTimeout(this.keywordFilterTimeout)
|
||||||
|
this.keywordFilterTimeout = setTimeout(() => {
|
||||||
|
this.setGroupedBooks()
|
||||||
|
}, 500)
|
||||||
|
},
|
||||||
increaseSize() {
|
increaseSize() {
|
||||||
this.selectedSizeIndex = Math.min(this.availableSizes.length - 1, this.selectedSizeIndex + 1)
|
this.selectedSizeIndex = Math.min(this.availableSizes.length - 1, this.selectedSizeIndex + 1)
|
||||||
this.resize()
|
this.resize()
|
||||||
|
@ -3,9 +3,12 @@
|
|||||||
<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 class="font-book">{{ numShowing }} Audiobooks</p>
|
<p class="font-book">{{ numShowing }} Audiobooks</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<controls-filter-select v-model="settings.filterBy" class="w-48 h-7.5" @change="updateFilter" />
|
|
||||||
<span class="px-4 text-sm">by</span>
|
<ui-text-input v-model="_keywordFilter" placeholder="Keyword Filter" :padding-y="1.5" class="text-xs w-40" />
|
||||||
<controls-order-select v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-48 h-7.5" @change="updateOrder" />
|
|
||||||
|
<controls-filter-select v-model="settings.filterBy" class="w-48 h-7.5 ml-4" @change="updateFilter" />
|
||||||
|
|
||||||
|
<controls-order-select v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-48 h-7.5 ml-4" @change="updateOrder" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -21,6 +24,14 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
numShowing() {
|
numShowing() {
|
||||||
return this.$store.getters['audiobooks/getFiltered']().length
|
return this.$store.getters['audiobooks/getFiltered']().length
|
||||||
|
},
|
||||||
|
_keywordFilter: {
|
||||||
|
get() {
|
||||||
|
return this.$store.state.audiobooks.keywordFilter
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit('audiobooks/setKeywordFilter', val)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -42,9 +42,9 @@
|
|||||||
</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-primary bg-opacity-50' : ''" role="option" @click="clickedSublistOption(item)">
|
<li :key="item.value" class="text-gray-50 select-none relative px-2 cursor-pointer hover:bg-black-400" :class="`${sublist}.${item.value}` === selected ? 'bg-primary 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-2 text-xs">{{ snakeToNormal(item) }}</span>
|
<span class="font-normal truncate py-2 text-xs">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
@ -81,6 +81,11 @@ export default {
|
|||||||
text: 'Series',
|
text: 'Series',
|
||||||
value: 'series',
|
value: 'series',
|
||||||
sublist: true
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Authors',
|
||||||
|
value: 'authors',
|
||||||
|
sublist: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -109,14 +114,15 @@ export default {
|
|||||||
if (!this.selected) return ''
|
if (!this.selected) return ''
|
||||||
var parts = this.selected.split('.')
|
var parts = this.selected.split('.')
|
||||||
if (parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
return this.snakeToNormal(parts[1])
|
return this.$decode(parts[1])
|
||||||
}
|
}
|
||||||
var _sel = this.items.find((i) => i.value === this.selected)
|
var _sel = this.items.find((i) => i.value === this.selected)
|
||||||
if (!_sel) return ''
|
if (!_sel) return ''
|
||||||
return _sel.text
|
return _sel.text
|
||||||
},
|
},
|
||||||
genres() {
|
genres() {
|
||||||
return this.$store.state.audiobooks.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
|
||||||
@ -124,8 +130,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: {
|
||||||
@ -134,15 +148,6 @@ export default {
|
|||||||
this.showMenu = false
|
this.showMenu = 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(' ')
|
|
||||||
},
|
|
||||||
clickOutside() {
|
clickOutside() {
|
||||||
if (!this.selectedItemSublist) this.sublist = null
|
if (!this.selectedItemSublist) this.sublist = null
|
||||||
this.showMenu = false
|
this.showMenu = false
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<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">
|
<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>
|
<span class="material-icons text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item)">close</span>
|
||||||
</div>
|
</div>
|
||||||
{{ $snakeToNormal(item) }}
|
{{ item }}
|
||||||
</div>
|
</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" />
|
<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>
|
</div>
|
||||||
@ -18,7 +18,7 @@
|
|||||||
<template v-for="item in itemsToShow">
|
<template v-for="item in itemsToShow">
|
||||||
<li :key="item" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
|
<li :key="item" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="font-normal ml-3 block truncate">{{ $snakeToNormal(item) }}</span>
|
<span class="font-normal ml-3 block truncate">{{ item }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="selected.includes(item)" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
<span v-if="selected.includes(item)" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
||||||
<span class="material-icons text-xl">checkmark</span>
|
<span class="material-icons text-xl">checkmark</span>
|
||||||
@ -75,8 +75,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.items.filter((i) => {
|
return this.items.filter((i) => {
|
||||||
var normie = this.$snakeToNormal(i)
|
// var normie = this.$snakeToNormal(i)
|
||||||
var iValue = String(normie).toLowerCase()
|
var iValue = String(i).toLowerCase()
|
||||||
return iValue.includes(this.currentSearch.toLowerCase())
|
return iValue.includes(this.currentSearch.toLowerCase())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -170,8 +170,8 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
insertNewItem(item) {
|
insertNewItem(item) {
|
||||||
var kebabItem = this.$normalToSnake(item)
|
// var kebabItem = this.$normalToSnake(item)
|
||||||
this.selected.push(kebabItem)
|
this.selected.push(item)
|
||||||
this.$emit('input', this.selected)
|
this.$emit('input', this.selected)
|
||||||
this.textInput = null
|
this.textInput = null
|
||||||
this.currentSearch = null
|
this.currentSearch = null
|
||||||
@ -183,9 +183,9 @@ export default {
|
|||||||
if (!this.textInput) return
|
if (!this.textInput) return
|
||||||
|
|
||||||
var cleaned = this.textInput.toLowerCase().trim()
|
var cleaned = this.textInput.toLowerCase().trim()
|
||||||
var cleanedKebab = this.$normalToSnake(cleaned)
|
// var cleanedKebab = this.$normalToSnake(cleaned)
|
||||||
var matchesItem = this.items.find((i) => {
|
var matchesItem = this.items.find((i) => {
|
||||||
return i === cleaned || cleanedKebab === i
|
return i === cleaned
|
||||||
})
|
})
|
||||||
if (matchesItem) {
|
if (matchesItem) {
|
||||||
this.clickedOption(null, matchesItem)
|
this.clickedOption(null, matchesItem)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<input v-model="inputValue" :type="type" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="py-2 px-3 rounded bg-primary text-gray-200 focus:border-gray-500 focus:outline-none" :class="transparent ? '' : 'border border-gray-600'" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
|
<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>
|
||||||
@ -12,8 +12,15 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'text'
|
default: 'text'
|
||||||
},
|
},
|
||||||
transparent: Boolean,
|
disabled: Boolean,
|
||||||
disabled: Boolean
|
paddingY: {
|
||||||
|
type: Number,
|
||||||
|
default: 2
|
||||||
|
},
|
||||||
|
paddingX: {
|
||||||
|
type: Number,
|
||||||
|
default: 3
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
@ -26,6 +33,12 @@ export default {
|
|||||||
set(val) {
|
set(val) {
|
||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
classList() {
|
||||||
|
var _list = []
|
||||||
|
_list.push(`px-${this.paddingX}`)
|
||||||
|
_list.push(`py-${this.paddingY}`)
|
||||||
|
return _list.join(' ')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full p-8">
|
<div class="w-full h-full p-8">
|
||||||
<div class="w-full max-w-2xl mx-auto">
|
<div class="w-full max-w-xl mx-auto">
|
||||||
<h1 class="text-2xl">Account</h1>
|
<h1 class="text-2xl">Account</h1>
|
||||||
|
|
||||||
<div class="my-4">
|
<div class="my-4">
|
||||||
@ -27,6 +27,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="py-4 mt-8 flex">
|
||||||
|
<ui-btn color="primary flex items-center text-lg" @click="logout"><span class="material-icons mr-4 icon-text">logout</span>Logout</ui-btn>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -56,6 +60,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
logout() {
|
||||||
|
this.$axios.$post('/logout').catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
})
|
||||||
|
if (localStorage.getItem('token')) {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
}
|
||||||
|
this.$router.push('/login')
|
||||||
|
},
|
||||||
resetForm() {
|
resetForm() {
|
||||||
this.password = null
|
this.password = null
|
||||||
this.newPassword = null
|
this.newPassword = null
|
||||||
|
@ -7,6 +7,9 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
streamAudiobook() {
|
streamAudiobook() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamAudiobook
|
||||||
|
@ -57,6 +57,11 @@ Vue.prototype.$normalToSnake = (normie) => {
|
|||||||
.join('_')
|
.join('_')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
const availableChars = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
|
const availableChars = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
|
||||||
const getCharCode = (char) => availableChars.indexOf(char)
|
const getCharCode = (char) => availableChars.indexOf(char)
|
||||||
const getCharFromCode = (code) => availableChars[Number(code)] || -1
|
const getCharFromCode = (code) => availableChars[Number(code)] || -1
|
||||||
@ -109,21 +114,6 @@ Vue.prototype.$codeToString = (code) => {
|
|||||||
return finalform
|
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) {
|
function loadImageBlob(uri) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const img = document.createElement('img')
|
const img = document.createElement('img')
|
||||||
@ -204,3 +194,8 @@ Vue.prototype.$sanitizeFilename = (input, replacement = '') => {
|
|||||||
.replace(windowsTrailingRe, replacement);
|
.replace(windowsTrailingRe, replacement);
|
||||||
return sanitized
|
return sanitized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
encode,
|
||||||
|
decode
|
||||||
|
}
|
@ -1,14 +1,17 @@
|
|||||||
import { sort } from '@/assets/fastSort'
|
import { sort } from '@/assets/fastSort'
|
||||||
import { cleanFilterString } from '@/plugins/init.client'
|
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']
|
||||||
|
|
||||||
|
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']
|
||||||
|
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
audiobooks: [],
|
audiobooks: [],
|
||||||
listeners: [],
|
listeners: [],
|
||||||
genres: [...STANDARD_GENRES],
|
genres: [...STANDARD_GENRES],
|
||||||
tags: [],
|
tags: [],
|
||||||
series: []
|
series: [],
|
||||||
|
keywordFilter: null
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
@ -20,12 +23,19 @@ 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)
|
||||||
else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.author === filter)
|
else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.author === filter)
|
||||||
}
|
}
|
||||||
|
if (state.keywordFilter) {
|
||||||
|
const keywordFilterKeys = ['title', 'subtitle', 'author', 'series', 'narrarator']
|
||||||
|
return filtered.filter(ab => {
|
||||||
|
if (!ab.book) return false
|
||||||
|
return !!keywordFilterKeys.find(key => (ab.book[key] && ab.book[key].includes(state.keywordFilter)))
|
||||||
|
})
|
||||||
|
}
|
||||||
return filtered
|
return filtered
|
||||||
},
|
},
|
||||||
getFilteredAndSorted: (state, getters, rootState) => () => {
|
getFilteredAndSorted: (state, getters, rootState) => () => {
|
||||||
@ -40,7 +50,12 @@ 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)].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
|
||||||
|
},
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,6 +79,9 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
|
setKeywordFilter(state, val) {
|
||||||
|
state.keywordFilter = val
|
||||||
|
},
|
||||||
set(state, audiobooks) {
|
set(state, audiobooks) {
|
||||||
// GENRES
|
// GENRES
|
||||||
var genres = [...state.genres]
|
var genres = [...state.genres]
|
||||||
|
@ -5,7 +5,8 @@ module.exports = {
|
|||||||
options: {
|
options: {
|
||||||
safelist: [
|
safelist: [
|
||||||
'bg-success',
|
'bg-success',
|
||||||
'bg-red-600'
|
'bg-red-600',
|
||||||
|
'py-1.5'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"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": {
|
||||||
|
Loading…
Reference in New Issue
Block a user