mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-04-20 01:17:45 +02:00
Merge pull request #1105 from ruoti/collapseseries-patch
Patching handling of titles with multiple series
This commit is contained in:
commit
8319891c96
@ -28,13 +28,19 @@
|
|||||||
<span class="font-mono">{{ numShowing }}</span>
|
<span class="font-mono">{{ numShowing }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn color="primary" small :loading="processingSeries" class="flex items-center" @click="markSeriesFinished">
|
<ui-checkbox v-model="settings.collapseBookSeries" label="Collapse Series" checkbox-bg="bg"
|
||||||
|
check-color="white" small class="mr-2" @input="updateCollapseBookSeries" />
|
||||||
|
<ui-btn color="primary" small :loading="processingSeries" class="flex items-center ml-1 sm:ml-4"
|
||||||
|
@click="markSeriesFinished">
|
||||||
<div class="h-5 w-5">
|
<div class="h-5 w-5">
|
||||||
<svg v-if="isSeriesFinished" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
|
<svg v-if="isSeriesFinished" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||||
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
|
fill="rgb(63, 181, 68)">
|
||||||
|
<path
|
||||||
|
d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
|
||||||
</svg>
|
</svg>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-7 19.6l-7-4.66V3h14v12.93l-7 4.67zm-2.01-7.42l-2.58-2.59L6 12l4 4 8-8-1.42-1.42z" />
|
<path
|
||||||
|
d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-7 19.6l-7-4.66V3h14v12.93l-7 4.67zm-2.01-7.42l-2.58-2.59L6 12l4 4 8-8-1.42-1.42z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<span class="pl-2"> Mark Series {{ isSeriesFinished ? 'Not Finished' : 'Finished' }}</span>
|
<span class="pl-2"> Mark Series {{ isSeriesFinished ? 'Not Finished' : 'Finished' }}</span>
|
||||||
@ -280,6 +286,9 @@ export default {
|
|||||||
updateCollapseSeries() {
|
updateCollapseSeries() {
|
||||||
this.saveSettings()
|
this.saveSettings()
|
||||||
},
|
},
|
||||||
|
updateCollapseBookSeries() {
|
||||||
|
this.saveSettings()
|
||||||
|
},
|
||||||
saveSettings() {
|
saveSettings() {
|
||||||
this.$store.dispatch('user/updateUserSettings', this.settings)
|
this.$store.dispatch('user/updateUserSettings', this.settings)
|
||||||
},
|
},
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="bookshelf" class="w-full overflow-y-auto">
|
<div id="bookshelf" class="w-full overflow-y-auto">
|
||||||
<template v-for="shelf in totalShelves">
|
<template v-for="shelf in totalShelves">
|
||||||
<div :key="shelf" :id="`shelf-${shelf - 1}`" class="w-full px-4 sm:px-8 relative" :class="{ bookshelfRow: !isAlternativeBookshelfView }" :style="{ height: shelfHeight + 'px' }">
|
<div :key="shelf" :id="`shelf-${shelf - 1}`" class="w-full px-4 sm:px-8 relative"
|
||||||
<div v-if="!isAlternativeBookshelfView" class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-20" :class="`h-${shelfDividerHeightIndex}`" />
|
:class="{ bookshelfRow: !isAlternativeBookshelfView }" :style="{ height: shelfHeight + 'px' }">
|
||||||
|
<div v-if="!isAlternativeBookshelfView" class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-20"
|
||||||
|
:class="`h-${shelfDividerHeightIndex}`" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="initialized && !totalShelves && !hasFilter && entityName === 'books'" class="w-full flex flex-col items-center justify-center py-12">
|
<div v-if="initialized && !totalShelves && !hasFilter && entityName === 'books'"
|
||||||
|
class="w-full flex flex-col items-center justify-center py-12">
|
||||||
<p class="text-center text-2xl font-book mb-4 py-4">{{ libraryName }} Library is empty!</p>
|
<p class="text-center text-2xl font-book mb-4 py-4">{{ libraryName }} Library is empty!</p>
|
||||||
<div v-if="userIsAdminOrUp" class="flex">
|
<div v-if="userIsAdminOrUp" class="flex">
|
||||||
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
|
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
|
||||||
@ -119,6 +122,9 @@ export default {
|
|||||||
collapseSeries() {
|
collapseSeries() {
|
||||||
return this.$store.getters['user/getUserSetting']('collapseSeries')
|
return this.$store.getters['user/getUserSetting']('collapseSeries')
|
||||||
},
|
},
|
||||||
|
collapseBookSeries() {
|
||||||
|
return this.$store.getters['user/getUserSetting']('collapseBookSeries')
|
||||||
|
},
|
||||||
coverAspectRatio() {
|
coverAspectRatio() {
|
||||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
@ -319,7 +325,6 @@ export default {
|
|||||||
this.totalEntities = payload.total
|
this.totalEntities = payload.total
|
||||||
this.totalShelves = Math.ceil(this.totalEntities / this.entitiesPerShelf)
|
this.totalShelves = Math.ceil(this.totalEntities / this.entitiesPerShelf)
|
||||||
this.entities = new Array(this.totalEntities)
|
this.entities = new Array(this.totalEntities)
|
||||||
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < payload.results.length; i++) {
|
for (let i = 0; i < payload.results.length; i++) {
|
||||||
@ -329,8 +334,21 @@ export default {
|
|||||||
this.entityComponentRefs[index].setEntity(this.entities[index])
|
this.entityComponentRefs[index].setEntity(this.entities[index])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$eventBus.$emit('bookshelf-total-entities', this.getEntitiesCount())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getEntitiesCount() {
|
||||||
|
let uniqueEntities = new Set()
|
||||||
|
this.entities.forEach(entity => {
|
||||||
|
if (entity.collapsedSeries) {
|
||||||
|
entity.collapsedSeries.libraryItemIds.forEach(uniqueEntities.add, uniqueEntities)
|
||||||
|
} else {
|
||||||
|
uniqueEntities.add(entity.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return uniqueEntities.size
|
||||||
|
},
|
||||||
loadPage(page) {
|
loadPage(page) {
|
||||||
this.pagesLoaded[page] = true
|
this.pagesLoaded[page] = true
|
||||||
this.fetchEntites(page)
|
this.fetchEntites(page)
|
||||||
@ -437,6 +455,9 @@ export default {
|
|||||||
searchParams.set('filter', this.seriesFilterBy)
|
searchParams.set('filter', this.seriesFilterBy)
|
||||||
} else if (this.page === 'series-books') {
|
} else if (this.page === 'series-books') {
|
||||||
searchParams.set('filter', `series.${this.$encode(this.seriesId)}`)
|
searchParams.set('filter', `series.${this.$encode(this.seriesId)}`)
|
||||||
|
if (this.collapseBookSeries) {
|
||||||
|
searchParams.set('collapseseries', 1)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.filterBy && this.filterBy !== 'all') {
|
if (this.filterBy && this.filterBy !== 'all') {
|
||||||
searchParams.set('filter', this.filterBy)
|
searchParams.set('filter', this.filterBy)
|
||||||
@ -452,8 +473,6 @@ export default {
|
|||||||
return searchParams.toString()
|
return searchParams.toString()
|
||||||
},
|
},
|
||||||
checkUpdateSearchParams() {
|
checkUpdateSearchParams() {
|
||||||
if (this.page === 'series-books') return false
|
|
||||||
|
|
||||||
var newSearchParams = this.buildSearchParams()
|
var newSearchParams = this.buildSearchParams()
|
||||||
var currentQueryString = window.location.search
|
var currentQueryString = window.location.search
|
||||||
if (currentQueryString && currentQueryString.startsWith('?')) currentQueryString = currentQueryString.slice(1)
|
if (currentQueryString && currentQueryString.startsWith('?')) currentQueryString = currentQueryString.slice(1)
|
||||||
@ -513,7 +532,7 @@ export default {
|
|||||||
if (indexOf >= 0) {
|
if (indexOf >= 0) {
|
||||||
this.entities = this.entities.filter((ent) => ent.id !== libraryItem.id)
|
this.entities = this.entities.filter((ent) => ent.id !== libraryItem.id)
|
||||||
this.totalEntities = this.entities.length
|
this.totalEntities = this.entities.length
|
||||||
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
|
this.$eventBus.$emit('bookshelf-total-entities', this.getEntitiesCount())
|
||||||
this.executeRebuild()
|
this.executeRebuild()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -551,7 +570,7 @@ export default {
|
|||||||
if (indexOf >= 0) {
|
if (indexOf >= 0) {
|
||||||
this.entities = this.entities.filter((ent) => ent.id !== collection.id)
|
this.entities = this.entities.filter((ent) => ent.id !== collection.id)
|
||||||
this.totalEntities = this.entities.length
|
this.totalEntities = this.entities.length
|
||||||
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
|
this.$eventBus.$emit('bookshelf-total-entities', this.getEntitiesCount())
|
||||||
this.executeRebuild()
|
this.executeRebuild()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -715,6 +734,7 @@ export default {
|
|||||||
.bookshelfRow {
|
.bookshelfRow {
|
||||||
background-image: var(--bookshelf-texture-img);
|
background-image: var(--bookshelf-texture-img);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bookshelfDivider {
|
.bookshelfDivider {
|
||||||
background: rgb(149, 119, 90);
|
background: rgb(149, 119, 90);
|
||||||
background: var(--bookshelf-divider-bg);
|
background: var(--bookshelf-divider-bg);
|
||||||
|
@ -1,106 +1,164 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="card" :id="`book-card-${index}`" :style="{ minWidth: width + 'px', maxWidth: width + 'px', height: height + 'px' }" class="rounded-sm z-10 bg-primary cursor-pointer box-shadow-book" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
<div ref="card" :id="`book-card-${index}`"
|
||||||
|
:style="{ minWidth: width + 'px', maxWidth: width + 'px', height: height + 'px' }"
|
||||||
|
class="rounded-sm z-10 bg-primary cursor-pointer box-shadow-book" @mousedown.prevent @mouseup.prevent
|
||||||
|
@mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
<!-- When cover image does not fill -->
|
<!-- When cover image does not fill -->
|
||||||
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
||||||
<div class="absolute cover-bg" ref="coverBg" />
|
<div class="absolute cover-bg" ref="coverBg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Alternative bookshelf title/author/sort -->
|
<!-- Alternative bookshelf title/author/sort -->
|
||||||
<div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
|
<div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full"
|
||||||
|
:style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
|
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
|
||||||
{{ displayTitle }}
|
{{ displayTitle }}
|
||||||
</p>
|
</p>
|
||||||
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || ' ' }}</p>
|
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo ||
|
||||||
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
' '
|
||||||
|
}}</p>
|
||||||
|
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{
|
||||||
|
displaySortLine
|
||||||
|
}}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="booksInSeries" class="absolute z-20 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ booksInSeries }}</div>
|
<div v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right"
|
||||||
|
:style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }"
|
||||||
|
style="background-color: #78350f">
|
||||||
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequenceList }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="booksInSeries" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20"
|
||||||
|
:style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }"
|
||||||
|
style="background-color: #cd9d49dd">
|
||||||
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ booksInSeries }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
||||||
<div v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
|
<div v-show="libraryItem && !imageReady"
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
|
class="absolute top-0 left-0 w-full h-full flex items-center justify-center"
|
||||||
|
:style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
|
||||||
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cover Image -->
|
<!-- Cover Image -->
|
||||||
<img v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
<img v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300"
|
||||||
|
:class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded"
|
||||||
|
:style="{ opacity: imageReady ? 1 : 0 }" />
|
||||||
|
|
||||||
<!-- Placeholder Cover Title & Author -->
|
<!-- Placeholder Cover Title & Author -->
|
||||||
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="!hasCover"
|
||||||
|
class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center"
|
||||||
|
:style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-center font-book" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p>
|
<p class="text-center font-book" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">
|
||||||
|
{{ titleCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center"
|
||||||
<p class="text-center font-book" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
|
:style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
||||||
|
<p class="text-center font-book" style="color: rgb(247 223 187); opacity: 0.75"
|
||||||
|
:style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- No progress shown for collapsed series in library and podcasts (unless showing podcast episode) -->
|
<!-- No progress shown for collapsed series in library and podcasts (unless showing podcast episode) -->
|
||||||
<div v-if="!booksInSeries && (!isPodcast || episodeProgress)" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
<div v-if="!booksInSeries && (!isPodcast || episodeProgress)"
|
||||||
|
class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b"
|
||||||
|
:class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }">
|
||||||
|
</div>
|
||||||
<!-- Finished progress bar for collapsed series -->
|
<!-- Finished progress bar for collapsed series -->
|
||||||
<div v-else-if="booksInSeries && seriesIsFinished" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b bg-success" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
<div v-else-if="booksInSeries && seriesIsFinished"
|
||||||
|
class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b bg-success"
|
||||||
|
:style="{ width: width * userProgressPercent + 'px' }"></div>
|
||||||
|
|
||||||
<!-- Overlay is not shown if collapsing series in library -->
|
<!-- Overlay is not shown if collapsing series in library -->
|
||||||
<div v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen) && !processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded hidden md:block" :class="overlayWrapperClasslist">
|
<div v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen) && !processing"
|
||||||
|
class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded hidden md:block"
|
||||||
|
:class="overlayWrapperClasslist">
|
||||||
<div v-show="showPlayButton" class="h-full flex items-center justify-center pointer-events-none">
|
<div v-show="showPlayButton" class="h-full flex items-center justify-center pointer-events-none">
|
||||||
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="play">
|
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto"
|
||||||
|
@click.stop.prevent="play">
|
||||||
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
|
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="showReadButton" class="h-full flex items-center justify-center pointer-events-none">
|
<div v-show="showReadButton" class="h-full flex items-center justify-center pointer-events-none">
|
||||||
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="clickReadEBook">
|
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto"
|
||||||
|
@click.stop.prevent="clickReadEBook">
|
||||||
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">auto_stories</span>
|
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">auto_stories</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="userCanUpdate" v-show="!isSelectionMode" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-150 top-0 right-0" :style="{ padding: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="editClick">
|
<div v-if="userCanUpdate" v-show="!isSelectionMode"
|
||||||
|
class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-150 top-0 right-0"
|
||||||
|
:style="{ padding: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="editClick">
|
||||||
<span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">edit</span>
|
<span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">edit</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="selectBtnClick">
|
<div class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100"
|
||||||
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
|
:style="{ top: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }"
|
||||||
|
@click.stop.prevent="selectBtnClick">
|
||||||
|
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''"
|
||||||
|
:style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' :
|
||||||
|
'radio_button_unchecked'
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- More Menu Icon -->
|
<!-- More Menu Icon -->
|
||||||
<div ref="moreIcon" v-show="!isSelectionMode" class="hidden md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-150" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
|
<div ref="moreIcon" v-show="!isSelectionMode"
|
||||||
|
class="hidden md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-150"
|
||||||
|
:style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }"
|
||||||
|
@click.stop.prevent="clickShowMore">
|
||||||
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
|
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Processing/loading spinner overlay -->
|
<!-- Processing/loading spinner overlay -->
|
||||||
<div v-if="processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 rounded flex items-center justify-center">
|
<div v-if="processing"
|
||||||
|
class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 rounded flex items-center justify-center">
|
||||||
<widgets-loading-spinner size="la-lg" />
|
<widgets-loading-spinner size="la-lg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Series name overlay -->
|
<!-- Series name overlay -->
|
||||||
<div v-if="booksInSeries && libraryItem && isHovering" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-60 rounded flex items-center justify-center" :style="{ padding: sizeMultiplier + 'rem' }">
|
<div v-if="booksInSeries && libraryItem && isHovering"
|
||||||
|
class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-60 rounded flex items-center justify-center"
|
||||||
|
:style="{ padding: sizeMultiplier + 'rem' }">
|
||||||
<p class="text-gray-200 text-center" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">{{ series }}</p>
|
<p class="text-gray-200 text-center" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">{{ series }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error widget -->
|
<!-- Error widget -->
|
||||||
<ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0 z-10">
|
<ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0 z-10">
|
||||||
<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' }"
|
||||||
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
|
class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
|
||||||
|
<span class="material-icons text-red-100 pr-1"
|
||||||
|
:style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
|
||||||
</div>
|
</div>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<div v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" :style="{ padding: 0.375 * sizeMultiplier + 'rem' }">
|
<div v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10"
|
||||||
|
:style="{ padding: 0.375 * sizeMultiplier + 'rem' }">
|
||||||
<span class="material-icons" :style="{ fontSize: sizeMultiplier * 1.5 + 'rem' }">rss_feed</span>
|
<span class="material-icons" :style="{ fontSize: sizeMultiplier * 1.5 + 'rem' }">rss_feed</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Series sequence -->
|
<!-- Series sequence -->
|
||||||
<div v-if="seriesSequence && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
<div v-if="seriesSequence && !isHovering && !isSelectionMode"
|
||||||
|
class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10"
|
||||||
|
:style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequence }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequence }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Podcast Episode # -->
|
<!-- Podcast Episode # -->
|
||||||
<div v-if="recentEpisodeNumber && !isHovering && !isSelectionMode && !processing" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
<div v-if="recentEpisodeNumber && !isHovering && !isSelectionMode && !processing"
|
||||||
|
class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10"
|
||||||
|
:style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">Episode #{{ recentEpisodeNumber }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">Episode #{{ recentEpisodeNumber }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Podcast Num Episodes -->
|
<!-- Podcast Num Episodes -->
|
||||||
<div v-else-if="numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
|
<div v-else-if="numEpisodes && !isHovering && !isSelectionMode"
|
||||||
|
class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center"
|
||||||
|
:style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodes }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodes }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -236,6 +294,9 @@ export default {
|
|||||||
// Only added to item object when collapseSeries is enabled
|
// Only added to item object when collapseSeries is enabled
|
||||||
return this.collapsedSeries ? this.collapsedSeries.numBooks : 0
|
return this.collapsedSeries ? this.collapsedSeries.numBooks : 0
|
||||||
},
|
},
|
||||||
|
seriesSequenceList() {
|
||||||
|
return this.collapsedSeries ? this.collapsedSeries.seriesSequenceList : null
|
||||||
|
},
|
||||||
libraryItemIdsInSeries() {
|
libraryItemIdsInSeries() {
|
||||||
// Only added to item object when collapseSeries is enabled
|
// Only added to item object when collapseSeries is enabled
|
||||||
return this.collapsedSeries ? this.collapsedSeries.libraryItemIds || [] : []
|
return this.collapsedSeries ? this.collapsedSeries.libraryItemIds || [] : []
|
||||||
@ -515,7 +576,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var mediaMetadata = libraryItem.media.metadata
|
var mediaMetadata = libraryItem.media.metadata
|
||||||
if (mediaMetadata.series) {
|
if (mediaMetadata.series && Array.isArray(mediaMetadata.series)) {
|
||||||
var newSeries = mediaMetadata.series.find((se) => se.id === this.series.id)
|
var newSeries = mediaMetadata.series.find((se) => se.id === this.series.id)
|
||||||
if (newSeries) {
|
if (newSeries) {
|
||||||
// update selected series
|
// update selected series
|
||||||
|
@ -1,19 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
<div ref="wrapper" class="relative" v-click-outside="clickOutside">
|
||||||
<button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
<button type="button"
|
||||||
|
class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer"
|
||||||
|
aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label"
|
||||||
|
@click.prevent="showMenu = !showMenu">
|
||||||
<span class="flex items-center justify-between">
|
<span class="flex items-center justify-between">
|
||||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||||
<span class="material-icons text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
<span class="material-icons text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-80 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
|
<ul v-show="showMenu"
|
||||||
|
class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
||||||
|
role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="item in selectItems">
|
<template v-for="item in selectItems">
|
||||||
<li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="item.value === selected ? 'bg-primary bg-opacity-50' : ''" role="option" @click="clickedOption(item.value)">
|
<li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400"
|
||||||
|
:class="item.value === selected ? 'bg-primary bg-opacity-50' : ''" role="option"
|
||||||
|
@click="clickedOption(item.value)">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="font-normal ml-3 block truncate text-xs">{{ item.text }}</span>
|
<span class="font-normal ml-3 block truncate text-xs">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
<span v-if="item.value === selected"
|
||||||
|
class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
||||||
<span class="material-icons text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
<span class="material-icons text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
@ -29,9 +37,7 @@ export default {
|
|||||||
descending: Boolean
|
descending: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
const bookItems = [
|
||||||
showMenu: false,
|
|
||||||
bookItems: [
|
|
||||||
{
|
{
|
||||||
text: 'Title',
|
text: 'Title',
|
||||||
value: 'media.metadata.title'
|
value: 'media.metadata.title'
|
||||||
@ -68,7 +74,17 @@ export default {
|
|||||||
text: 'File Modified',
|
text: 'File Modified',
|
||||||
value: 'mtimeMs'
|
value: 'mtimeMs'
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
|
|
||||||
|
const seriesItems = [...bookItems, {
|
||||||
|
text: 'Sequence',
|
||||||
|
value: 'sequence'
|
||||||
|
}]
|
||||||
|
|
||||||
|
return {
|
||||||
|
showMenu: false,
|
||||||
|
bookItems: bookItems,
|
||||||
|
seriesItems: seriesItems,
|
||||||
podcastItems: [
|
podcastItems: [
|
||||||
{
|
{
|
||||||
text: 'Title',
|
text: 'Title',
|
||||||
@ -122,8 +138,21 @@ export default {
|
|||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
||||||
},
|
},
|
||||||
selectItems() {
|
selectItems() {
|
||||||
if (this.isPodcast) return this.podcastItems
|
let items = null
|
||||||
return this.bookItems
|
if (this.isPodcast) {
|
||||||
|
items = this.podcastItems
|
||||||
|
} else if (this.$store.getters['user/getUserSetting']('filterBy').startsWith('series.')) {
|
||||||
|
items = this.seriesItems
|
||||||
|
} else {
|
||||||
|
items = this.bookItems
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!items.some(i => i.value === this.selected)) {
|
||||||
|
this.selected = items[0].value
|
||||||
|
this.selectedDesc = !this.defaultsToAsc(items[0].value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
},
|
},
|
||||||
selectedText() {
|
selectedText() {
|
||||||
var _selected = this.selected
|
var _selected = this.selected
|
||||||
@ -143,12 +172,19 @@ export default {
|
|||||||
this.selectedDesc = !this.selectedDesc
|
this.selectedDesc = !this.selectedDesc
|
||||||
} else {
|
} else {
|
||||||
this.selected = val
|
this.selected = val
|
||||||
if (val == 'media.metadata.title' || val == 'media.metadata.author' || val == 'media.metadata.authorName' || val == 'media.metadata.authorNameLF') {
|
if (this.defaultsToAsc(val)) this.selectedDesc = false
|
||||||
this.selectedDesc = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.showMenu = false
|
this.showMenu = false
|
||||||
this.$nextTick(() => this.$emit('change', val))
|
this.$nextTick(() => this.$emit('change', val))
|
||||||
|
},
|
||||||
|
defaultsToAsc(val) {
|
||||||
|
return (
|
||||||
|
val == 'media.metadata.title' ||
|
||||||
|
val == 'media.metadata.author' ||
|
||||||
|
val == 'media.metadata.authorName' ||
|
||||||
|
val == 'media.metadata.authorNameLF' ||
|
||||||
|
val == 'sequence'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,8 @@ export const state = () => ({
|
|||||||
filterBy: 'all',
|
filterBy: 'all',
|
||||||
playbackRate: 1,
|
playbackRate: 1,
|
||||||
bookshelfCoverSize: 120,
|
bookshelfCoverSize: 120,
|
||||||
collapseSeries: false
|
collapseSeries: false,
|
||||||
|
collapseBookSeries: false
|
||||||
},
|
},
|
||||||
settingsListeners: [],
|
settingsListeners: [],
|
||||||
collections: [],
|
collections: [],
|
||||||
|
@ -160,21 +160,42 @@ class LibraryController {
|
|||||||
minified: req.query.minified === '1',
|
minified: req.query.minified === '1',
|
||||||
collapseseries: req.query.collapseseries === '1'
|
collapseseries: req.query.collapseseries === '1'
|
||||||
}
|
}
|
||||||
|
var mediaIsBook = payload.mediaType === 'book'
|
||||||
|
|
||||||
|
// Step 1 - Filter the retrieved library items
|
||||||
var filterSeries = null
|
var filterSeries = null
|
||||||
if (payload.filterBy) {
|
if (payload.filterBy) {
|
||||||
// If filtering by series, will include seriesName and seriesSequence on media metadata
|
|
||||||
filterSeries = (payload.mediaType == 'book' && payload.filterBy.startsWith('series.')) ? libraryHelpers.decode(payload.filterBy.replace('series.', '')) : null
|
|
||||||
if (filterSeries === 'No Series') filterSeries = null
|
|
||||||
|
|
||||||
libraryItems = libraryHelpers.getFilteredLibraryItems(libraryItems, payload.filterBy, req.user, this.rssFeedManager.feedsArray)
|
libraryItems = libraryHelpers.getFilteredLibraryItems(libraryItems, payload.filterBy, req.user, this.rssFeedManager.feedsArray)
|
||||||
payload.total = libraryItems.length
|
payload.total = libraryItems.length
|
||||||
|
|
||||||
|
// Determining if we are filtering titles by a series, and if so, which series
|
||||||
|
filterSeries = (mediaIsBook && payload.filterBy.startsWith('series.')) ? libraryHelpers.decode(payload.filterBy.replace('series.', '')) : null
|
||||||
|
if (filterSeries === 'No Series') filterSeries = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2 - If selected, collapse library items by the series they belong to.
|
||||||
|
// If also filtering by series, will not collapse the filtered series as this would lead
|
||||||
|
// to series having a collapsed series that is just that series.
|
||||||
|
if (payload.collapseseries) {
|
||||||
|
let collapsedItems = libraryHelpers.collapseBookSeries(libraryItems, this.db.series, filterSeries)
|
||||||
|
|
||||||
|
if (!(collapsedItems.length == 1 && collapsedItems[0].collapsedSeries)) {
|
||||||
|
libraryItems = collapsedItems
|
||||||
|
payload.total = libraryItems.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3 - Sort the retrieved library items.
|
||||||
|
var sortArray = []
|
||||||
|
|
||||||
|
// When on the series page, sort by sequence only
|
||||||
|
if (filterSeries && !payload.sortBy) {
|
||||||
|
sortArray.push({ asc: (li) => li.media.metadata.getSeries(filterSeries).sequence })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.sortBy) {
|
if (payload.sortBy) {
|
||||||
var sortKey = payload.sortBy
|
|
||||||
|
|
||||||
// old sort key TODO: should be mutated in dbMigration
|
// old sort key TODO: should be mutated in dbMigration
|
||||||
|
var sortKey = payload.sortBy
|
||||||
if (sortKey.startsWith('book.')) {
|
if (sortKey.startsWith('book.')) {
|
||||||
sortKey = sortKey.replace('book.', 'media.metadata.')
|
sortKey = sortKey.replace('book.', 'media.metadata.')
|
||||||
}
|
}
|
||||||
@ -186,29 +207,42 @@ class LibraryController {
|
|||||||
sortKey += 'IgnorePrefix'
|
sortKey += 'IgnorePrefix'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start sort
|
// If series are collapsed and not sorting by title or sequence,
|
||||||
var direction = payload.sortDesc ? 'desc' : 'asc'
|
// sort all collapsed series to the end in alphabetical order
|
||||||
var sortArray = [
|
const sortBySequence = filterSeries && (sortKey === 'sequence')
|
||||||
{
|
if (payload.collapseseries && !(sortByTitle || sortBySequence)) {
|
||||||
[direction]: (li) => {
|
sortArray.push({
|
||||||
// When collapsing by series and sorting by title use the series name instead of the book title
|
asc: (li) => {
|
||||||
if (payload.mediaType === 'book' && payload.collapseseries && li.media.metadata.seriesName) {
|
if (li.collapsedSeries) {
|
||||||
if (sortByTitle) {
|
return this.db.serverSettings.sortingIgnorePrefix ?
|
||||||
return this.db.serverSettings.sortingIgnorePrefix ? li.media.metadata.seriesNameIgnorePrefix : li.media.metadata.seriesName
|
li.collapsedSeries.nameIgnorePrefix :
|
||||||
|
li.collapsedSeries.name
|
||||||
} else {
|
} else {
|
||||||
// When not sorting by title always show the collapsed series at the end
|
return ''
|
||||||
return direction === 'desc' ? -1 : 'zzzz'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort series based on the sortBy attribute
|
||||||
|
var direction = payload.sortDesc ? 'desc' : 'asc'
|
||||||
|
sortArray.push({
|
||||||
|
[direction]: (li) => {
|
||||||
|
if (mediaIsBook && sortBySequence) {
|
||||||
|
return li.media.metadata.getSeries(filterSeries).sequence
|
||||||
|
} else if (mediaIsBook && sortByTitle && li.collapsedSeries) {
|
||||||
|
return this.db.serverSettings.sortingIgnorePrefix ?
|
||||||
|
li.collapsedSeries.nameIgnorePrefix :
|
||||||
|
li.collapsedSeries.name
|
||||||
|
} else {
|
||||||
// Supports dot notation strings i.e. "media.metadata.title"
|
// Supports dot notation strings i.e. "media.metadata.title"
|
||||||
return sortKey.split('.').reduce((a, b) => a[b], li)
|
return sortKey.split('.').reduce((a, b) => a[b], li)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
})
|
||||||
|
|
||||||
// Secondary sort when sorting by book author use series sort title
|
// Secondary sort when sorting by book author use series sort title
|
||||||
if (payload.mediaType === 'book' && payload.sortBy.includes('author')) {
|
if (mediaIsBook && payload.sortBy.includes('author')) {
|
||||||
sortArray.push({
|
sortArray.push({
|
||||||
asc: (li) => {
|
asc: (li) => {
|
||||||
if (li.media.metadata.series && li.media.metadata.series.length) {
|
if (li.media.metadata.series && li.media.metadata.series.length) {
|
||||||
@ -218,30 +252,55 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortArray.length) {
|
||||||
libraryItems = naturalSort(libraryItems).by(sortArray)
|
libraryItems = naturalSort(libraryItems).by(sortArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.collapseseries) {
|
// Step 4 - Transform the items to pass to the client side
|
||||||
libraryItems = libraryHelpers.collapseBookSeries(libraryItems, this.db.series)
|
payload.results = libraryItems.map(li => {
|
||||||
payload.total = libraryItems.length
|
let json = payload.minified ? li.toJSONMinified() : li.toJSON()
|
||||||
} else if (filterSeries) {
|
|
||||||
// Book media when filtering series will include series object on media metadata
|
if (li.collapsedSeries) {
|
||||||
libraryItems = libraryItems.map(li => {
|
json.collapsedSeries = {
|
||||||
var series = li.media.metadata.getSeries(filterSeries)
|
id: li.collapsedSeries.id,
|
||||||
var liJson = payload.minified ? li.toJSONMinified() : li.toJSON()
|
name: li.collapsedSeries.name,
|
||||||
liJson.media.metadata.series = series
|
nameIgnorePrefix: li.collapsedSeries.nameIgnorePrefix,
|
||||||
return liJson
|
libraryItemIds: li.collapsedSeries.books.map(b => b.id),
|
||||||
})
|
numBooks: li.collapsedSeries.books.length
|
||||||
libraryItems = naturalSort(libraryItems).asc(li => li.media.metadata.series.sequence)
|
|
||||||
} else {
|
|
||||||
libraryItems = libraryItems.map(li => payload.minified ? li.toJSONMinified() : li.toJSON())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.limit) {
|
// If collapsing by series and filtering by a series, generate the list of sequences the collapsed
|
||||||
var startIndex = payload.page * payload.limit
|
// series represents in the filtered series
|
||||||
libraryItems = libraryItems.slice(startIndex, startIndex + payload.limit)
|
if (filterSeries) {
|
||||||
|
json.collapsedSeries.seriesSequenceList =
|
||||||
|
naturalSort(li.collapsedSeries.books.map(b => b.filterSeriesSequence)).asc()
|
||||||
|
.reduce((ranges, currentSequence) => {
|
||||||
|
let lastRange = ranges.at(-1)
|
||||||
|
let isNumber = /^(\d+|\d+\.\d*|\d*\.\d+)$/.test(currentSequence)
|
||||||
|
if (isNumber) currentSequence = parseFloat(currentSequence)
|
||||||
|
|
||||||
|
if (lastRange && isNumber && lastRange.isNumber && ((lastRange.end + 1) == currentSequence)) {
|
||||||
|
lastRange.end = currentSequence
|
||||||
}
|
}
|
||||||
payload.results = libraryItems
|
else {
|
||||||
|
ranges.push({ start: currentSequence, end: currentSequence, isNumber: isNaN(currentSequence) })
|
||||||
|
}
|
||||||
|
|
||||||
|
return ranges
|
||||||
|
}, [])
|
||||||
|
.map(r => r.start == r.end ? r.start : `${r.start}-${r.end}`)
|
||||||
|
.join(', ')
|
||||||
|
}
|
||||||
|
} else if (filterSeries) {
|
||||||
|
// If filtering by series, make sure to include the series metadata
|
||||||
|
json.media.metadata.series = li.media.metadata.getSeries(filterSeries)
|
||||||
|
}
|
||||||
|
|
||||||
|
return json
|
||||||
|
})
|
||||||
|
|
||||||
res.json(payload)
|
res.json(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,7 +334,7 @@ class LibraryController {
|
|||||||
minified: req.query.minified === '1'
|
minified: req.query.minified === '1'
|
||||||
}
|
}
|
||||||
|
|
||||||
var series = libraryHelpers.getSeriesFromBooks(libraryItems, this.db.series, payload.filterBy, req.user, payload.minified)
|
var series = libraryHelpers.getSeriesFromBooks(libraryItems, this.db.series, null, payload.filterBy, req.user, payload.minified)
|
||||||
|
|
||||||
const direction = payload.sortDesc ? 'desc' : 'asc'
|
const direction = payload.sortDesc ? 'desc' : 'asc'
|
||||||
series = naturalSort(series).by([
|
series = naturalSort(series).by([
|
||||||
|
@ -153,7 +153,7 @@ module.exports = {
|
|||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
|
|
||||||
getSeriesFromBooks(books, allSeries, filterBy, user, minified = false) {
|
getSeriesFromBooks(books, allSeries, filterSeries, filterBy, user, minified = false) {
|
||||||
const _series = {}
|
const _series = {}
|
||||||
const seriesToFilterOut = {}
|
const seriesToFilterOut = {}
|
||||||
books.forEach((libraryItem) => {
|
books.forEach((libraryItem) => {
|
||||||
@ -179,6 +179,9 @@ module.exports = {
|
|||||||
|
|
||||||
const abJson = minified ? libraryItem.toJSONMinified() : libraryItem.toJSONExpanded()
|
const abJson = minified ? libraryItem.toJSONMinified() : libraryItem.toJSONExpanded()
|
||||||
abJson.sequence = bookSeriesObj.sequence
|
abJson.sequence = bookSeriesObj.sequence
|
||||||
|
if (filterSeries) {
|
||||||
|
abJson.filterSeriesSequence = libraryItem.media.metadata.getSeries(filterSeries).sequence
|
||||||
|
}
|
||||||
if (!_series[bookSeriesObj.id]) {
|
if (!_series[bookSeriesObj.id]) {
|
||||||
_series[bookSeriesObj.id] = {
|
_series[bookSeriesObj.id] = {
|
||||||
id: bookSeriesObj.id,
|
id: bookSeriesObj.id,
|
||||||
@ -280,35 +283,34 @@ module.exports = {
|
|||||||
return totalSize
|
return totalSize
|
||||||
},
|
},
|
||||||
|
|
||||||
collapseBookSeries(libraryItems, series) {
|
|
||||||
var seriesObjects = this.getSeriesFromBooks(libraryItems, series, null, null, true)
|
|
||||||
var seriesToUse = {}
|
|
||||||
var libraryItemIdsToHide = []
|
|
||||||
seriesObjects.forEach((series) => {
|
|
||||||
series.firstBook = series.books.find(b => !seriesToUse[b.id]) // Find first book not already used
|
|
||||||
if (series.firstBook) {
|
|
||||||
seriesToUse[series.firstBook.id] = series
|
|
||||||
libraryItemIdsToHide = libraryItemIdsToHide.concat(series.books.filter(b => !seriesToUse[b.id]).map(b => b.id))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return libraryItems.map((li) => {
|
collapseBookSeries(libraryItems, series, filterSeries) {
|
||||||
|
// Get series from the library items. If this list is being collapsed after filtering for a series,
|
||||||
|
// don't collapse that series, only books that are in other series.
|
||||||
|
var seriesObjects = this
|
||||||
|
.getSeriesFromBooks(libraryItems, series, filterSeries, null, null, true)
|
||||||
|
.filter(s => s.id != filterSeries)
|
||||||
|
|
||||||
|
var filteredLibraryItems = []
|
||||||
|
|
||||||
|
libraryItems.forEach((li) => {
|
||||||
if (li.mediaType != 'book') return
|
if (li.mediaType != 'book') return
|
||||||
var libraryItemJson = li.toJSONMinified()
|
|
||||||
if (libraryItemIdsToHide.includes(li.id)) {
|
// Handle when this is the first book in a series
|
||||||
return null
|
seriesObjects.filter(s => s.books[0].id == li.id).forEach(series => {
|
||||||
}
|
// Clone the library item as we need to attach data to it, but don't
|
||||||
if (seriesToUse[li.id]) {
|
// want to change the global copy of the library item
|
||||||
libraryItemJson.collapsedSeries = {
|
filteredLibraryItems.push(Object.assign(
|
||||||
id: seriesToUse[li.id].id,
|
Object.create(Object.getPrototypeOf(li)),
|
||||||
name: seriesToUse[li.id].name,
|
li, { collapsedSeries: series }))
|
||||||
nameIgnorePrefix: seriesToUse[li.id].nameIgnorePrefix,
|
});
|
||||||
libraryItemIds: seriesToUse[li.id].books.map(b => b.id),
|
|
||||||
numBooks: seriesToUse[li.id].books.length
|
// Only included books not contained in series
|
||||||
}
|
if (!seriesObjects.some(s => s.books.some(b => b.id == li.id)))
|
||||||
}
|
filteredLibraryItems.push(li)
|
||||||
return libraryItemJson
|
});
|
||||||
}).filter(li => li)
|
|
||||||
|
return filteredLibraryItems
|
||||||
},
|
},
|
||||||
|
|
||||||
buildPersonalizedShelves(user, libraryItems, mediaType, allSeries, allAuthors, maxEntitiesPerShelf = 10) {
|
buildPersonalizedShelves(user, libraryItems, mediaType, allSeries, allAuthors, maxEntitiesPerShelf = 10) {
|
||||||
|
Loading…
Reference in New Issue
Block a user