mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-29 10:09:11 +01:00
Add:Server setting to set custom sorting prefixes to ignore #358
This commit is contained in:
parent
c75895d711
commit
cfe27dff80
@ -1,112 +0,0 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div class="rounded-sm h-full relative" :style="{ padding: `${paddingY}px ${paddingX}px` }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard">
|
||||
<nuxt-link :to="groupTo" class="cursor-pointer">
|
||||
<div class="w-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'" :style="{ height: coverHeight + 'px', width: coverWidth + 'px' }">
|
||||
<covers-collection-cover ref="groupcover" :book-items="bookItems" :width="coverWidth" :height="coverHeight" />
|
||||
|
||||
<div v-show="isHovering" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
|
||||
<!-- <div class="absolute pointer-events-auto" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem' }" @click.stop.prevent="toggleSelected">
|
||||
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">radio_button_unchecked</span>
|
||||
</div> -->
|
||||
<div class="absolute pointer-events-auto" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickEdit">
|
||||
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
|
||||
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto bottom-0 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, coverWidth) + 'px' }">
|
||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${1 * sizeMultiplier}rem` }">
|
||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ collectionName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
collection: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 120
|
||||
},
|
||||
paddingY: {
|
||||
type: Number,
|
||||
default: 24
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isHovering: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
width(newVal) {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.groupcover && this.$refs.groupcover.init) {
|
||||
this.$refs.groupcover.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labelFontSize() {
|
||||
if (this.coverWidth < 160) return 0.75
|
||||
return 0.875
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
_collection() {
|
||||
return this.collection || {}
|
||||
},
|
||||
groupTo() {
|
||||
return `/collection/${this._collection.id}`
|
||||
},
|
||||
coverWidth() {
|
||||
return this.width * 2
|
||||
},
|
||||
coverHeight() {
|
||||
return this.width * 1.6
|
||||
},
|
||||
sizeMultiplier() {
|
||||
return this.width / 120
|
||||
},
|
||||
paddingX() {
|
||||
return 16 * this.sizeMultiplier
|
||||
},
|
||||
bookItems() {
|
||||
return this._collection.books || []
|
||||
},
|
||||
collectionName() {
|
||||
return this._collection.name || 'No Name'
|
||||
},
|
||||
showExperimentalFeatures() {
|
||||
return this.$store.state.showExperimentalFeatures
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleSelected() {
|
||||
// Selected
|
||||
},
|
||||
clickEdit() {
|
||||
this.$store.commit('globals/setEditCollection', this.collection)
|
||||
},
|
||||
mouseoverCard() {
|
||||
this.isHovering = true
|
||||
},
|
||||
mouseleaveCard() {
|
||||
this.isHovering = false
|
||||
},
|
||||
clickCard() {
|
||||
this.$emit('click', this.collection)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -12,9 +12,6 @@
|
||||
<div class="absolute top-2 right-2 w-7 h-7 rounded-lg bg-black bg-opacity-90 text-gray-300 box-shadow-book flex items-center justify-center border border-white border-opacity-25 pointer-events-none z-40">
|
||||
<p class="font-book text-xl">{{ bookItems.length }}</p>
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 w-full h-1 flex flex-nowrap z-40">
|
||||
<div v-for="userProgress in userProgressItems" :key="userProgress.audiobookId" class="h-full w-full" :class="userProgress.isRead ? 'bg-success' : userProgress.progress > 0 ? 'bg-yellow-400' : ''" />
|
||||
</div>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
@ -100,15 +97,6 @@ export default {
|
||||
bookItems() {
|
||||
return this._group.books || []
|
||||
},
|
||||
userAudiobooks() {
|
||||
return Object.values(this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {})
|
||||
},
|
||||
userProgressItems() {
|
||||
return this.bookItems.map((item) => {
|
||||
var userAudiobook = this.userAudiobooks.find((ab) => ab.audiobookId === item.id)
|
||||
return userAudiobook || {}
|
||||
})
|
||||
},
|
||||
groupName() {
|
||||
return this._group.name || 'No Name'
|
||||
},
|
||||
|
@ -197,28 +197,19 @@ export default {
|
||||
playIconFontSize() {
|
||||
return Math.max(2, 3 * this.sizeMultiplier)
|
||||
},
|
||||
authors() {
|
||||
return this.mediaMetadata.authors || []
|
||||
},
|
||||
author() {
|
||||
if (this.isPodcast) return this.mediaMetadata.author
|
||||
return this.authors.map((au) => au.name).join(', ')
|
||||
return this.mediaMetadata.authorName
|
||||
},
|
||||
authorLF() {
|
||||
return this.authors
|
||||
.map((au) => {
|
||||
var parts = au.name.split(' ')
|
||||
if (parts.length === 1) return parts[0]
|
||||
return `${parts[1]}, ${parts[0]}`
|
||||
})
|
||||
.join(', ')
|
||||
return this.mediaMetadata.authorNameLF
|
||||
},
|
||||
volumeNumber() {
|
||||
return this.mediaMetadata.volumeNumber || null
|
||||
},
|
||||
displayTitle() {
|
||||
if (this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix && this.title.toLowerCase().startsWith('the ')) {
|
||||
return this.title.substr(4) + ', The'
|
||||
if (this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix) {
|
||||
return this.mediaMetadata.titleIgnorePrefix
|
||||
}
|
||||
return this.title
|
||||
},
|
||||
|
@ -47,9 +47,21 @@
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- <div class="flex items-center py-2">
|
||||
<ui-toggle-switch v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
|
||||
<p class="pl-4 text-lg">Ignore prefixes when sorting title and series</p>
|
||||
</div> -->
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
|
||||
<p class="pl-4 text-lg">Ignore prefix "The" when sorting title and series</p>
|
||||
<ui-tooltip :text="tooltips.sortingIgnorePrefix">
|
||||
<p class="pl-4 text-lg">
|
||||
Ignore prefixes when sorting title and series
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<div v-if="newServerSettings.sortingIgnorePrefix" class="w-72 ml-14 mb-2">
|
||||
<ui-multi-select v-model="newServerSettings.sortingPrefixes" small :items="newServerSettings.sortingPrefixes" label="Prefixes to Ignore (case insensitive)" @input="updateSortingPrefixes" :disabled="updatingServerSettings" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
@ -203,6 +215,7 @@ export default {
|
||||
scannerPreferOpfMetadata: 'OPF file metadata will be used for book details over folder names',
|
||||
scannerPreferAudioMetadata: 'Audio file ID3 meta tags will be used for book details over folder names',
|
||||
scannerParseSubtitle: 'Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by " - "<br>i.e. "Book Title - A Subtitle Here" has the subtitle "A Subtitle Here"',
|
||||
sortingIgnorePrefix: 'i.e. for prefix "the" book title "The Book Title" would sort as "Book Title, The"',
|
||||
scannerFindCovers: 'If your audiobook does not have an embedded cover or a cover image inside the folder, the scanner will attempt to find a cover.<br>Note: This will extend scan time',
|
||||
bookshelfView: 'Alternative bookshelf view that shows title & author under book covers',
|
||||
storeCoverWithBook: 'By default covers are stored in /metadata/books, enabling this setting will store covers in the books folder. Only one file named "cover" will be kept',
|
||||
@ -215,7 +228,6 @@ export default {
|
||||
watch: {
|
||||
serverSettings(newVal, oldVal) {
|
||||
if (newVal && !oldVal) {
|
||||
this.newServerSettings = { ...this.serverSettings }
|
||||
this.initServerSettings()
|
||||
}
|
||||
}
|
||||
@ -243,6 +255,16 @@ export default {
|
||||
updateEnableChromecast(val) {
|
||||
this.updateServerSettings({ enableChromecast: val })
|
||||
},
|
||||
updateSortingPrefixes(val) {
|
||||
if (!val || !val.length) {
|
||||
this.$toast.error('Must have at least 1 prefix')
|
||||
return
|
||||
}
|
||||
var prefixes = val.map((prefix) => prefix.trim().toLowerCase())
|
||||
this.updateServerSettings({
|
||||
sortingPrefixes: prefixes
|
||||
})
|
||||
},
|
||||
updateScannerCoverProvider(val) {
|
||||
this.updateServerSettings({
|
||||
scannerCoverProvider: val
|
||||
@ -278,6 +300,7 @@ export default {
|
||||
},
|
||||
initServerSettings() {
|
||||
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
||||
this.newServerSettings.sortingPrefixes = [...(this.newServerSettings.sortingPrefixes || [])]
|
||||
|
||||
this.useSquareBookCovers = this.newServerSettings.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE
|
||||
|
||||
|
@ -43,6 +43,8 @@ class ServerSettings {
|
||||
this.podcastEpisodeSchedule = '0 * * * *' // Every hour
|
||||
|
||||
this.sortingIgnorePrefix = false
|
||||
this.sortingPrefixes = ['the', 'a']
|
||||
|
||||
this.chromecastEnabled = false
|
||||
this.logLevel = Logger.logLevel
|
||||
this.version = null
|
||||
@ -82,6 +84,7 @@ class ServerSettings {
|
||||
this.bookshelfView = settings.bookshelfView || BookshelfView.STANDARD
|
||||
|
||||
this.sortingIgnorePrefix = !!settings.sortingIgnorePrefix
|
||||
this.sortingPrefixes = settings.sortingPrefixes || ['the', 'a']
|
||||
this.chromecastEnabled = !!settings.chromecastEnabled
|
||||
this.logLevel = settings.logLevel || Logger.logLevel
|
||||
this.version = settings.version || null
|
||||
@ -114,6 +117,7 @@ class ServerSettings {
|
||||
coverAspectRatio: this.coverAspectRatio,
|
||||
bookshelfView: this.bookshelfView,
|
||||
sortingIgnorePrefix: this.sortingIgnorePrefix,
|
||||
sortingPrefixes: [...this.sortingPrefixes],
|
||||
chromecastEnabled: this.chromecastEnabled,
|
||||
logLevel: this.logLevel,
|
||||
version: this.version
|
||||
@ -123,7 +127,13 @@ class ServerSettings {
|
||||
update(payload) {
|
||||
var hasUpdates = false
|
||||
for (const key in payload) {
|
||||
if (this[key] !== payload[key]) {
|
||||
if (key === 'sortingPrefixes' && payload[key] && payload[key].length) {
|
||||
var prefixesCleaned = payload[key].filter(prefix => !!prefix).map(prefix => prefix.toLowerCase())
|
||||
if (prefixesCleaned.join(',') !== this[key].join(',')) {
|
||||
this[key] = [...prefixesCleaned]
|
||||
hasUpdates = true
|
||||
}
|
||||
} else if (this[key] !== payload[key]) {
|
||||
if (key === 'logLevel') {
|
||||
Logger.setLogLevel(payload[key])
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ class Book {
|
||||
|
||||
toJSONMinified() {
|
||||
return {
|
||||
metadata: this.metadata.toJSON(),
|
||||
metadata: this.metadata.toJSONMinified(),
|
||||
coverPath: this.coverPath,
|
||||
tags: [...this.tags],
|
||||
numTracks: this.tracks.length,
|
||||
|
@ -59,9 +59,31 @@ class BookMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
toJSONMinified() {
|
||||
return {
|
||||
title: this.title,
|
||||
titleIgnorePrefix: this.titleIgnorePrefix,
|
||||
subtitle: this.subtitle,
|
||||
authorName: this.authorName,
|
||||
authorNameLF: this.authorNameLF,
|
||||
narratorName: this.narratorName,
|
||||
seriesName: this.seriesName,
|
||||
genres: [...this.genres],
|
||||
publishedYear: this.publishedYear,
|
||||
publishedDate: this.publishedDate,
|
||||
publisher: this.publisher,
|
||||
description: this.description,
|
||||
isbn: this.isbn,
|
||||
asin: this.asin,
|
||||
language: this.language,
|
||||
explicit: this.explicit
|
||||
}
|
||||
}
|
||||
|
||||
toJSONExpanded() {
|
||||
return {
|
||||
title: this.title,
|
||||
titleIgnorePrefix: this.titleIgnorePrefix,
|
||||
subtitle: this.subtitle,
|
||||
authors: this.authors.map(a => ({ ...a })), // Author JSONMinimal with name and id
|
||||
narrators: [...this.narrators],
|
||||
@ -88,8 +110,12 @@ class BookMetadata {
|
||||
|
||||
get titleIgnorePrefix() {
|
||||
if (!this.title) return ''
|
||||
if (this.title.toLowerCase().startsWith('the ')) {
|
||||
return this.title.substr(4) + ', The'
|
||||
var prefixesToIgnore = global.ServerSettings.sortingPrefixes || []
|
||||
for (const prefix of prefixesToIgnore) {
|
||||
// e.g. for prefix "the". If title is "The Book Title" return "Book Title, The"
|
||||
if (this.title.toLowerCase().startsWith(`${prefix} `)) {
|
||||
return this.title.substr(prefix.length + 1) + `, ${prefix.substr(0, 1).toUpperCase() + prefix.substr(1)}`
|
||||
}
|
||||
}
|
||||
return this.title
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user