mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-05-30 19:54:55 -04:00
Major bookshelf refactor
This commit is contained in:
parent
941f3248d8
commit
2186603039
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="bookshelf" ref="wrapper" class="w-full max-w-full h-full overflow-y-scroll relative">
|
<div id="bookshelf" ref="wrapper" class="w-full max-w-full h-full overflow-y-scroll relative" :style="{ fontSize: sizeMultiplier + 'rem' }">
|
||||||
<!-- Cover size widget -->
|
<!-- Cover size widget -->
|
||||||
<widgets-cover-size-widget class="fixed right-4 z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" />
|
<widgets-cover-size-widget class="fixed right-4r z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" />
|
||||||
|
|
||||||
<div v-if="loaded && !shelves.length && !search" class="w-full flex flex-col items-center justify-center py-12">
|
<div v-if="loaded && !shelves.length && !search" class="w-full flex flex-col items-center justify-center py-12">
|
||||||
<p class="text-center text-2xl mb-4 py-4">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p>
|
<p class="text-center text-2xl mb-4 py-4">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p>
|
||||||
@ -15,22 +15,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Alternate plain view -->
|
<!-- Alternate plain view -->
|
||||||
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24">
|
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24">
|
||||||
<template v-for="(shelf, index) in shelves">
|
<template v-for="(shelf, index) in supportedShelves">
|
||||||
<widgets-item-slider v-if="shelf.type === 'book' || shelf.type === 'podcast'" :shelf-id="shelf.id" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)">
|
<widgets-item-slider :shelf-id="shelf.id" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" :type="shelf.type" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)">
|
||||||
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
|
<p class="font-semibold text-gray-100">{{ $strings[shelf.labelStringKey] }}</p>
|
||||||
</widgets-item-slider>
|
</widgets-item-slider>
|
||||||
<widgets-episode-slider v-else-if="shelf.type === 'episode'" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)">
|
|
||||||
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
|
|
||||||
</widgets-episode-slider>
|
|
||||||
<widgets-series-slider v-else-if="shelf.type === 'series'" :key="index + '.'" :items="shelf.entities" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
|
|
||||||
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
|
|
||||||
</widgets-series-slider>
|
|
||||||
<widgets-authors-slider v-else-if="shelf.type === 'authors'" :key="index + '.'" :items="shelf.entities" :height="192 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
|
|
||||||
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
|
|
||||||
</widgets-authors-slider>
|
|
||||||
<widgets-narrators-slider v-else-if="shelf.type === 'narrators'" :key="index + '.'" :items="shelf.entities" :height="100 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
|
|
||||||
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
|
|
||||||
</widgets-narrators-slider>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<!-- Regular bookshelf view -->
|
<!-- Regular bookshelf view -->
|
||||||
@ -63,6 +51,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
supportedShelves() {
|
||||||
|
return this.shelves.filter((shelf) => ['book', 'episode', 'series', 'authors', 'narrators'].includes(shelf.type))
|
||||||
|
},
|
||||||
userIsAdminOrUp() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
@ -90,8 +81,7 @@ export default {
|
|||||||
return this.coverAspectRatio == 1
|
return this.coverAspectRatio == 1
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
return this.$store.getters['user/getSizeMultiplier']
|
||||||
return this.bookCoverWidth / baseSize
|
|
||||||
},
|
},
|
||||||
selectedMediaItems() {
|
selectedMediaItems() {
|
||||||
return this.$store.state.globals.selectedMediaItems || []
|
return this.$store.state.globals.selectedMediaItems || []
|
||||||
|
@ -1,66 +1,52 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div ref="shelf" class="w-full max-w-full bookshelf-row categorizedBookshelfRow relative overflow-x-scroll overflow-y-hidden z-10" :style="{ paddingLeft: paddingLeft * sizeMultiplier + 'rem', height: shelfHeight + 'px' }" @scroll="scrolled">
|
<div ref="shelf" class="w-full max-w-full bookshelf-row categorizedBookshelfRow relative overflow-x-scroll no-scroll overflow-y-hidden z-10" :style="{ paddingLeft: paddingLeft + 'em' }" @scroll="scrolled">
|
||||||
<div class="w-full h-full pt-6">
|
<div class="w-full h-full pt-6">
|
||||||
<div v-if="shelf.type === 'book' || shelf.type === 'podcast'" class="flex items-center">
|
<div v-if="shelf.type === 'book' || shelf.type === 'podcast'" class="flex items-center">
|
||||||
<template v-for="(entity, index) in shelf.entities">
|
<template v-for="(entity, index) in shelf.entities">
|
||||||
<cards-lazy-book-card :key="`${entity.id}-${index}`" :ref="`shelf-book-${entity.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" :continue-listening-shelf="continueListeningShelf" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editItem" />
|
<cards-lazy-book-card :key="entity.id" :ref="`shelf-book-${entity.id}`" :index="index" :book-mount="entity" :continue-listening-shelf="continueListeningShelf" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editItem" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="shelf.type === 'episode'" class="flex items-center">
|
<div v-if="shelf.type === 'episode'" class="flex items-center">
|
||||||
<template v-for="(entity, index) in shelf.entities">
|
<template v-for="(entity, index) in shelf.entities">
|
||||||
<cards-lazy-book-card
|
<cards-lazy-book-card :key="entity.recentEpisode.id" :ref="`shelf-episode-${entity.recentEpisode.id}`" :index="index" :book-mount="entity" :continue-listening-shelf="continueListeningShelf" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @editPodcast="editItem" @edit="editEpisode" />
|
||||||
:key="entity.recentEpisode.id"
|
|
||||||
:ref="`shelf-episode-${entity.recentEpisode.id}`"
|
|
||||||
:index="index"
|
|
||||||
:width="bookCoverWidth"
|
|
||||||
:height="bookCoverHeight"
|
|
||||||
:book-cover-aspect-ratio="bookCoverAspectRatio"
|
|
||||||
:book-mount="entity"
|
|
||||||
:continue-listening-shelf="continueListeningShelf"
|
|
||||||
class="relative mx-2"
|
|
||||||
@hook:updated="updatedBookCard"
|
|
||||||
@select="selectItem"
|
|
||||||
@editPodcast="editItem"
|
|
||||||
@edit="editEpisode"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="shelf.type === 'series'" class="flex items-center">
|
<div v-if="shelf.type === 'series'" class="flex items-center">
|
||||||
<template v-for="entity in shelf.entities">
|
<template v-for="entity in shelf.entities">
|
||||||
<cards-lazy-series-card :key="entity.name" :series-mount="entity" :height="bookCoverHeight" :width="bookCoverWidth * 2" :book-cover-aspect-ratio="bookCoverAspectRatio" class="relative mx-2" @hook:updated="updatedBookCard" />
|
<cards-lazy-series-card :key="entity.name" :series-mount="entity" class="relative mx-2" @hook:updated="updatedBookCard" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="shelf.type === 'tags'" class="flex items-center">
|
<div v-if="shelf.type === 'tags'" class="flex items-center">
|
||||||
<template v-for="entity in shelf.entities">
|
<template v-for="entity in shelf.entities">
|
||||||
<cards-group-card :key="entity.name" :group="entity" :height="bookCoverHeight" :width="bookCoverWidth * 2" :book-cover-aspect-ratio="bookCoverAspectRatio" class="relative mx-2" @hook:updated="updatedBookCard" />
|
<cards-group-card :key="entity.name" :group="entity" class="relative mx-2" @hook:updated="updatedBookCard" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="shelf.type === 'authors'" class="flex items-center">
|
<div v-if="shelf.type === 'authors'" class="flex items-center">
|
||||||
<template v-for="entity in shelf.entities">
|
<template v-for="entity in shelf.entities">
|
||||||
<cards-author-card :key="entity.id" :width="bookCoverWidth / 1.25" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" @edit="editAuthor" />
|
<cards-author-card :key="entity.id" :author="entity" @hook:updated="updatedBookCard" class="mx-2" @edit="editAuthor" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="shelf.type === 'narrators'" class="flex items-center">
|
<div v-if="shelf.type === 'narrators'" class="flex items-center">
|
||||||
<template v-for="entity in shelf.entities">
|
<template v-for="entity in shelf.entities">
|
||||||
<cards-narrator-card :key="entity.name" :width="150" :height="100" :narrator="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" />
|
<cards-narrator-card :key="entity.name" :narrator="entity" @hook:updated="updatedBookCard" class="mx-2" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="relative">
|
||||||
<div class="absolute text-center categoryPlacard transform z-30 bottom-px left-4 md:left-8 w-44 rounded-md" style="height: 22px">
|
<div class="relative text-center categoryPlacard transform z-30 top-0 left-4 md:left-8 w-44 rounded-md">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em 0.5em` }">
|
||||||
<p class="transform text-sm">{{ $strings[shelf.labelStringKey] }}</p>
|
<p :style="{ fontSize: 0.9 + 'em' }">{{ $strings[shelf.labelStringKey] }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bookshelfDividerCategorized h-6 w-full absolute top-0 left-0 right-0 z-20"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-show="canScrollLeft && !isScrolling" class="hidden sm:flex absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollLeft">
|
||||||
<div class="bookshelfDividerCategorized h-6 w-full absolute bottom-0 left-0 right-0 z-20"></div>
|
|
||||||
|
|
||||||
<div v-show="canScrollLeft && !isScrolling" class="hidden sm:flex absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-30" @click="scrollLeft">
|
|
||||||
<span class="material-icons text-6xl text-white">chevron_left</span>
|
<span class="material-icons text-6xl text-white">chevron_left</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="canScrollRight && !isScrolling" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-30" @click="scrollRight">
|
<div v-show="canScrollRight && !isScrolling" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollRight">
|
||||||
<span class="material-icons text-6xl text-white">chevron_right</span>
|
<span class="material-icons text-6xl text-white">chevron_right</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -74,9 +60,6 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
sizeMultiplier: Number,
|
|
||||||
bookCoverWidth: Number,
|
|
||||||
bookCoverAspectRatio: Number,
|
|
||||||
continueListeningShelf: Boolean
|
continueListeningShelf: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -89,12 +72,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bookCoverHeight() {
|
sizeMultiplier() {
|
||||||
return this.bookCoverWidth * this.bookCoverAspectRatio
|
return this.$store.getters['user/getSizeMultiplier']
|
||||||
},
|
|
||||||
shelfHeight() {
|
|
||||||
if (this.shelf.type === 'narrators') return 148
|
|
||||||
return this.bookCoverHeight + 48
|
|
||||||
},
|
},
|
||||||
paddingLeft() {
|
paddingLeft() {
|
||||||
if (window.innerWidth < 768) return 1
|
if (window.innerWidth < 768) return 1
|
||||||
@ -227,4 +206,4 @@ export default {
|
|||||||
background: rgb(48, 48, 48);
|
background: rgb(48, 48, 48);
|
||||||
background: linear-gradient(-90deg, rgba(48, 48, 48, 0) 0%, rgba(25, 25, 25, 0.25) 8%, rgba(17, 17, 17, 0.4) 28%, rgba(17, 17, 17, 0.6) 71%, rgba(10, 10, 10, 0.6) 86%, rgba(0, 0, 0, 0.7) 100%);
|
background: linear-gradient(-90deg, rgba(48, 48, 48, 0) 0%, rgba(25, 25, 25, 0.25) 8%, rgba(17, 17, 17, 0.4) 28%, rgba(17, 17, 17, 0.6) 71%, rgba(10, 10, 10, 0.6) 86%, rgba(0, 0, 0, 0.7) 100%);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="bookshelf" class="w-full overflow-y-auto">
|
<div id="bookshelf" ref="bookshelf" class="w-full overflow-y-auto" :style="{ fontSize: sizeMultiplier + 'rem' }">
|
||||||
<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" :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 v-if="!isAlternativeBookshelfView" class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-20 h-6" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -21,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<widgets-cover-size-widget class="fixed right-4 z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" />
|
<widgets-cover-size-widget class="fixed right-4r z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -49,10 +49,9 @@ export default {
|
|||||||
entityIndexesMounted: [],
|
entityIndexesMounted: [],
|
||||||
entityComponentRefs: {},
|
entityComponentRefs: {},
|
||||||
currentBookWidth: 0,
|
currentBookWidth: 0,
|
||||||
pageLoadQueue: [],
|
|
||||||
isFetchingEntities: false,
|
isFetchingEntities: false,
|
||||||
scrollTimeout: null,
|
scrollTimeout: null,
|
||||||
booksPerFetch: 100,
|
booksPerFetch: 0,
|
||||||
totalShelves: 0,
|
totalShelves: 0,
|
||||||
bookshelfMarginLeft: 0,
|
bookshelfMarginLeft: 0,
|
||||||
isSelectionMode: false,
|
isSelectionMode: false,
|
||||||
@ -63,7 +62,10 @@ export default {
|
|||||||
resizeTimeout: null,
|
resizeTimeout: null,
|
||||||
mountWindowWidth: 0,
|
mountWindowWidth: 0,
|
||||||
lastItemIndexSelected: -1,
|
lastItemIndexSelected: -1,
|
||||||
tempIsScanning: false
|
tempIsScanning: false,
|
||||||
|
cardWidth: 0,
|
||||||
|
cardHeight: 0,
|
||||||
|
resizeObserver: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@ -160,52 +162,40 @@ export default {
|
|||||||
return this.$store.getters['libraries/getCurrentLibraryName']
|
return this.$store.getters['libraries/getCurrentLibraryName']
|
||||||
},
|
},
|
||||||
bookWidth() {
|
bookWidth() {
|
||||||
const coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
return this.cardWidth
|
||||||
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return coverSize * 1.6
|
|
||||||
return coverSize
|
|
||||||
},
|
},
|
||||||
bookHeight() {
|
bookHeight() {
|
||||||
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return this.bookWidth
|
return this.cardHeight
|
||||||
return this.bookWidth * 1.6
|
|
||||||
},
|
},
|
||||||
shelfPadding() {
|
shelfPadding() {
|
||||||
if (this.bookshelfWidth < 640) return 32
|
if (this.bookshelfWidth < 640) return 32 * this.sizeMultiplier
|
||||||
return 64
|
return 64 * this.sizeMultiplier
|
||||||
},
|
},
|
||||||
totalPadding() {
|
totalPadding() {
|
||||||
return this.shelfPadding * 2
|
return this.shelfPadding * 2
|
||||||
},
|
},
|
||||||
entityWidth() {
|
entityWidth() {
|
||||||
if (this.entityName === 'series' || this.entityName === 'collections') {
|
return this.cardWidth
|
||||||
if (this.bookWidth * 2 > this.bookshelfWidth - this.shelfPadding) return this.bookWidth * 1.6
|
|
||||||
return this.bookWidth * 2
|
|
||||||
}
|
|
||||||
return this.bookWidth
|
|
||||||
},
|
},
|
||||||
entityHeight() {
|
entityHeight() {
|
||||||
return this.bookHeight
|
return this.cardHeight
|
||||||
},
|
},
|
||||||
shelfDividerHeightIndex() {
|
shelfPaddingHeight() {
|
||||||
return 6
|
return 16
|
||||||
},
|
},
|
||||||
shelfHeight() {
|
shelfHeight() {
|
||||||
if (this.isAlternativeBookshelfView) {
|
const dividerHeight = this.isAlternativeBookshelfView ? 0 : 24 // h-6
|
||||||
const isItemEntity = this.entityName === 'series-books' || this.entityName === 'items'
|
return this.cardHeight + (this.shelfPaddingHeight + dividerHeight) * this.sizeMultiplier
|
||||||
const extraTitleSpace = isItemEntity ? 80 : this.entityName === 'albums' ? 60 : 40
|
|
||||||
return this.entityHeight + extraTitleSpace * this.sizeMultiplier
|
|
||||||
}
|
|
||||||
return this.entityHeight + 40
|
|
||||||
},
|
},
|
||||||
totalEntityCardWidth() {
|
totalEntityCardWidth() {
|
||||||
// Includes margin
|
// Includes margin
|
||||||
return this.entityWidth + 24
|
return this.entityWidth + 24 * this.sizeMultiplier
|
||||||
},
|
},
|
||||||
selectedMediaItems() {
|
selectedMediaItems() {
|
||||||
return this.$store.state.globals.selectedMediaItems || []
|
return this.$store.state.globals.selectedMediaItems || []
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
const baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
return this.$store.getters['user/getSizeMultiplier']
|
||||||
return this.entityWidth / baseSize
|
|
||||||
},
|
},
|
||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
@ -436,10 +426,14 @@ export default {
|
|||||||
rebuild() {
|
rebuild() {
|
||||||
this.initSizeData()
|
this.initSizeData()
|
||||||
|
|
||||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
var lastBookIndex = Math.min(this.totalEntities, this.booksPerFetch)
|
||||||
this.entityIndexesMounted = []
|
this.entityIndexesMounted = []
|
||||||
for (let i = 0; i < lastBookIndex; i++) {
|
for (let i = 0; i < lastBookIndex; i++) {
|
||||||
this.entityIndexesMounted.push(i)
|
this.entityIndexesMounted.push(i)
|
||||||
|
if (!this.entities[i]) {
|
||||||
|
const page = Math.floor(i / this.booksPerFetch)
|
||||||
|
this.loadPage(page)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var bookshelfEl = document.getElementById('bookshelf')
|
var bookshelfEl = document.getElementById('bookshelf')
|
||||||
if (bookshelfEl) {
|
if (bookshelfEl) {
|
||||||
@ -501,7 +495,8 @@ export default {
|
|||||||
this.resetEntities()
|
this.resetEntities()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
settingsUpdated(settings) {
|
async settingsUpdated(settings) {
|
||||||
|
await this.cardsHelpers.setCardSize()
|
||||||
const wasUpdated = this.checkUpdateSearchParams()
|
const wasUpdated = this.checkUpdateSearchParams()
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
this.resetEntities()
|
this.resetEntities()
|
||||||
@ -606,6 +601,20 @@ export default {
|
|||||||
this.executeRebuild()
|
this.executeRebuild()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updatePagesLoaded() {
|
||||||
|
let numPages = Math.ceil(this.totalEntities / this.booksPerFetch)
|
||||||
|
for (let page = 0; page < numPages; page++) {
|
||||||
|
let numEntities = Math.min(this.totalEntities - page * this.booksPerFetch, this.booksPerFetch)
|
||||||
|
this.pagesLoaded[page] = true
|
||||||
|
for (let i = 0; i < numEntities; i++) {
|
||||||
|
const index = page * this.booksPerFetch + i
|
||||||
|
if (!this.entities[index]) {
|
||||||
|
this.pagesLoaded[page] = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
initSizeData(_bookshelf) {
|
initSizeData(_bookshelf) {
|
||||||
var bookshelf = _bookshelf || document.getElementById('bookshelf')
|
var bookshelf = _bookshelf || document.getElementById('bookshelf')
|
||||||
if (!bookshelf) {
|
if (!bookshelf) {
|
||||||
@ -622,6 +631,13 @@ export default {
|
|||||||
this.entitiesPerShelf = Math.max(1, Math.floor((this.bookshelfWidth - this.shelfPadding) / this.totalEntityCardWidth))
|
this.entitiesPerShelf = Math.max(1, Math.floor((this.bookshelfWidth - this.shelfPadding) / this.totalEntityCardWidth))
|
||||||
this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2
|
this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2
|
||||||
this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2
|
this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2
|
||||||
|
const booksPerFetch = this.entitiesPerShelf * this.shelvesPerPage
|
||||||
|
if (booksPerFetch !== this.booksPerFetch) {
|
||||||
|
this.booksPerFetch = booksPerFetch
|
||||||
|
if (this.totalEntities) {
|
||||||
|
this.updatePagesLoaded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.currentBookWidth = this.bookWidth
|
this.currentBookWidth = this.bookWidth
|
||||||
if (this.totalEntities) {
|
if (this.totalEntities) {
|
||||||
@ -630,13 +646,8 @@ export default {
|
|||||||
return entitiesPerShelfBefore < this.entitiesPerShelf // Books per shelf has changed
|
return entitiesPerShelfBefore < this.entitiesPerShelf // Books per shelf has changed
|
||||||
},
|
},
|
||||||
async init(bookshelf) {
|
async init(bookshelf) {
|
||||||
if (this.entityName === 'series') {
|
|
||||||
this.booksPerFetch = 50
|
|
||||||
} else {
|
|
||||||
this.booksPerFetch = 100
|
|
||||||
}
|
|
||||||
this.checkUpdateSearchParams()
|
|
||||||
this.initSizeData(bookshelf)
|
this.initSizeData(bookshelf)
|
||||||
|
this.checkUpdateSearchParams()
|
||||||
|
|
||||||
this.pagesLoaded[0] = true
|
this.pagesLoaded[0] = true
|
||||||
await this.fetchEntites(0)
|
await this.fetchEntites(0)
|
||||||
@ -743,7 +754,8 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
async mounted() {
|
||||||
|
await this.cardsHelpers.setCardSize()
|
||||||
this.initListeners()
|
this.initListeners()
|
||||||
|
|
||||||
this.routeFullPath = window.location.pathname + (window.location.search || '')
|
this.routeFullPath = window.location.pathname + (window.location.search || '')
|
||||||
@ -778,6 +790,6 @@ export default {
|
|||||||
.bookshelfDivider {
|
.bookshelfDivider {
|
||||||
background: rgb(149, 119, 90);
|
background: rgb(149, 119, 90);
|
||||||
background: var(--bookshelf-divider-bg);
|
background: var(--bookshelf-divider-bg);
|
||||||
box-shadow: 2px 14px 8px #111111aa;
|
box-shadow: 0.125em 0.875em 0.5em #111111aa;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,38 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<nuxt-link :to="`/author/${author?.id}`">
|
<div :style="{ width: cardWidth + 'px' }">
|
||||||
<div cy-id="card" :style="{ width: width + 'px'}" @mouseover="mouseover" @mouseleave="mouseleave">
|
<nuxt-link :to="`/author/${author.id}`">
|
||||||
<div cy-id="imageArea" :style="{ height: height + 'px' }" class=" bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
<div cy-id="card" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
<!-- Image or placeholder -->
|
<div cy-id="imageArea" :style="{ height: cardHeight + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
||||||
<covers-author-image :author="author"/>
|
<!-- Image or placeholder -->
|
||||||
|
<covers-author-image :author="author" />
|
||||||
|
|
||||||
<!-- Author name & num books overlay -->
|
<!-- Author name & num books overlay -->
|
||||||
<div cy-id="textInline" v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2">
|
<div cy-id="textInline" v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2">
|
||||||
<p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
|
<p class="text-center font-semibold truncate" :style="{ fontSize: 0.75 + 'em' }">{{ name }}</p>
|
||||||
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} {{ $strings.LabelBooks }}</p>
|
<p class="text-center text-gray-200" :style="{ fontSize: 0.65 + 'em' }">{{ numBooks }} {{ $strings.LabelBooks }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Search icon btn -->
|
<!-- Search icon btn -->
|
||||||
<div cy-id="match" v-show="!searching && isHovering && userCanUpdate" class="absolute top-0 left-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="searchAuthor">
|
<div cy-id="match" v-show="!searching && isHovering && userCanUpdate" class="absolute top-0 left-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="searchAuthor">
|
||||||
<ui-tooltip :text="$strings.ButtonQuickMatch" direction="bottom">
|
<ui-tooltip :text="$strings.ButtonQuickMatch" direction="bottom">
|
||||||
<span class="material-icons text-lg">search</span>
|
<span class="material-icons text-lg">search</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div cy-id="edit" v-show="isHovering && !searching && userCanUpdate" class="absolute top-0 right-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="$emit('edit', author)">
|
<div cy-id="edit" v-show="isHovering && !searching && userCanUpdate" class="absolute top-0 right-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="$emit('edit', author)">
|
||||||
<ui-tooltip :text="$strings.LabelEdit" direction="bottom">
|
<ui-tooltip :text="$strings.LabelEdit" direction="bottom">
|
||||||
<span class="material-icons text-lg">edit</span>
|
<span class="material-icons text-lg">edit</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading spinner -->
|
<!-- Loading spinner -->
|
||||||
<div cy-id="spinner" v-show="searching" class="absolute top-0 left-0 z-10 w-full h-full bg-black bg-opacity-50 flex items-center justify-center">
|
<div cy-id="spinner" v-show="searching" class="absolute top-0 left-0 z-10 w-full h-full bg-black bg-opacity-50 flex items-center justify-center">
|
||||||
<widgets-loading-spinner size="" />
|
<widgets-loading-spinner size="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div cy-id="nameBelow" v-show="nameBelow" class="w-full py-1 px-2">
|
||||||
|
<p class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: 0.75 + 'em' }">{{ name }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div cy-id="nameBelow" v-show="nameBelow" class="w-full py-1 px-2">
|
</nuxt-link>
|
||||||
<p class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nuxt-link>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -43,12 +45,14 @@ export default {
|
|||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
width: Number,
|
width: Number,
|
||||||
height: Number,
|
height: {
|
||||||
sizeMultiplier: {
|
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1
|
default: 192
|
||||||
},
|
},
|
||||||
nameBelow: Boolean
|
nameBelow: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -57,6 +61,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
cardWidth() {
|
||||||
|
return this.width || this.cardHeight * 0.8
|
||||||
|
},
|
||||||
|
cardHeight() {
|
||||||
|
return this.height * this.sizeMultiplier
|
||||||
|
},
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
},
|
},
|
||||||
@ -83,6 +93,9 @@ export default {
|
|||||||
},
|
},
|
||||||
libraryProvider() {
|
libraryProvider() {
|
||||||
return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
|
return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
|
||||||
|
},
|
||||||
|
sizeMultiplier() {
|
||||||
|
return this.$store.getters['user/getSizeMultiplier']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -128,4 +141,4 @@ export default {
|
|||||||
this.$eventBus.$off(`searching-author-${this.authorId}`, this.setSearching)
|
this.$eventBus.$off(`searching-author-${this.authorId}`, this.setSearching)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="rounded-sm h-full relative" :style="{ width: width + 'px', height: height + 'px' }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard">
|
<div class="rounded-sm h-full relative" :style="{ width: cardWidth + 'px', height: cardHeight + 'px' }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard">
|
||||||
<nuxt-link :to="groupTo" class="cursor-pointer">
|
<nuxt-link :to="groupTo" class="cursor-pointer">
|
||||||
<div class="w-full h-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'">
|
<div class="w-full h-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'">
|
||||||
<covers-group-cover ref="groupcover" :id="groupEncode" :name="groupName" :type="groupType" :book-items="bookItems" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-group-cover ref="groupcover" :id="groupEncode" :name="groupName" :type="groupType" :book-items="bookItems" :width="cardWidth" :height="cardHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
|
||||||
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
||||||
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ groupName }}</p>
|
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ groupName }}</p>
|
||||||
@ -24,8 +24,10 @@ export default {
|
|||||||
default: () => null
|
default: () => null
|
||||||
},
|
},
|
||||||
width: Number,
|
width: Number,
|
||||||
height: Number,
|
height: {
|
||||||
bookCoverAspectRatio: Number
|
type: Number,
|
||||||
|
default: 192
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -33,6 +35,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
bookCoverAspectRatio() {
|
||||||
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
|
},
|
||||||
|
cardWidth() {
|
||||||
|
return this.width || this.cardHeight * 2
|
||||||
|
},
|
||||||
|
cardHeight() {
|
||||||
|
return this.height * this.sizeMultiplier
|
||||||
|
},
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
@ -46,8 +57,7 @@ export default {
|
|||||||
return `/library/${this.currentLibraryId}/bookshelf?filter=${this.filter}`
|
return `/library/${this.currentLibraryId}/bookshelf?filter=${this.filter}`
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
|
return this.$store.getters['user/getSizeMultiplier']
|
||||||
return this.width / 240
|
|
||||||
},
|
},
|
||||||
bookItems() {
|
bookItems() {
|
||||||
return this._group.books || []
|
return this._group.books || []
|
||||||
@ -78,4 +88,4 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,18 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="card" :id="`album-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
<div ref="card" :id="`album-card-${index}`" :style="{ width: cardWidth + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
<div class="relative" :style="{ height: coverHeight + 'px' }">
|
||||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||||
<covers-preview-cover ref="cover" :src="coverSrc" :width="width" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||||
</div>
|
<covers-preview-cover ref="cover" :src="coverSrc" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<div class="relative w-full">
|
||||||
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ artist || ' ' }}</p>
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
|
||||||
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em ${0.5}em` }">
|
||||||
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
||||||
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
|
||||||
|
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ artist || ' ' }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -22,8 +26,10 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
index: Number,
|
index: Number,
|
||||||
width: Number,
|
width: Number,
|
||||||
height: Number,
|
height: {
|
||||||
bookCoverAspectRatio: Number,
|
type: Number,
|
||||||
|
default: 192
|
||||||
|
},
|
||||||
bookshelfView: {
|
bookshelfView: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
@ -42,6 +48,29 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
bookCoverAspectRatio() {
|
||||||
|
return this.store.getters['libraries/getBookCoverAspectRatio']
|
||||||
|
},
|
||||||
|
cardWidth() {
|
||||||
|
return this.width || this.coverHeight
|
||||||
|
},
|
||||||
|
coverHeight() {
|
||||||
|
return this.height * this.sizeMultiplier
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
cardHeight() {
|
||||||
|
return this.coverHeight + this.bottomTextHeight
|
||||||
|
},
|
||||||
|
bottomTextHeight() {
|
||||||
|
if (!this.isAlternativeBookshelfView) return 0
|
||||||
|
const lineHeight = 1.5
|
||||||
|
const remSize = 16
|
||||||
|
const baseHeight = this.sizeMultiplier * lineHeight * remSize
|
||||||
|
const titleHeight = this.labelFontSize * baseHeight
|
||||||
|
const paddingHeight = 4 * 2 * this.sizeMultiplier // py-1
|
||||||
|
return titleHeight + paddingHeight
|
||||||
|
},
|
||||||
|
*/
|
||||||
coverSrc() {
|
coverSrc() {
|
||||||
const config = this.$config || this.$nuxt.$config
|
const config = this.$config || this.$nuxt.$config
|
||||||
if (!this.album || !this.album.libraryItemId) return `${config.routerBasePath}/book_placeholder.jpg`
|
if (!this.album || !this.album.libraryItemId) return `${config.routerBasePath}/book_placeholder.jpg`
|
||||||
@ -49,11 +78,10 @@ export default {
|
|||||||
},
|
},
|
||||||
labelFontSize() {
|
labelFontSize() {
|
||||||
if (this.width < 160) return 0.75
|
if (this.width < 160) return 0.75
|
||||||
return 0.875
|
return 0.9
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
const baseSize = this.bookCoverAspectRatio === 1 ? 192 : 120
|
return this.store.getters['user/getSizeMultiplier']
|
||||||
return this.width / baseSize
|
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.album ? this.album.title : ''
|
return this.album ? this.album.title : ''
|
||||||
@ -111,4 +139,4 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,125 +1,134 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="card" :id="`book-card-${index}`" :style="{ minWidth: width + 'px', maxWidth: width + 'px', height: height + 'px' }" class="absolute 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: coverWidth + 'px', maxWidth: coverWidth + 'px' }" class="absolute rounded-sm z-10 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
<!-- When cover image does not fill -->
|
<div :id="`cover-area-${index}`" class="relative w-full top-0 left-0 rounded overflow-hidden z-10 bg-primary box-shadow-book" :style="{ height: coverHeight + 'px ' }">
|
||||||
<div cy-id="coverBg" v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
<!-- When cover image does not fill -->
|
||||||
<div class="absolute cover-bg" ref="coverBg" />
|
<div cy-id="coverBg" 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>
|
||||||
|
|
||||||
|
<div cy-id="seriesSequenceList" v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #78350f">
|
||||||
|
<p :style="{ fontSize: 0.8 + 'em' }">#{{ seriesSequenceList }}</p>
|
||||||
|
</div>
|
||||||
|
<div cy-id="booksInSeries" v-else-if="booksInSeries" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #cd9d49dd">
|
||||||
|
<p :style="{ fontSize: 0.8 + 'em' }">{{ booksInSeries }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
||||||
|
<div cy-id="titleImageNotReady" v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: 0.5 + 'em' }">
|
||||||
|
<p :style="{ fontSize: 0.8 + 'em' }" class="text-gray-300 text-center">{{ title }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cover Image -->
|
||||||
|
<img cy-id="coverImage" v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="relative 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 -->
|
||||||
|
<div cy-id="placeholderTitle" 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 + 'em' }">
|
||||||
|
<div>
|
||||||
|
<p cy-id="placeholderTitleText" class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'em' }">{{ titleCleaned }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div cy-id="placeholderAuthor" v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'em', bottom: authorBottom + 'em' }">
|
||||||
|
<p cy-id="placeholderAuthorText" class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'em' }">{{ authorCleaned }}</p>
|
||||||
|
</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 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }" style="background-color: #78350f">
|
||||||
|
<p :style="{ fontSize: 0.8 + 'em' }">#{{ 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 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }" style="background-color: #cd9d49dd">
|
||||||
|
<p :style="{ fontSize: 0.8 + 'em' }">{{ booksInSeries }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No progress shown for podcasts (unless showing podcast episode) -->
|
||||||
|
<div cy-id="progressBar" v-if="!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: coverWidth * userProgressPercent + 'px' }"></div>
|
||||||
|
|
||||||
|
<!-- Overlay is not shown if collapsing series in library -->
|
||||||
|
<div cy-id="overlay" v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen) && !processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded md:block" :class="overlayWrapperClasslist">
|
||||||
|
<div cy-id="playButton" 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">
|
||||||
|
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'em' }">play_circle_filled</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div cy-id="readButton" 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">
|
||||||
|
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'em' }">auto_stories</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div cy-id="editButton" 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 + 'em' }" @click.stop.prevent="editClick">
|
||||||
|
<span class="material-icons" :style="{ fontSize: 1 + 'em' }">edit</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Radio button -->
|
||||||
|
<div cy-id="selectedRadioButton" v-if="!isAuthorBookshelfView" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 + 'em', left: 0.375 + 'em' }" @click.stop.prevent="selectBtnClick">
|
||||||
|
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 + 'em' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- More Menu Icon -->
|
||||||
|
<div cy-id="moreButton" ref="moreIcon" v-show="!isSelectionMode && moreMenuItems.length" class="md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-150" :style="{ bottom: 0.375 + 'em', right: 0.375 + 'em' }" @click.stop.prevent="clickShowMore">
|
||||||
|
<span class="material-icons" :style="{ fontSize: 1.2 + 'em' }">more_vert</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div cy-id="ebookFormat" v-if="ebookFormat" class="absolute" :style="{ bottom: 0.375 + 'em', left: 0.375 + 'em' }">
|
||||||
|
<span class="text-white/80" :style="{ fontSize: 0.8 + 'em' }">{{ ebookFormat }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Processing/loading spinner overlay -->
|
||||||
|
<div cy-id="loadingSpinner" 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" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Series name overlay -->
|
||||||
|
<div cy-id="seriesNameOverlay" 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: 1 + 'em' }">
|
||||||
|
<p v-if="seriesName" class="text-gray-200 text-center" :style="{ fontSize: 1.1 + 'em' }">{{ seriesName }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error widget -->
|
||||||
|
<ui-tooltip cy-id="ErrorTooltip" v-if="showError" :text="errorText" class="absolute bottom-4 left-0 z-10">
|
||||||
|
<div :style="{ height: 1.5 + 'em', width: 2.5 + 'em' }" 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 + 'em' }">priority_high</span>
|
||||||
|
</div>
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
|
<div cy-id="rssFeed" v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" :style="{ padding: 0.375 + 'em' }">
|
||||||
|
<span class="material-icons" :style="{ fontSize: 1.5 + 'em' }">rss_feed</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Series sequence -->
|
||||||
|
<div cy-id="seriesSequence" v-if="seriesSequence && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }">
|
||||||
|
<p :style="{ fontSize: 0.8 + 'em' }">#{{ seriesSequence }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Podcast Episode # -->
|
||||||
|
<div cy-id="podcastEpisodeNumber" v-if="recentEpisodeNumber !== null && !isHovering && !isSelectionMode && !processing" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }">
|
||||||
|
<p :style="{ fontSize: 0.8 + 'em' }">
|
||||||
|
Episode<span v-if="recentEpisodeNumber"> #{{ recentEpisodeNumber }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Podcast Num Episodes -->
|
||||||
|
<div cy-id="numEpisodes" v-else-if="!numEpisodesIncomplete && 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 + 'em', right: 0.375 + 'em', width: 1.25 + 'em', height: 1.25 + 'em' }">
|
||||||
|
<p :style="{ fontSize: 0.8 + 'em' }">{{ numEpisodes }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Podcast Num Episodes -->
|
||||||
|
<div cy-id="numEpisodesIncomplete" v-else-if="numEpisodesIncomplete && !isHovering && !isSelectionMode" class="absolute rounded-full bg-yellow-400 text-black font-semibold box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', width: 1.25 + 'em', height: 1.25 + 'em' }">
|
||||||
|
<p :style="{ fontSize: 0.8 + 'em' }">{{ numEpisodesIncomplete }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Alternative bookshelf title/author/sort -->
|
<!-- Alternative bookshelf title/author/sort -->
|
||||||
<div cy-id="detailBottom" v-if="isAlternativeBookshelfView || isAuthorBookshelfView" dir="auto" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
|
<div cy-id="detailBottom" :id="`description-area-${index}`" v-if="isAlternativeBookshelfView || isAuthorBookshelfView" dir="auto" class="relative mt-2 mb-2 left-0 z-50 w-full">
|
||||||
<div :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
|
<div :style="{ fontSize: 0.9 + 'em' }">
|
||||||
<ui-tooltip v-if="displayTitle" :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
|
<ui-tooltip v-if="displayTitle" :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
|
||||||
<p cy-id="title" ref="displayTitle" class="truncate">{{ displayTitle }}</p>
|
<p cy-id="title" ref="displayTitle" class="truncate">{{ displayTitle }}</p>
|
||||||
<widgets-explicit-indicator cy-id="explicitIndicator" v-if="isExplicit" />
|
<widgets-explicit-indicator cy-id="explicitIndicator" v-if="isExplicit" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<p cy-id="line2" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || ' ' }}</p>
|
<p cy-id="line2" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displayLineTwo || ' ' }}</p>
|
||||||
<p cy-id="line3" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
<p cy-id="line3" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displaySortLine }}</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div cy-id="seriesSequenceList" 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 cy-id="booksInSeries" 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 cy-id="titleImageNotReady" 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' }">
|
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="text-gray-300 text-center">{{ title }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Cover Image -->
|
|
||||||
<img cy-id="coverImage" 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 -->
|
|
||||||
<div cy-id="placeholderTitle" 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>
|
|
||||||
<p cy-id="placeholderTitleText" class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div cy-id="placeholderAuthor" v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
|
||||||
<p cy-id="placeholderAuthorText" class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- No progress shown for podcasts (unless showing podcast episode) -->
|
|
||||||
<div cy-id="progressBar" v-if="!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>
|
|
||||||
|
|
||||||
<!-- Overlay is not shown if collapsing series in library -->
|
|
||||||
<div cy-id="overlay" v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen) && !processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded md:block" :class="overlayWrapperClasslist">
|
|
||||||
<div cy-id="playButton" 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">
|
|
||||||
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div cy-id="readButton" 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">
|
|
||||||
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">auto_stories</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div cy-id="editButton" 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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Radio button -->
|
|
||||||
<div cy-id="selectedRadioButton" v-if="!isAuthorBookshelfView" 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">
|
|
||||||
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- More Menu Icon -->
|
|
||||||
<div cy-id="moreButton" ref="moreIcon" v-show="!isSelectionMode && moreMenuItems.length" class="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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div cy-id="ebookFormat" v-if="ebookFormat" class="absolute" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }">
|
|
||||||
<span class="text-white/80" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ ebookFormat }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Processing/loading spinner overlay -->
|
|
||||||
<div cy-id="loadingSpinner" 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" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Series name overlay -->
|
|
||||||
<div cy-id="seriesNameOverlay" 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 v-if="seriesName" class="text-gray-200 text-center" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">{{ seriesName }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Error widget -->
|
|
||||||
<ui-tooltip cy-id="ErrorTooltip" 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">
|
|
||||||
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
|
|
||||||
</div>
|
|
||||||
</ui-tooltip>
|
|
||||||
|
|
||||||
<div cy-id="rssFeed" 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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Series sequence -->
|
|
||||||
<div cy-id="seriesSequence" 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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Podcast Episode # -->
|
|
||||||
<div cy-id="podcastEpisodeNumber" v-if="recentEpisodeNumber !== null && !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<span v-if="recentEpisodeNumber"> #{{ recentEpisodeNumber }}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Podcast Num Episodes -->
|
|
||||||
<div cy-id="numEpisodes" v-else-if="!numEpisodesIncomplete && 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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Podcast Num Episodes -->
|
|
||||||
<div cy-id="numEpisodesIncomplete" v-else-if="numEpisodesIncomplete && !isHovering && !isSelectionMode" class="absolute rounded-full bg-yellow-400 text-black font-semibold 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' }">{{ numEpisodesIncomplete }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -131,15 +140,11 @@ import MoreMenu from '@/components/widgets/MoreMenu'
|
|||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
index: Number,
|
index: Number,
|
||||||
width: {
|
width: Number,
|
||||||
type: Number,
|
|
||||||
default: 120
|
|
||||||
},
|
|
||||||
height: {
|
height: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 192
|
default: 192
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio: Number,
|
|
||||||
bookshelfView: Number,
|
bookshelfView: Number,
|
||||||
bookMount: {
|
bookMount: {
|
||||||
// Book can be passed as prop or set with setEntity()
|
// Book can be passed as prop or set with setEntity()
|
||||||
@ -174,6 +179,39 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
bookCoverAspectRatio() {
|
||||||
|
return this.store.getters['libraries/getBookCoverAspectRatio']
|
||||||
|
},
|
||||||
|
coverWidth() {
|
||||||
|
return this.width || this.coverHeight / this.bookCoverAspectRatio
|
||||||
|
},
|
||||||
|
coverHeight() {
|
||||||
|
return this.height * this.sizeMultiplier
|
||||||
|
},
|
||||||
|
cardWidth() {
|
||||||
|
// This method returns immediately without waiting for the DOM to update
|
||||||
|
return this.coverWidth
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
cardHeight() {
|
||||||
|
// This method returns immediately without waiting for the DOM to update
|
||||||
|
return this.coverHeight + this.detailsHeight
|
||||||
|
},
|
||||||
|
detailsHeight() {
|
||||||
|
if (!this.isAlternativeBookshelfView) return 0
|
||||||
|
const lineHeight = 1.5
|
||||||
|
const remSize = 16
|
||||||
|
const baseHeight = this.sizeMultiplier * lineHeight * remSize
|
||||||
|
const titleHeight = 0.9 * baseHeight
|
||||||
|
const line2Height = 0.8 * baseHeight
|
||||||
|
const line3Height = this.displaySortLine ? 0.8 * baseHeight : 0
|
||||||
|
const marginHeight = 8 * 2 * this.sizeMultiplier // py-2
|
||||||
|
return titleHeight + line2Height + line3Height + marginHeight
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
sizeMultiplier() {
|
||||||
|
return this.store.getters['user/getSizeMultiplier']
|
||||||
|
},
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.store.state.serverSettings.dateFormat
|
return this.store.state.serverSettings.dateFormat
|
||||||
},
|
},
|
||||||
@ -273,10 +311,6 @@ export default {
|
|||||||
squareAspectRatio() {
|
squareAspectRatio() {
|
||||||
return this.bookCoverAspectRatio === 1
|
return this.bookCoverAspectRatio === 1
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
|
||||||
const baseSize = this.squareAspectRatio ? 192 : 120
|
|
||||||
return this.width / baseSize
|
|
||||||
},
|
|
||||||
title() {
|
title() {
|
||||||
return this.mediaMetadata.title || ''
|
return this.mediaMetadata.title || ''
|
||||||
},
|
},
|
||||||
@ -298,7 +332,7 @@ export default {
|
|||||||
if (this.recentEpisode) return this.recentEpisode.title
|
if (this.recentEpisode) return this.recentEpisode.title
|
||||||
const ignorePrefix = this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix
|
const ignorePrefix = this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix
|
||||||
if (this.collapsedSeries) return ignorePrefix ? this.collapsedSeries.nameIgnorePrefix : this.collapsedSeries.name
|
if (this.collapsedSeries) return ignorePrefix ? this.collapsedSeries.nameIgnorePrefix : this.collapsedSeries.name
|
||||||
return ignorePrefix ? this.mediaMetadata.titleIgnorePrefix : this.title
|
return ignorePrefix ? this.mediaMetadata.titleIgnorePrefix : this.title || '\u00A0'
|
||||||
},
|
},
|
||||||
displayLineTwo() {
|
displayLineTwo() {
|
||||||
if (this.recentEpisode) return this.title
|
if (this.recentEpisode) return this.title
|
||||||
@ -557,16 +591,16 @@ export default {
|
|||||||
return this.$root.socket || this.$nuxt.$root.socket
|
return this.$root.socket || this.$nuxt.$root.socket
|
||||||
},
|
},
|
||||||
titleFontSize() {
|
titleFontSize() {
|
||||||
return 0.75 * this.sizeMultiplier
|
return 0.75
|
||||||
},
|
},
|
||||||
authorFontSize() {
|
authorFontSize() {
|
||||||
return 0.6 * this.sizeMultiplier
|
return 0.6
|
||||||
},
|
},
|
||||||
placeholderCoverPadding() {
|
placeholderCoverPadding() {
|
||||||
return 0.8 * this.sizeMultiplier
|
return 0.8
|
||||||
},
|
},
|
||||||
authorBottom() {
|
authorBottom() {
|
||||||
return 0.75 * this.sizeMultiplier
|
return 0.75
|
||||||
},
|
},
|
||||||
titleCleaned() {
|
titleCleaned() {
|
||||||
if (!this.title) return ''
|
if (!this.title) return ''
|
||||||
@ -590,11 +624,6 @@ export default {
|
|||||||
const constants = this.$constants || this.$nuxt.$constants
|
const constants = this.$constants || this.$nuxt.$constants
|
||||||
return this.bookshelfView === constants.BookshelfView.AUTHOR
|
return this.bookshelfView === constants.BookshelfView.AUTHOR
|
||||||
},
|
},
|
||||||
titleDisplayBottomOffset() {
|
|
||||||
if (!this.isAlternativeBookshelfView && !this.isAuthorBookshelfView) return 0
|
|
||||||
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
|
|
||||||
return 4.25 * this.sizeMultiplier
|
|
||||||
},
|
|
||||||
rssFeed() {
|
rssFeed() {
|
||||||
if (this.booksInSeries) return null
|
if (this.booksInSeries) return null
|
||||||
return this._libraryItem.rssFeed || null
|
return this._libraryItem.rssFeed || null
|
||||||
|
@ -1,24 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="card" :id="`collection-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
<div ref="card" :id="`collection-card-${index}`" :style="{ width: cardWidth + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
<div class="relative" :style="{ height: coverHeight + 'px' }">
|
||||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||||
<covers-collection-cover ref="cover" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||||
</div>
|
<covers-collection-cover ref="cover" :book-items="books" :width="cardWidth" :height="coverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
<div v-show="isHovering && userCanUpdate" 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', 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 v-show="isHovering && userCanUpdate" 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 + 'em', right: 0.5 + 'em' }" @click.stop.prevent="clickEdit">
|
||||||
|
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 + 'em', left: 0.5 + 'em', fontSize: 1.5 + 'em' }">rss_feed</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span>
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
|
||||||
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em ${0.5}em` }">
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
<div v-else class="relative z-30 left-0 right-0 mx-auto h-8 py-1 rounded-md text-center">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -28,8 +30,10 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
index: Number,
|
index: Number,
|
||||||
width: Number,
|
width: Number,
|
||||||
height: Number,
|
height: {
|
||||||
bookCoverAspectRatio: Number,
|
type: Number,
|
||||||
|
default: 192
|
||||||
|
},
|
||||||
bookshelfView: {
|
bookshelfView: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
@ -49,13 +53,33 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
bookCoverAspectRatio() {
|
||||||
|
return this.store.getters['libraries/getBookCoverAspectRatio']
|
||||||
|
},
|
||||||
|
cardWidth() {
|
||||||
|
return this.width || this.coverHeight * 2
|
||||||
|
},
|
||||||
|
coverHeight() {
|
||||||
|
return this.height * this.sizeMultiplier
|
||||||
|
},
|
||||||
|
cardHeight() {
|
||||||
|
return this.coverHeight + this.bottomTextHeight
|
||||||
|
},
|
||||||
|
bottomTextHeight() {
|
||||||
|
if (!this.isAlternativeBookshelfView) return 0 // bottom text appears on top of the divider
|
||||||
|
const lineHeight = 1.5
|
||||||
|
const remSize = 16
|
||||||
|
const baseHeight = this.sizeMultiplier * lineHeight * remSize
|
||||||
|
const titleHeight = this.labelFontSize * baseHeight
|
||||||
|
const paddingHeight = 4 * 2 * this.sizeMultiplier // py-1
|
||||||
|
return titleHeight + paddingHeight
|
||||||
|
},
|
||||||
labelFontSize() {
|
labelFontSize() {
|
||||||
if (this.width < 160) return 0.75
|
if (this.width < 160) return 0.75
|
||||||
return 0.875
|
return 0.9
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
|
return this.store.getters['user/getSizeMultiplier']
|
||||||
return this.width / 240
|
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.collection ? this.collection.name : ''
|
return this.collection ? this.collection.name : ''
|
||||||
@ -119,4 +143,4 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,21 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="card" :id="`playlist-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
<div ref="card" :id="`playlist-card-${index}`" :style="{ width: cardWidth + 'px', fontSize: sizeMultiplier + 'rem' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
<div class="relative" :style="{ height: coverHeight + 'px' }">
|
||||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||||
<covers-playlist-cover ref="cover" :items="items" :width="width" :height="height" />
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||||
</div>
|
<covers-playlist-cover ref="cover" :items="items" :width="cardWidth" :height="coverHeight" />
|
||||||
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
|
</div>
|
||||||
<div class="absolute pointer-events-auto" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickEdit">
|
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
|
||||||
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
<div class="absolute pointer-events-auto" :style="{ top: 0.5 + 'em', right: 0.5 + 'em' }" @click.stop.prevent="clickEdit">
|
||||||
|
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 -bottom-6 left-0 right-0 mx-auto h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em ${0.5}em` }">
|
||||||
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
<div v-else class="relative z-30 left-0 right-0 mx-auto h-8 py-1 rounded-md text-center">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -25,8 +28,10 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
index: Number,
|
index: Number,
|
||||||
width: Number,
|
width: Number,
|
||||||
height: Number,
|
height: {
|
||||||
bookCoverAspectRatio: Number,
|
type: Number,
|
||||||
|
default: 192
|
||||||
|
},
|
||||||
bookshelfView: {
|
bookshelfView: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
@ -45,13 +50,21 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
bookCoverAspectRatio() {
|
||||||
|
return this.store.getters['libraries/getBookCoverAspectRatio']
|
||||||
|
},
|
||||||
|
cardWidth() {
|
||||||
|
return this.width || this.coverHeight
|
||||||
|
},
|
||||||
|
coverHeight() {
|
||||||
|
return this.height * this.sizeMultiplier
|
||||||
|
},
|
||||||
labelFontSize() {
|
labelFontSize() {
|
||||||
if (this.width < 160) return 0.75
|
if (this.width < 160) return 0.75
|
||||||
return 0.875
|
return 0.9
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6)
|
return this.store.getters['user/getSizeMultiplier']
|
||||||
return this.width / 120
|
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.playlist ? this.playlist.name : ''
|
return this.playlist ? this.playlist.name : ''
|
||||||
@ -112,4 +125,4 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,28 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div cy-id="card" ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
<div cy-id="card" ref="card" :id="`series-card-${index}`" :style="{ width: cardWidth + 'px' }" class="absolute rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
<div cy-id="covers-area" class="relative" :style="{ height: coverHeight + 'px' }">
|
||||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||||
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
||||||
|
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="cardWidth" :height="coverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div cy-id="seriesLengthMarker" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #cd9d49dd">
|
||||||
|
<p :style="{ fontSize: 0.8 + 'em' }">{{ books.length }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div cy-id="seriesProgressBar" v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
|
||||||
|
|
||||||
|
<div cy-id="hoveringDisplayTitle" v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: '1em' }">
|
||||||
|
<p :style="{ fontSize: 1.2 + 'em' }">{{ displayTitle }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span cy-id="rssFeedMarker" v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 + 'em', left: 0.5 + 'em', fontSize: 1.5 + 'em' }">rss_feed</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div cy-id="seriesLengthMarker" class="absolute z-10 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">{{ books.length }}</div>
|
<div cy-id="standardBottomText" v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
|
||||||
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em 0.5em` }">
|
||||||
<div cy-id="seriesProgressBar" v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
|
<p cy-id="standardBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ displayTitle }}</p>
|
||||||
|
|
||||||
<div cy-id="hoveringDisplayTitle" v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
|
||||||
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span cy-id="rssFeedMarker" v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span>
|
|
||||||
|
|
||||||
<div cy-id="standardBottomText" v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
|
||||||
<p cy-id="standardBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div cy-id="detailBottomText" v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
<div cy-id="detailBottomText" v-else class="relative z-30 left-0 right-0 mx-auto py-1 rounded-md text-center">
|
||||||
<p cy-id="detailBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
<p cy-id="detailBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ displayTitle }}</p>
|
||||||
<p cy-id="detailBottomSortLine" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
<p cy-id="detailBottomSortLine" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displaySortLine }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -32,13 +36,14 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
index: Number,
|
index: Number,
|
||||||
width: Number,
|
width: Number,
|
||||||
height: Number,
|
height: {
|
||||||
bookCoverAspectRatio: Number,
|
type: Number,
|
||||||
|
default: 192
|
||||||
|
},
|
||||||
bookshelfView: {
|
bookshelfView: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
},
|
},
|
||||||
isCategorized: Boolean,
|
|
||||||
seriesMount: {
|
seriesMount: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => null
|
default: () => null
|
||||||
@ -56,16 +61,24 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
bookCoverAspectRatio() {
|
||||||
|
return this.store.getters['libraries/getBookCoverAspectRatio']
|
||||||
|
},
|
||||||
|
cardWidth() {
|
||||||
|
return this.width || this.coverHeight * 2
|
||||||
|
},
|
||||||
|
coverHeight() {
|
||||||
|
return this.height * this.sizeMultiplier
|
||||||
|
},
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.store.state.serverSettings.dateFormat
|
return this.store.state.serverSettings.dateFormat
|
||||||
},
|
},
|
||||||
labelFontSize() {
|
labelFontSize() {
|
||||||
if (this.width < 160) return 0.75
|
if (this.width < 160) return 0.75
|
||||||
return 0.875
|
return 0.9
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
|
return this.store.getters['user/getSizeMultiplier']
|
||||||
return this.width / 240
|
|
||||||
},
|
},
|
||||||
seriesId() {
|
seriesId() {
|
||||||
return this.series ? this.series.id : ''
|
return this.series ? this.series.id : ''
|
||||||
@ -78,7 +91,7 @@ export default {
|
|||||||
},
|
},
|
||||||
displayTitle() {
|
displayTitle() {
|
||||||
if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title
|
if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title
|
||||||
return this.title
|
return this.title || '\u00A0'
|
||||||
},
|
},
|
||||||
displaySortLine() {
|
displaySortLine() {
|
||||||
switch (this.orderBy) {
|
switch (this.orderBy) {
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(name)}`">
|
<div>
|
||||||
<div cy-id="card" :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(name)}`">
|
||||||
<div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-40">
|
<div cy-id="card" :style="{ width: cardWidth + 'px', height: cardHeight + 'px', fontSize: sizeMultiplier + 'rem' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
||||||
<span class="material-icons-outlined text-[10rem]">record_voice_over</span>
|
<div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-40">
|
||||||
</div>
|
<span class="material-icons-outlined text-[10em]">record_voice_over</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Narrator name & num books overlay -->
|
<!-- Narrator name & num books overlay -->
|
||||||
<div class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2">
|
<div class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2">
|
||||||
<p cy-id="name" class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
|
<p cy-id="name" class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: 0.75 + 'em' }">{{ name }}</p>
|
||||||
<p cy-id="numBooks" class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
|
<p cy-id="numBooks" class="text-center text-gray-200" :style="{ fontSize: 0.65 + 'em' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nuxt-link>
|
||||||
</nuxt-link>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -21,23 +23,22 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
width: {
|
width: Number,
|
||||||
type: Number,
|
height: {
|
||||||
default: 150
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 100
|
default: 100
|
||||||
},
|
|
||||||
sizeMultiplier: {
|
|
||||||
type: Number,
|
|
||||||
default: 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
cardWidth() {
|
||||||
|
return this.cardHeight * 1.5
|
||||||
|
},
|
||||||
|
cardHeight() {
|
||||||
|
return this.height * this.sizeMultiplier
|
||||||
|
},
|
||||||
name() {
|
name() {
|
||||||
return this.narrator?.name || ''
|
return this.narrator?.name || ''
|
||||||
},
|
},
|
||||||
@ -49,8 +50,11 @@ export default {
|
|||||||
},
|
},
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
|
},
|
||||||
|
sizeMultiplier() {
|
||||||
|
return this.$store.getters['user/getSizeMultiplier']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {}
|
methods: {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent>
|
<div class="rounded-full py-1r bg-primary px-2r border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent>
|
||||||
<span class="material-icons" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize" aria-label="Decrease Cover Size" role="button">remove</span>
|
<span class="material-icons" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize" aria-label="Decrease Cover Size" role="button">remove</span>
|
||||||
<p class="px-2 font-mono">{{ bookCoverWidth }}</p>
|
<p class="px-2r font-mono" style="font-size: 1rem">{{ bookCoverWidth }}</p>
|
||||||
<span class="material-icons" :class="selectedSizeIndex === availableSizes.length - 1 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="increaseSize" aria-label="Increase Cover Size" role="button">add</span>
|
<span class="material-icons" :class="selectedSizeIndex === availableSizes.length - 1 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="increaseSize" aria-label="Increase Cover Size" role="button">add</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -48,4 +48,4 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -3,31 +3,19 @@
|
|||||||
<div class="flex items-center py-3">
|
<div class="flex items-center py-3">
|
||||||
<slot />
|
<slot />
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
|
<button cy-id="leftScrollButton" v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
|
||||||
<span class="material-icons text-2xl">chevron_left</span>
|
<span class="material-icons text-2xl">chevron_left</span>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
|
<button cy-id="rightScrollButton" v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
|
||||||
<span class="material-icons text-2xl">chevron_right</span>
|
<span class="material-icons text-2xl">chevron_right</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll" style="scroll-behavior: smooth" @scroll="scrolled">
|
<div cy-id="slider" ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll" style="scroll-behavior: smooth" @scroll="scrolled">
|
||||||
<div class="flex space-x-4" :style="{ height: height + 'px' }">
|
<div class="flex space-x-4">
|
||||||
<template v-for="(item, index) in items">
|
<template v-for="(item, index) in items">
|
||||||
<cards-lazy-book-card
|
<div cy-id="item" ref="item" :key="itemKeyFunc(item)">
|
||||||
:key="item.id + '-' + shelfId + '-' + index"
|
<component :is="componentName" :ref="itemRefFunc(item)" :index="index" :[itemPropName]="item" :bookshelf-view="bookshelfView" :continue-listening-shelf="continueListeningShelf" class="relative" @edit="editFunc" @editPodcast="editItem" @select="selectItem" @hook:updated="setScrollVars" />
|
||||||
:ref="`slider-item-${item.id}`"
|
</div>
|
||||||
:index="index"
|
|
||||||
:book-mount="item"
|
|
||||||
:height="cardHeight"
|
|
||||||
:width="cardWidth"
|
|
||||||
:book-cover-aspect-ratio="bookCoverAspectRatio"
|
|
||||||
:bookshelf-view="bookshelfView"
|
|
||||||
:continue-listening-shelf="continueListeningShelf"
|
|
||||||
class="relative"
|
|
||||||
@edit="editItem"
|
|
||||||
@select="selectItem"
|
|
||||||
@hook:updated="setScrollVars"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -41,49 +29,112 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
height: {
|
|
||||||
type: Number,
|
|
||||||
default: 192
|
|
||||||
},
|
|
||||||
bookshelfView: {
|
bookshelfView: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1
|
default: 1
|
||||||
},
|
},
|
||||||
shelfId: String,
|
shelfId: {
|
||||||
continueListeningShelf: Boolean
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
continueListeningShelf: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'book'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isScrollable: false,
|
isScrollable: false,
|
||||||
canScrollLeft: false,
|
canScrollLeft: false,
|
||||||
canScrollRight: false,
|
canScrollRight: false,
|
||||||
clientWidth: 0
|
clientWidth: 0,
|
||||||
|
shelfOptionsByType: {
|
||||||
|
episode: {
|
||||||
|
component: 'cards-lazy-book-card',
|
||||||
|
itemPropName: 'book-mount',
|
||||||
|
itemIdFunc: (item) => item.recentEpisode.id
|
||||||
|
},
|
||||||
|
series: {
|
||||||
|
component: 'cards-lazy-series-card',
|
||||||
|
itemPropName: 'series-mount',
|
||||||
|
itemIdFunc: (item) => item.id
|
||||||
|
},
|
||||||
|
authors: {
|
||||||
|
component: 'cards-author-card',
|
||||||
|
itemPropName: 'author',
|
||||||
|
itemIdFunc: (item) => item.id
|
||||||
|
},
|
||||||
|
narrators: {
|
||||||
|
component: 'cards-narrator-card',
|
||||||
|
itemPropName: 'narrator',
|
||||||
|
itemIdFunc: (item) => item.name
|
||||||
|
},
|
||||||
|
book: {
|
||||||
|
component: 'cards-lazy-book-card',
|
||||||
|
itemPropName: 'book-mount',
|
||||||
|
itemIdFunc: (item) => item.id
|
||||||
|
},
|
||||||
|
podcast: {
|
||||||
|
component: 'cards-lazy-book-card',
|
||||||
|
itemPropName: 'book-mount',
|
||||||
|
itemIdFunc: (item) => item.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shelfOptions: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bookCoverAspectRatio() {
|
|
||||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
|
||||||
},
|
|
||||||
cardScaleMulitiplier() {
|
|
||||||
return this.height / 192
|
|
||||||
},
|
|
||||||
cardHeight() {
|
|
||||||
return this.height - 40 * this.cardScaleMulitiplier
|
|
||||||
},
|
|
||||||
cardWidth() {
|
|
||||||
return this.cardHeight / this.bookCoverAspectRatio
|
|
||||||
},
|
|
||||||
booksPerPage() {
|
|
||||||
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
|
||||||
},
|
|
||||||
isSelectionMode() {
|
isSelectionMode() {
|
||||||
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
||||||
|
},
|
||||||
|
options() {
|
||||||
|
if (!this.shelfOptions) {
|
||||||
|
this.shelfOptions = this.shelfOptionsByType[this.type]
|
||||||
|
}
|
||||||
|
return this.shelfOptions
|
||||||
|
},
|
||||||
|
itemIdFunc() {
|
||||||
|
return this.options.itemIdFunc
|
||||||
|
},
|
||||||
|
itemKeyFunc() {
|
||||||
|
return (item) => this.itemIdFunc(item) + this.shelfId
|
||||||
|
},
|
||||||
|
itemRefFunc() {
|
||||||
|
return (item) => `slider-item-${this.itemIdFunc(item)}`
|
||||||
|
},
|
||||||
|
componentName() {
|
||||||
|
return this.options.component
|
||||||
|
},
|
||||||
|
itemPropName() {
|
||||||
|
return this.options.itemPropName
|
||||||
|
},
|
||||||
|
editFunc() {
|
||||||
|
switch (this.type) {
|
||||||
|
case 'episode':
|
||||||
|
return this.editEpisode
|
||||||
|
case 'authors':
|
||||||
|
return this.editAuthor
|
||||||
|
default:
|
||||||
|
return this.editItem
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clearSelectedEntities() {
|
clearSelectedEntities() {
|
||||||
this.updateSelectionMode(false)
|
this.updateSelectionMode(false)
|
||||||
},
|
},
|
||||||
|
editEpisode({ libraryItem, episode }) {
|
||||||
|
this.$store.commit('setSelectedLibraryItem', libraryItem)
|
||||||
|
this.$store.commit('globals/setSelectedEpisode', episode)
|
||||||
|
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||||
|
},
|
||||||
|
editAuthor(author) {
|
||||||
|
this.$store.commit('globals/showEditAuthorModal', author)
|
||||||
|
},
|
||||||
editItem(libraryItem) {
|
editItem(libraryItem) {
|
||||||
var itemIds = this.items.map((e) => e.id)
|
var itemIds = this.items.map((e) => e.id)
|
||||||
this.$store.commit('setBookshelfBookIds', itemIds)
|
this.$store.commit('setBookshelfBookIds', itemIds)
|
||||||
@ -99,7 +150,7 @@ export default {
|
|||||||
const selectedMediaItems = this.$store.state.globals.selectedMediaItems
|
const selectedMediaItems = this.$store.state.globals.selectedMediaItems
|
||||||
|
|
||||||
this.items.forEach((item) => {
|
this.items.forEach((item) => {
|
||||||
let component = this.$refs[`slider-item-${item.id}`]
|
let component = this.$refs[this.itemRefFunc(item)]
|
||||||
if (!component || !component.length) return
|
if (!component || !component.length) return
|
||||||
component = component[0]
|
component = component[0]
|
||||||
component.setSelectionMode(val)
|
component.setSelectionMode(val)
|
||||||
@ -113,7 +164,7 @@ export default {
|
|||||||
if (!this.canScrollRight) return
|
if (!this.canScrollRight) return
|
||||||
const slider = this.$refs.slider
|
const slider = this.$refs.slider
|
||||||
if (!slider) return
|
if (!slider) return
|
||||||
const scrollAmount = this.booksPerPage * this.cardWidth
|
const scrollAmount = this.clientWidth
|
||||||
const maxScrollLeft = slider.scrollWidth - slider.clientWidth
|
const maxScrollLeft = slider.scrollWidth - slider.clientWidth
|
||||||
|
|
||||||
const newScrollLeft = Math.min(maxScrollLeft, slider.scrollLeft + scrollAmount)
|
const newScrollLeft = Math.min(maxScrollLeft, slider.scrollLeft + scrollAmount)
|
||||||
@ -124,7 +175,7 @@ export default {
|
|||||||
const slider = this.$refs.slider
|
const slider = this.$refs.slider
|
||||||
if (!slider) return
|
if (!slider) return
|
||||||
|
|
||||||
const scrollAmount = this.booksPerPage * this.cardWidth
|
const scrollAmount = this.clientWidth
|
||||||
|
|
||||||
const newScrollLeft = Math.max(0, slider.scrollLeft - scrollAmount)
|
const newScrollLeft = Math.max(0, slider.scrollLeft - scrollAmount)
|
||||||
slider.scrollLeft = newScrollLeft
|
slider.scrollLeft = newScrollLeft
|
||||||
@ -133,11 +184,11 @@ export default {
|
|||||||
const slider = this.$refs.slider
|
const slider = this.$refs.slider
|
||||||
if (!slider) return
|
if (!slider) return
|
||||||
const { scrollLeft, scrollWidth, clientWidth } = slider
|
const { scrollLeft, scrollWidth, clientWidth } = slider
|
||||||
const scrollPercent = (scrollLeft + clientWidth) / scrollWidth
|
const scrollRemaining = Math.abs(scrollLeft + clientWidth - scrollWidth)
|
||||||
|
|
||||||
this.clientWidth = clientWidth
|
this.clientWidth = clientWidth
|
||||||
this.isScrollable = scrollWidth > clientWidth
|
this.isScrollable = scrollWidth > clientWidth
|
||||||
this.canScrollRight = scrollPercent < 1
|
this.canScrollRight = scrollRemaining >= 1
|
||||||
this.canScrollLeft = scrollLeft > 0
|
this.canScrollLeft = scrollLeft > 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -145,12 +196,27 @@ export default {
|
|||||||
this.setScrollVars()
|
this.setScrollVars()
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
|
this.setScrollVars()
|
||||||
this.$eventBus.$on('item-selected', this.itemSelectedEvt)
|
switch (this.type) {
|
||||||
|
case 'series':
|
||||||
|
return
|
||||||
|
case 'authors':
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||||
|
this.$eventBus.$on('item-selected', this.itemSelectedEvt)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
|
switch (this.type) {
|
||||||
this.$eventBus.$off('item-selected', this.itemSelectedEvt)
|
case 'series':
|
||||||
|
return
|
||||||
|
case 'authors':
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||||
|
this.$eventBus.$off('item-selected', this.itemSelectedEvt)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -13,9 +13,6 @@ describe('AuthorCard', () => {
|
|||||||
|
|
||||||
const propsData = {
|
const propsData = {
|
||||||
author,
|
author,
|
||||||
width: 192 * 0.8,
|
|
||||||
height: 192,
|
|
||||||
sizeMultiplier: 1,
|
|
||||||
nameBelow: false
|
nameBelow: false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,7 +24,8 @@ describe('AuthorCard', () => {
|
|||||||
$store: {
|
$store: {
|
||||||
getters: {
|
getters: {
|
||||||
'user/getUserCanUpdate': true,
|
'user/getUserCanUpdate': true,
|
||||||
'libraries/getLibraryProvider': () => 'audible.us'
|
'libraries/getLibraryProvider': () => 'audible.us',
|
||||||
|
'user/getSizeMultiplier': 1
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
libraries: {
|
libraries: {
|
||||||
@ -36,9 +34,9 @@ describe('AuthorCard', () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
$eventBus: {
|
$eventBus: {
|
||||||
$on: () => { },
|
$on: () => {},
|
||||||
$off: () => { },
|
$off: () => {}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stubs = {
|
const stubs = {
|
||||||
@ -59,8 +57,10 @@ describe('AuthorCard', () => {
|
|||||||
cy.get('&card').should(($el) => {
|
cy.get('&card').should(($el) => {
|
||||||
const width = $el.width()
|
const width = $el.width()
|
||||||
const height = $el.height()
|
const height = $el.height()
|
||||||
expect(width).to.be.closeTo(propsData.width, 0.01)
|
const defaultHeight = 192
|
||||||
expect(height).to.be.closeTo(propsData.height, 0.01)
|
const defaultWidth = defaultHeight * 0.8
|
||||||
|
expect(width).to.be.closeTo(defaultWidth, 0.01)
|
||||||
|
expect(height).to.be.closeTo(defaultHeight, 0.01)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -78,21 +78,25 @@ describe('AuthorCard', () => {
|
|||||||
.and(($el) => {
|
.and(($el) => {
|
||||||
const height = $el.height()
|
const height = $el.height()
|
||||||
const width = $el.width()
|
const width = $el.width()
|
||||||
const sizeMultiplier = propsData.sizeMultiplier
|
const sizeMultiplier = 1
|
||||||
const defaultFontSize = 16
|
const defaultFontSize = 16
|
||||||
const defaultLineHeight = 1.5
|
const defaultLineHeight = 1.5
|
||||||
const fontSizeMultiplier = 0.75
|
const fontSizeMultiplier = 0.75
|
||||||
const px2 = 16
|
const px2 = 16
|
||||||
|
const defaultHeight = 192
|
||||||
|
const defaultWidth = defaultHeight * 0.8
|
||||||
expect(height).to.be.closeTo(defaultFontSize * fontSizeMultiplier * sizeMultiplier * defaultLineHeight, 0.01)
|
expect(height).to.be.closeTo(defaultFontSize * fontSizeMultiplier * sizeMultiplier * defaultLineHeight, 0.01)
|
||||||
nameBelowHeight = height
|
nameBelowHeight = height
|
||||||
expect(width).to.be.closeTo(propsData.width - px2, 0.01)
|
expect(width).to.be.closeTo(defaultWidth - px2, 0.01)
|
||||||
})
|
})
|
||||||
cy.get('&card').should(($el) => {
|
cy.get('&card').should(($el) => {
|
||||||
const width = $el.width()
|
const width = $el.width()
|
||||||
const height = $el.height()
|
const height = $el.height()
|
||||||
const py1 = 8
|
const py1 = 8
|
||||||
expect(width).to.be.closeTo(propsData.width, 0.01)
|
const defaultHeight = 192
|
||||||
expect(height).to.be.closeTo(propsData.height + nameBelowHeight + py1, 0.01)
|
const defaultWidth = defaultHeight * 0.8
|
||||||
|
expect(width).to.be.closeTo(defaultWidth, 0.01)
|
||||||
|
expect(height).to.be.closeTo(defaultHeight + nameBelowHeight + py1, 0.01)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -110,11 +114,12 @@ describe('AuthorCard', () => {
|
|||||||
cy.get('&card').trigger('mouseleave')
|
cy.get('&card').trigger('mouseleave')
|
||||||
cy.get('&match').should('be.hidden')
|
cy.get('&match').should('be.hidden')
|
||||||
cy.get('&edit').should('be.hidden')
|
cy.get('&edit').should('be.hidden')
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders the component with spinner while searching', () => {
|
it('renders the component with spinner while searching', () => {
|
||||||
const data = () => { return { searching: true, isHovering: false } }
|
const data = () => {
|
||||||
|
return { searching: true, isHovering: false }
|
||||||
|
}
|
||||||
cy.mount(AuthorCard, { ...mountOptions, data })
|
cy.mount(AuthorCard, { ...mountOptions, data })
|
||||||
|
|
||||||
cy.get('&textInline').should('be.hidden')
|
cy.get('&textInline').should('be.hidden')
|
||||||
@ -171,7 +176,7 @@ describe('AuthorCard', () => {
|
|||||||
const updatedMocks = {
|
const updatedMocks = {
|
||||||
...mocks,
|
...mocks,
|
||||||
$axios: {
|
$axios: {
|
||||||
$post: cy.stub().resolves({ updated: true, author: { name: 'John Doe', imagePath: "path/to/image" } })
|
$post: cy.stub().resolves({ updated: true, author: { name: 'John Doe', imagePath: 'path/to/image' } })
|
||||||
},
|
},
|
||||||
$toast: {
|
$toast: {
|
||||||
success: cy.stub().as('success'),
|
success: cy.stub().as('success'),
|
||||||
@ -188,4 +193,4 @@ describe('AuthorCard', () => {
|
|||||||
cy.get('@error').should('not.have.been.called')
|
cy.get('@error').should('not.have.been.called')
|
||||||
cy.get('@info').should('not.have.been.called')
|
cy.get('@info').should('not.have.been.called')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
85
client/cypress/tests/components/cards/ItemSlider.cy.js
Normal file
85
client/cypress/tests/components/cards/ItemSlider.cy.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import ItemSlider from '@/components/widgets/ItemSlider.vue'
|
||||||
|
import NarratorCard from '@/components/cards/NarratorCard.vue'
|
||||||
|
import AuthorCard from '@/components/cards/AuthorCard.vue'
|
||||||
|
|
||||||
|
function createMountOptions(shelftype) {
|
||||||
|
const items = {
|
||||||
|
narrators: [
|
||||||
|
{ name: 'John Doe', numBooks: 5 },
|
||||||
|
{ name: 'Jane Doe', numBooks: 3 },
|
||||||
|
{ name: 'Jack Doe', numBooks: 1 },
|
||||||
|
{ name: 'Jill Doe', numBooks: 7 }
|
||||||
|
],
|
||||||
|
authors: [
|
||||||
|
{ id: 1, name: 'John Doe', numBooks: 5 },
|
||||||
|
{ id: 2, name: 'Jane Doe', numBooks: 3 },
|
||||||
|
{ id: 3, name: 'Jack Doe', numBooks: 1 },
|
||||||
|
{ id: 4, name: 'Jill Doe', numBooks: 7 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const propsData = {
|
||||||
|
items: items[shelftype],
|
||||||
|
shelfId: 'shelf-123',
|
||||||
|
type: shelftype
|
||||||
|
}
|
||||||
|
const stubs = {
|
||||||
|
'cards-narrator-card': NarratorCard,
|
||||||
|
'cards-author-card': AuthorCard
|
||||||
|
}
|
||||||
|
const mocks = {
|
||||||
|
$store: {
|
||||||
|
getters: {
|
||||||
|
'user/getUserCanUpdate': true,
|
||||||
|
'user/getSizeMultiplier': 1,
|
||||||
|
'globals/getIsBatchSelectingMediaItems': false
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
libraries: {
|
||||||
|
currentLibraryId: 'library-123'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
$eventBus: {
|
||||||
|
$on: () => {},
|
||||||
|
$off: () => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const slots = {
|
||||||
|
default: `<p class="font-semibold text-gray-100">${shelftype}</p>`
|
||||||
|
}
|
||||||
|
|
||||||
|
return { propsData, stubs, mocks, slots }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ItemSlider', () => {
|
||||||
|
let mountOptions = null
|
||||||
|
|
||||||
|
beforeEach(() => {})
|
||||||
|
|
||||||
|
it('renders a narrators slider', () => {
|
||||||
|
mountOptions = createMountOptions('narrators')
|
||||||
|
cy.mount(ItemSlider, mountOptions)
|
||||||
|
|
||||||
|
cy.get('&item').should('have.length', 4)
|
||||||
|
cy.get('&leftScrollButton').should('be.visible').and('not.have.class', 'text-gray-300')
|
||||||
|
cy.get('&rightScrollButton').should('be.visible').and('have.class', 'text-gray-300')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders an authors slider', () => {
|
||||||
|
mountOptions = createMountOptions('authors')
|
||||||
|
cy.mount(ItemSlider, mountOptions)
|
||||||
|
|
||||||
|
cy.get('&item').should('have.length', 4)
|
||||||
|
cy.get('&leftScrollButton').should('be.visible').and('not.have.class', 'text-gray-300')
|
||||||
|
cy.get('&rightScrollButton').should('be.visible').and('have.class', 'text-gray-300')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides the scroll button when all items are visible', () => {
|
||||||
|
mountOptions = createMountOptions('narrators')
|
||||||
|
mountOptions.propsData.items = mountOptions.propsData.items.slice(0, 2)
|
||||||
|
cy.mount(ItemSlider, mountOptions)
|
||||||
|
|
||||||
|
cy.get('&leftScrollButton').should('not.exist')
|
||||||
|
cy.get('&rightScrollButton').should('not.exist')
|
||||||
|
})
|
||||||
|
})
|
@ -7,61 +7,21 @@ import { Constants } from '@/plugins/constants'
|
|||||||
function createMountOptions() {
|
function createMountOptions() {
|
||||||
const book = {
|
const book = {
|
||||||
id: '1',
|
id: '1',
|
||||||
ino: '281474976785140',
|
|
||||||
libraryId: 'library-123',
|
libraryId: 'library-123',
|
||||||
folderId: 'folder-123',
|
|
||||||
path: '/path/to/book',
|
|
||||||
relPath: 'book',
|
|
||||||
isFile: false,
|
|
||||||
mtimeMs: 1689017292016,
|
|
||||||
ctimeMs: 1689017292016,
|
|
||||||
birthtimeMs: 1689017281555,
|
|
||||||
addedAt: 1700154928492,
|
|
||||||
updatedAt: 1713300533345,
|
|
||||||
isMissing: false,
|
|
||||||
isInvalid: false,
|
|
||||||
mediaType: 'book',
|
mediaType: 'book',
|
||||||
media: {
|
media: {
|
||||||
id: 'book1',
|
id: 'book1',
|
||||||
metadata: {
|
metadata: { title: 'The Fellowship of the Ring', titleIgnorePrefix: 'Fellowship of the Ring', authorName: 'J. R. R. Tolkien' },
|
||||||
title: 'The Fellowship of the Ring',
|
numTracks: 1
|
||||||
titleIgnorePrefix: 'Fellowship of the Ring',
|
}
|
||||||
subtitle: 'LOTR, Book 1',
|
|
||||||
authorName: 'J. R. R. Tolkien',
|
|
||||||
authorNameLF: 'Tolkien, J. R. R.',
|
|
||||||
narratorName: 'Andy Sirkis',
|
|
||||||
genres: ['Science Fiction & Fantasy'],
|
|
||||||
publishedYear: '2017',
|
|
||||||
publishedDate: null,
|
|
||||||
publisher: 'Book Publisher',
|
|
||||||
description: 'Book Description',
|
|
||||||
isbn: null,
|
|
||||||
asin: 'B075LXMLNV',
|
|
||||||
language: 'English',
|
|
||||||
explicit: false,
|
|
||||||
abridged: false
|
|
||||||
},
|
|
||||||
coverPath: null,
|
|
||||||
tags: ['Fantasy', 'Adventure'],
|
|
||||||
numTracks: 1,
|
|
||||||
numAudioFiles: 1,
|
|
||||||
numChapters: 31,
|
|
||||||
duration: 64410,
|
|
||||||
size: 511206878
|
|
||||||
},
|
|
||||||
numFiles: 4,
|
|
||||||
size: 511279587
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const propsData = {
|
const propsData = {
|
||||||
index: 0,
|
index: 0,
|
||||||
bookMount: book,
|
bookMount: book,
|
||||||
bookCoverAspectRatio: 1,
|
|
||||||
bookshelfView: Constants.BookshelfView.DETAIL,
|
bookshelfView: Constants.BookshelfView.DETAIL,
|
||||||
continueListeningShelf: false,
|
continueListeningShelf: false,
|
||||||
filterBy: null,
|
filterBy: null,
|
||||||
width: 192,
|
|
||||||
height: 192,
|
|
||||||
sortingIgnorePrefix: false,
|
sortingIgnorePrefix: false,
|
||||||
orderBy: null
|
orderBy: null
|
||||||
}
|
}
|
||||||
@ -84,7 +44,9 @@ function createMountOptions() {
|
|||||||
'user/getUserCanDownload': true,
|
'user/getUserCanDownload': true,
|
||||||
'user/getIsAdminOrUp': true,
|
'user/getIsAdminOrUp': true,
|
||||||
'user/getUserMediaProgress': (id) => null,
|
'user/getUserMediaProgress': (id) => null,
|
||||||
|
'user/getSizeMultiplier': 1,
|
||||||
'libraries/getLibraryProvider': () => 'audible.us',
|
'libraries/getLibraryProvider': () => 'audible.us',
|
||||||
|
'libraries/getBookCoverAspectRatio': 1,
|
||||||
'globals/getLibraryItemCoverSrc': () => 'https://my.server.com/book_placeholder.jpg',
|
'globals/getLibraryItemCoverSrc': () => 'https://my.server.com/book_placeholder.jpg',
|
||||||
getLibraryItemsStreaming: () => null,
|
getLibraryItemsStreaming: () => null,
|
||||||
getIsMediaQueued: () => false,
|
getIsMediaQueued: () => false,
|
||||||
@ -109,16 +71,6 @@ describe('LazyBookCard', () => {
|
|||||||
let mountOptions = null
|
let mountOptions = null
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mountOptions = createMountOptions()
|
mountOptions = createMountOptions()
|
||||||
// cy.intercept(
|
|
||||||
// 'https://my.server.com/**/*',
|
|
||||||
// { middleware: true },
|
|
||||||
// (req) => {
|
|
||||||
// req.on('before:response', (res) => {
|
|
||||||
// // force all API responses to not be cached
|
|
||||||
// res.headers['cache-control'] = 'no-store'
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
})
|
})
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
@ -174,11 +126,14 @@ describe('LazyBookCard', () => {
|
|||||||
// the detailBottom element, currently rendered outside the card's area,
|
// the detailBottom element, currently rendered outside the card's area,
|
||||||
// and requires complex layout calculations outside of the component.
|
// and requires complex layout calculations outside of the component.
|
||||||
// todo: fix the component to render the detailBottom element inside the card's area
|
// todo: fix the component to render the detailBottom element inside the card's area
|
||||||
cy.get('#book-card-0').should(($el) => {
|
cy.get('#cover-area-0').should(($el) => {
|
||||||
const width = $el.width()
|
const width = $el.width()
|
||||||
const height = $el.height()
|
const height = $el.height()
|
||||||
expect(width).to.be.closeTo(mountOptions.propsData.width, 0.01)
|
const defaultHeight = 192
|
||||||
expect(height).to.be.closeTo(mountOptions.propsData.height, 0.01)
|
const defaultWidth = defaultHeight
|
||||||
|
|
||||||
|
expect(width).to.be.closeTo(defaultWidth, 0.01)
|
||||||
|
expect(height).to.be.closeTo(defaultHeight, 0.01)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -201,6 +156,7 @@ describe('LazyBookCard', () => {
|
|||||||
cy.mount(LazyBookCard, mountOptions)
|
cy.mount(LazyBookCard, mountOptions)
|
||||||
cy.get('#book-card-0').click()
|
cy.get('#book-card-0').click()
|
||||||
|
|
||||||
|
cy.get('&titleImageNotReady').should('be.hidden')
|
||||||
cy.get('@routerPush').should('have.been.calledOnceWithExactly', '/item/1')
|
cy.get('@routerPush').should('have.been.calledOnceWithExactly', '/item/1')
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -215,6 +171,7 @@ describe('LazyBookCard', () => {
|
|||||||
mountOptions.mocks.$store.getters['globals/getLibraryItemCoverSrc'] = () => 'https://my.server.com/cover1.jpg'
|
mountOptions.mocks.$store.getters['globals/getLibraryItemCoverSrc'] = () => 'https://my.server.com/cover1.jpg'
|
||||||
cy.mount(LazyBookCard, mountOptions)
|
cy.mount(LazyBookCard, mountOptions)
|
||||||
|
|
||||||
|
cy.get('&titleImageNotReady').should('be.hidden')
|
||||||
cy.get('&coverBg').should('be.visible')
|
cy.get('&coverBg').should('be.visible')
|
||||||
cy.get('&coverImage').should('have.class', 'object-contain')
|
cy.get('&coverImage').should('have.class', 'object-contain')
|
||||||
})
|
})
|
||||||
@ -223,6 +180,7 @@ describe('LazyBookCard', () => {
|
|||||||
mountOptions.mocks.$store.getters['globals/getLibraryItemCoverSrc'] = () => 'https://my.server.com/cover2.jpg'
|
mountOptions.mocks.$store.getters['globals/getLibraryItemCoverSrc'] = () => 'https://my.server.com/cover2.jpg'
|
||||||
cy.mount(LazyBookCard, mountOptions)
|
cy.mount(LazyBookCard, mountOptions)
|
||||||
|
|
||||||
|
cy.get('&titleImageNotReady').should('be.hidden')
|
||||||
cy.get('&coverBg').should('be.hidden')
|
cy.get('&coverBg').should('be.hidden')
|
||||||
cy.get('&coverImage').should('have.class', 'object-fill')
|
cy.get('&coverImage').should('have.class', 'object-fill')
|
||||||
})
|
})
|
||||||
@ -235,6 +193,7 @@ describe('LazyBookCard', () => {
|
|||||||
mountOptions.propsData.bookMount.media.coverPath = 'cover1.jpg'
|
mountOptions.propsData.bookMount.media.coverPath = 'cover1.jpg'
|
||||||
cy.mount(LazyBookCard, mountOptions)
|
cy.mount(LazyBookCard, mountOptions)
|
||||||
|
|
||||||
|
cy.get('&titleImageNotReady').should('be.hidden')
|
||||||
cy.get('&placeholderTitle').should('not.exist')
|
cy.get('&placeholderTitle').should('not.exist')
|
||||||
cy.get('&placeholderAuthor').should('not.exist')
|
cy.get('&placeholderAuthor').should('not.exist')
|
||||||
})
|
})
|
||||||
@ -243,6 +202,7 @@ describe('LazyBookCard', () => {
|
|||||||
mountOptions.propsData.bookshelfView = Constants.BookshelfView.STANDARD
|
mountOptions.propsData.bookshelfView = Constants.BookshelfView.STANDARD
|
||||||
cy.mount(LazyBookCard, mountOptions)
|
cy.mount(LazyBookCard, mountOptions)
|
||||||
|
|
||||||
|
cy.get('&titleImageNotReady').should('be.hidden')
|
||||||
cy.get('&detailBottom').should('not.exist')
|
cy.get('&detailBottom').should('not.exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -250,6 +210,7 @@ describe('LazyBookCard', () => {
|
|||||||
mountOptions.propsData.bookMount.media.metadata.explicit = true
|
mountOptions.propsData.bookMount.media.metadata.explicit = true
|
||||||
cy.mount(LazyBookCard, mountOptions)
|
cy.mount(LazyBookCard, mountOptions)
|
||||||
|
|
||||||
|
cy.get('&titleImageNotReady').should('be.hidden')
|
||||||
cy.get('&explicitIndicator').should('be.visible')
|
cy.get('&explicitIndicator').should('be.visible')
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -267,6 +228,7 @@ describe('LazyBookCard', () => {
|
|||||||
it('shows the collpased series', () => {
|
it('shows the collpased series', () => {
|
||||||
cy.mount(LazyBookCard, mountOptions)
|
cy.mount(LazyBookCard, mountOptions)
|
||||||
|
|
||||||
|
cy.get('&titleImageNotReady').should('be.hidden')
|
||||||
cy.get('&seriesSequenceList').should('not.exist')
|
cy.get('&seriesSequenceList').should('not.exist')
|
||||||
cy.get('&booksInSeries').should('be.visible').and('have.text', '3')
|
cy.get('&booksInSeries').should('be.visible').and('have.text', '3')
|
||||||
cy.get('&title').should('be.visible').and('have.text', 'The Lord of the Rings')
|
cy.get('&title').should('be.visible').and('have.text', 'The Lord of the Rings')
|
||||||
@ -283,6 +245,7 @@ describe('LazyBookCard', () => {
|
|||||||
cy.mount(LazyBookCard, mountOptions)
|
cy.mount(LazyBookCard, mountOptions)
|
||||||
cy.get('#book-card-0').trigger('mouseover')
|
cy.get('#book-card-0').trigger('mouseover')
|
||||||
|
|
||||||
|
cy.get('&titleImageNotReady').should('be.hidden')
|
||||||
cy.get('&seriesNameOverlay').should('be.visible').and('have.text', 'Middle Earth Chronicles')
|
cy.get('&seriesNameOverlay').should('be.visible').and('have.text', 'Middle Earth Chronicles')
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -290,6 +253,7 @@ describe('LazyBookCard', () => {
|
|||||||
mountOptions.propsData.bookMount.collapsedSeries.seriesSequenceList = '1-3'
|
mountOptions.propsData.bookMount.collapsedSeries.seriesSequenceList = '1-3'
|
||||||
cy.mount(LazyBookCard, mountOptions)
|
cy.mount(LazyBookCard, mountOptions)
|
||||||
|
|
||||||
|
cy.get('&titleImageNotReady').should('be.hidden')
|
||||||
cy.get('&seriesSequenceList').should('be.visible').and('have.text', '#1-3')
|
cy.get('&seriesSequenceList').should('be.visible').and('have.text', '#1-3')
|
||||||
cy.get('&booksInSeries').should('not.exist')
|
cy.get('&booksInSeries').should('not.exist')
|
||||||
})
|
})
|
||||||
@ -299,6 +263,7 @@ describe('LazyBookCard', () => {
|
|||||||
cy.mount(LazyBookCard, mountOptions)
|
cy.mount(LazyBookCard, mountOptions)
|
||||||
cy.get('#book-card-0').click()
|
cy.get('#book-card-0').click()
|
||||||
|
|
||||||
|
cy.get('&titleImageNotReady').should('be.hidden')
|
||||||
cy.get('@routerPush').should('have.been.calledOnceWithExactly', '/library/library-123/series/series-123')
|
cy.get('@routerPush').should('have.been.calledOnceWithExactly', '/library/library-123/series/series-123')
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -315,12 +280,15 @@ describe('LazyBookCard', () => {
|
|||||||
}
|
}
|
||||||
cy.mount(LazyBookCard, mountOptions)
|
cy.mount(LazyBookCard, mountOptions)
|
||||||
|
|
||||||
|
cy.get('&titleImageNotReady').should('be.hidden')
|
||||||
cy.get('&progressBar')
|
cy.get('&progressBar')
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.and('have.class', 'bg-yellow-400')
|
.and('have.class', 'bg-yellow-400')
|
||||||
.and(($el) => {
|
.and(($el) => {
|
||||||
const width = $el.width()
|
const width = $el.width()
|
||||||
expect(width).to.be.closeTo(((1 + 0.5) / 3) * mountOptions.propsData.width, 0.01)
|
const defaultHeight = 192
|
||||||
|
const defaultWidth = defaultHeight
|
||||||
|
expect(width).to.be.closeTo(((1 + 0.5) / 3) * defaultWidth, 0.01)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -330,12 +298,15 @@ describe('LazyBookCard', () => {
|
|||||||
}
|
}
|
||||||
cy.mount(LazyBookCard, mountOptions)
|
cy.mount(LazyBookCard, mountOptions)
|
||||||
|
|
||||||
|
cy.get('&titleImageNotReady').should('be.hidden')
|
||||||
cy.get('&progressBar')
|
cy.get('&progressBar')
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.and('have.class', 'bg-success')
|
.and('have.class', 'bg-success')
|
||||||
.and(($el) => {
|
.and(($el) => {
|
||||||
const width = $el.width()
|
const width = $el.width()
|
||||||
expect(width).to.be.equal(mountOptions.propsData.width)
|
const defaultHeight = 192
|
||||||
|
const defaultWidth = defaultHeight
|
||||||
|
expect(width).to.be.equal(defaultWidth)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -18,10 +18,6 @@ describe('LazySeriesCard', () => {
|
|||||||
|
|
||||||
const propsData = {
|
const propsData = {
|
||||||
index: 0,
|
index: 0,
|
||||||
width: 192 * 2,
|
|
||||||
height: 192,
|
|
||||||
sizeMultiplier: 1,
|
|
||||||
bookCoverAspectRatio: 1,
|
|
||||||
bookshelfView: 1,
|
bookshelfView: 1,
|
||||||
isCategorized: false,
|
isCategorized: false,
|
||||||
seriesMount: series,
|
seriesMount: series,
|
||||||
@ -38,6 +34,8 @@ describe('LazySeriesCard', () => {
|
|||||||
getters: {
|
getters: {
|
||||||
'user/getUserCanUpdate': true,
|
'user/getUserCanUpdate': true,
|
||||||
'user/getUserMediaProgress': (id) => null,
|
'user/getUserMediaProgress': (id) => null,
|
||||||
|
'user/getSizeMultiplier': 1,
|
||||||
|
'libraries/getBookCoverAspectRatio': 1,
|
||||||
'libraries/getLibraryProvider': () => 'audible.us',
|
'libraries/getLibraryProvider': () => 'audible.us',
|
||||||
'globals/getLibraryItemCoverSrc': () => 'https://my.server.com/book_placeholder.jpg'
|
'globals/getLibraryItemCoverSrc': () => 'https://my.server.com/book_placeholder.jpg'
|
||||||
},
|
},
|
||||||
@ -62,11 +60,13 @@ describe('LazySeriesCard', () => {
|
|||||||
it('renders the component', () => {
|
it('renders the component', () => {
|
||||||
cy.mount(LazySeriesCard, { propsData, stubs, mocks })
|
cy.mount(LazySeriesCard, { propsData, stubs, mocks })
|
||||||
|
|
||||||
cy.get('&card').should(($el) => {
|
cy.get('&covers-area').should(($el) => {
|
||||||
const width = $el.width()
|
const width = $el.width()
|
||||||
const height = $el.height()
|
const height = $el.height()
|
||||||
expect(width).to.be.closeTo(propsData.width, 0.01)
|
const defailtHeight = 192
|
||||||
expect(height).to.be.closeTo(propsData.height, 0.01)
|
const defaultWidth = defailtHeight * 2
|
||||||
|
expect(width).to.be.closeTo(defaultWidth, 0.01)
|
||||||
|
expect(height).to.be.closeTo(defailtHeight, 0.01)
|
||||||
})
|
})
|
||||||
cy.get('&seriesLengthMarker').should('be.visible').and('have.text', propsData.seriesMount.books.length)
|
cy.get('&seriesLengthMarker').should('be.visible').and('have.text', propsData.seriesMount.books.length)
|
||||||
cy.get('&seriesProgressBar').should('not.exist')
|
cy.get('&seriesProgressBar').should('not.exist')
|
||||||
@ -126,7 +126,9 @@ describe('LazySeriesCard', () => {
|
|||||||
.and('have.class', 'bg-yellow-400')
|
.and('have.class', 'bg-yellow-400')
|
||||||
.and(($el) => {
|
.and(($el) => {
|
||||||
const width = $el.width()
|
const width = $el.width()
|
||||||
expect(width).to.be.closeTo(((1 + 0.5) / 3) * propsData.width, 0.01)
|
const defailtHeight = 192
|
||||||
|
const defaultWidth = defailtHeight * 2
|
||||||
|
expect(width).to.be.closeTo(((1 + 0.5) / 3) * defaultWidth, 0.01)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -150,7 +152,9 @@ describe('LazySeriesCard', () => {
|
|||||||
.and('have.class', 'bg-success')
|
.and('have.class', 'bg-success')
|
||||||
.and(($el) => {
|
.and(($el) => {
|
||||||
const width = $el.width()
|
const width = $el.width()
|
||||||
expect(width).to.equal(propsData.width)
|
const defailtHeight = 192
|
||||||
|
const defaultWidth = defailtHeight * 2
|
||||||
|
expect(width).to.equal(defaultWidth)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -6,15 +6,13 @@ describe('<NarratorCard />', () => {
|
|||||||
numBooks: 5
|
numBooks: 5
|
||||||
}
|
}
|
||||||
const propsData = {
|
const propsData = {
|
||||||
narrator,
|
narrator
|
||||||
width: 200,
|
|
||||||
height: 150,
|
|
||||||
sizeMultiplier: 1.2
|
|
||||||
}
|
}
|
||||||
const mocks = {
|
const mocks = {
|
||||||
$store: {
|
$store: {
|
||||||
getters: {
|
getters: {
|
||||||
'user/getUserCanUpdate': true
|
'user/getUserCanUpdate': true,
|
||||||
|
'user/getSizeMultiplier': 1
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
libraries: {
|
libraries: {
|
||||||
@ -46,7 +44,7 @@ describe('<NarratorCard />', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders 1 book correctly', () => {
|
it('renders 1 book correctly', () => {
|
||||||
let propsData = { narrator: { name: 'John Doe', numBooks: 1 }, width: 200, height: 150, sizeMultiplier: 1.2 }
|
let propsData = { narrator: { name: 'John Doe', numBooks: 1 }, width: 200, height: 150 }
|
||||||
let mountOptions = { propsData, mocks }
|
let mountOptions = { propsData, mocks }
|
||||||
cy.mount(NarratorCard, mountOptions)
|
cy.mount(NarratorCard, mountOptions)
|
||||||
|
|
||||||
@ -54,7 +52,7 @@ describe('<NarratorCard />', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders the default name and num-books when narrator is not provided', () => {
|
it('renders the default name and num-books when narrator is not provided', () => {
|
||||||
let propsData = { width: 200, height: 150, sizeMultiplier: 1.2 }
|
let propsData = { width: 200, height: 150 }
|
||||||
let mountOptions = { propsData, mocks }
|
let mountOptions = { propsData, mocks }
|
||||||
cy.mount(NarratorCard, mountOptions)
|
cy.mount(NarratorCard, mountOptions)
|
||||||
cy.get('&name').should('have.text', '')
|
cy.get('&name').should('have.text', '')
|
||||||
@ -64,12 +62,12 @@ describe('<NarratorCard />', () => {
|
|||||||
it('has the correct width and height', () => {
|
it('has the correct width and height', () => {
|
||||||
let mountOptions = { propsData, mocks }
|
let mountOptions = { propsData, mocks }
|
||||||
cy.mount(NarratorCard, mountOptions)
|
cy.mount(NarratorCard, mountOptions)
|
||||||
cy.get('&card').should('have.css', 'width', '200px')
|
cy.get('&card').should('have.css', 'width', '150px')
|
||||||
cy.get('&card').should('have.css', 'height', '150px')
|
cy.get('&card').should('have.css', 'height', '100px')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has the correct width and height when not provided', () => {
|
it('has the correct width and height when not provided', () => {
|
||||||
let propsData = { narrator, sizeMultiplier: 1.2 }
|
let propsData = { narrator }
|
||||||
let mountOptions = { propsData, mocks }
|
let mountOptions = { propsData, mocks }
|
||||||
cy.mount(NarratorCard, mountOptions)
|
cy.mount(NarratorCard, mountOptions)
|
||||||
cy.get('&card').should('have.css', 'width', '150px')
|
cy.get('&card').should('have.css', 'width', '150px')
|
||||||
@ -79,7 +77,8 @@ describe('<NarratorCard />', () => {
|
|||||||
it('has the correct font sizes', () => {
|
it('has the correct font sizes', () => {
|
||||||
let mountOptions = { propsData, mocks }
|
let mountOptions = { propsData, mocks }
|
||||||
cy.mount(NarratorCard, mountOptions)
|
cy.mount(NarratorCard, mountOptions)
|
||||||
cy.get('&name').should('have.css', 'font-size', '14.4px') // 0.75 * 1.2 * 16
|
const defaultFontSize = 16
|
||||||
cy.get('&numBooks').should('have.css', 'font-size', '12.48px') // 0.65 * 1.2 * 16
|
cy.get('&name').should('have.css', 'font-size', `${0.75 * defaultFontSize}px`)
|
||||||
|
cy.get('&numBooks').should('have.css', 'font-size', `${0.65 * defaultFontSize}px`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -9,7 +9,8 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
cardsHelpers: {
|
cardsHelpers: {
|
||||||
mountEntityCard: this.mountEntityCard
|
mountEntityCard: this.mountEntityCard,
|
||||||
|
setCardSize: this.setCardSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -21,6 +22,56 @@ export default {
|
|||||||
if (this.entityName === 'albums') return Vue.extend(LazyAlbumCard)
|
if (this.entityName === 'albums') return Vue.extend(LazyAlbumCard)
|
||||||
return Vue.extend(LazyBookCard)
|
return Vue.extend(LazyBookCard)
|
||||||
},
|
},
|
||||||
|
getComponentName() {
|
||||||
|
if (this.entityName === 'series') return 'cards-lazy-series-card'
|
||||||
|
if (this.entityName === 'collections') return 'cards-lazy-collection-card'
|
||||||
|
if (this.entityName === 'playlists') return 'cards-lazy-playlist-card'
|
||||||
|
if (this.entityName === 'albums') return 'cards-lazy-album-card'
|
||||||
|
return 'cards-lazy-book-card'
|
||||||
|
},
|
||||||
|
async setCardSize() {
|
||||||
|
this.cardWidth = 0
|
||||||
|
this.cardHeight = 0
|
||||||
|
// load a dummy card to get the its width and height
|
||||||
|
const ComponentClass = this.getComponentClass()
|
||||||
|
const props = {
|
||||||
|
index: -1,
|
||||||
|
bookshelfView: this.bookshelfView,
|
||||||
|
sortingIgnorePrefix: !!this.sortingIgnorePrefix
|
||||||
|
}
|
||||||
|
if (this.entityName === 'items') {
|
||||||
|
props.filterBy = this.filterBy
|
||||||
|
props.orderBy = this.orderBy
|
||||||
|
} else if (this.entityName === 'series') {
|
||||||
|
props.orderBy = this.seriesSortBy
|
||||||
|
}
|
||||||
|
const instance = new ComponentClass({
|
||||||
|
propsData: props
|
||||||
|
})
|
||||||
|
instance.$mount()
|
||||||
|
this.resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
for (let entry of entries) {
|
||||||
|
this.cardWidth = entry.contentRect.width
|
||||||
|
this.cardHeight = entry.contentRect.height
|
||||||
|
this.resizeObserver.disconnect()
|
||||||
|
this.$refs.bookshelf.removeChild(instance.$el)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
instance.$el.style.visibility = 'hidden'
|
||||||
|
instance.$el.style.position = 'absolute'
|
||||||
|
this.$refs.bookshelf.appendChild(instance.$el)
|
||||||
|
this.resizeObserver.observe(instance.$el)
|
||||||
|
const timeBefore = performance.now()
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const unwatch = this.$watch('cardWidth', (value) => {
|
||||||
|
if (value) {
|
||||||
|
unwatch()
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const timeAfter = performance.now()
|
||||||
|
},
|
||||||
async mountEntityCard(index) {
|
async mountEntityCard(index) {
|
||||||
var shelf = Math.floor(index / this.entitiesPerShelf)
|
var shelf = Math.floor(index / this.entitiesPerShelf)
|
||||||
var shelfEl = document.getElementById(`shelf-${shelf}`)
|
var shelfEl = document.getElementById(`shelf-${shelf}`)
|
||||||
@ -34,7 +85,7 @@ export default {
|
|||||||
shelfEl.appendChild(bookComponent.$el)
|
shelfEl.appendChild(bookComponent.$el)
|
||||||
if (this.isSelectionMode) {
|
if (this.isSelectionMode) {
|
||||||
bookComponent.setSelectionMode(true)
|
bookComponent.setSelectionMode(true)
|
||||||
if (this.selectedMediaItems.some(i => i.id === bookComponent.libraryItemId) || this.isSelectAll) {
|
if (this.selectedMediaItems.some((i) => i.id === bookComponent.libraryItemId) || this.isSelectAll) {
|
||||||
bookComponent.selected = true
|
bookComponent.selected = true
|
||||||
} else {
|
} else {
|
||||||
bookComponent.selected = false
|
bookComponent.selected = false
|
||||||
@ -45,17 +96,10 @@ export default {
|
|||||||
bookComponent.isHovering = false
|
bookComponent.isHovering = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const shelfOffsetY = 16
|
|
||||||
const row = index % this.entitiesPerShelf
|
|
||||||
const shelfOffsetX = row * this.totalEntityCardWidth + this.bookshelfMarginLeft
|
|
||||||
|
|
||||||
const ComponentClass = this.getComponentClass()
|
const ComponentClass = this.getComponentClass()
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
index,
|
index,
|
||||||
width: this.entityWidth,
|
|
||||||
height: this.entityHeight,
|
|
||||||
bookCoverAspectRatio: this.coverAspectRatio,
|
|
||||||
bookshelfView: this.bookshelfView,
|
bookshelfView: this.bookshelfView,
|
||||||
sortingIgnorePrefix: !!this.sortingIgnorePrefix
|
sortingIgnorePrefix: !!this.sortingIgnorePrefix
|
||||||
}
|
}
|
||||||
@ -82,6 +126,9 @@ export default {
|
|||||||
this.entityComponentRefs[index] = instance
|
this.entityComponentRefs[index] = instance
|
||||||
|
|
||||||
instance.$mount()
|
instance.$mount()
|
||||||
|
const shelfOffsetY = this.shelfPaddingHeight * this.sizeMultiplier
|
||||||
|
const row = index % this.entitiesPerShelf
|
||||||
|
const shelfOffsetX = row * this.totalEntityCardWidth + this.bookshelfMarginLeft
|
||||||
instance.$el.style.transform = `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)`
|
instance.$el.style.transform = `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)`
|
||||||
instance.$el.classList.add('absolute', 'top-0', 'left-0')
|
instance.$el.classList.add('absolute', 'top-0', 'left-0')
|
||||||
shelfEl.appendChild(instance.$el)
|
shelfEl.appendChild(instance.$el)
|
||||||
@ -91,10 +138,10 @@ export default {
|
|||||||
}
|
}
|
||||||
if (this.isSelectionMode) {
|
if (this.isSelectionMode) {
|
||||||
instance.setSelectionMode(true)
|
instance.setSelectionMode(true)
|
||||||
if (instance.libraryItemId && this.selectedMediaItems.some(i => i.id === instance.libraryItemId) || this.isSelectAll) {
|
if ((instance.libraryItemId && this.selectedMediaItems.some((i) => i.id === instance.libraryItemId)) || this.isSelectAll) {
|
||||||
instance.selected = true
|
instance.selected = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div id="bookshelf" class="w-full h-full p-8 overflow-y-auto">
|
<div id="bookshelf" class="w-full h-full p-8 overflow-y-auto">
|
||||||
<div class="flex flex-wrap justify-center">
|
<div class="flex flex-wrap justify-center">
|
||||||
<template v-for="author in authorsSorted">
|
<template v-for="author in authorsSorted">
|
||||||
<cards-author-card :key="author.id" :author="author" :width="160" :height="200" class="p-3" @edit="editAuthor" />
|
<cards-author-card :key="author.id" :author="author" class="p-3" @edit="editAuthor" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,14 +25,14 @@ export const getters = {
|
|||||||
},
|
},
|
||||||
getUserMediaProgress: (state) => (libraryItemId, episodeId = null) => {
|
getUserMediaProgress: (state) => (libraryItemId, episodeId = null) => {
|
||||||
if (!state.user.mediaProgress) return null
|
if (!state.user.mediaProgress) return null
|
||||||
return state.user.mediaProgress.find(li => {
|
return state.user.mediaProgress.find((li) => {
|
||||||
if (episodeId && li.episodeId !== episodeId) return false
|
if (episodeId && li.episodeId !== episodeId) return false
|
||||||
return li.libraryItemId == libraryItemId
|
return li.libraryItemId == libraryItemId
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getUserBookmarksForItem: (state) => (libraryItemId) => {
|
getUserBookmarksForItem: (state) => (libraryItemId) => {
|
||||||
if (!state.user.bookmarks) return []
|
if (!state.user.bookmarks) return []
|
||||||
return state.user.bookmarks.filter(bm => bm.libraryItemId === libraryItemId)
|
return state.user.bookmarks.filter((bm) => bm.libraryItemId === libraryItemId)
|
||||||
},
|
},
|
||||||
getUserSetting: (state) => (key) => {
|
getUserSetting: (state) => (key) => {
|
||||||
return state.settings?.[key] || null
|
return state.settings?.[key] || null
|
||||||
@ -65,6 +65,9 @@ export const getters = {
|
|||||||
getIsSeriesRemovedFromContinueListening: (state) => (seriesId) => {
|
getIsSeriesRemovedFromContinueListening: (state) => (seriesId) => {
|
||||||
if (!state.user || !state.user.seriesHideFromContinueListening || !state.user.seriesHideFromContinueListening.length) return false
|
if (!state.user || !state.user.seriesHideFromContinueListening || !state.user.seriesHideFromContinueListening.length) return false
|
||||||
return state.user.seriesHideFromContinueListening.includes(seriesId)
|
return state.user.seriesHideFromContinueListening.includes(seriesId)
|
||||||
|
},
|
||||||
|
getSizeMultiplier: (state) => {
|
||||||
|
return state.settings.bookshelfCoverSize / 120
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,9 +155,9 @@ export const mutations = {
|
|||||||
updateMediaProgress(state, { id, data }) {
|
updateMediaProgress(state, { id, data }) {
|
||||||
if (!state.user) return
|
if (!state.user) return
|
||||||
if (!data) {
|
if (!data) {
|
||||||
state.user.mediaProgress = state.user.mediaProgress.filter(lip => lip.id != id)
|
state.user.mediaProgress = state.user.mediaProgress.filter((lip) => lip.id != id)
|
||||||
} else {
|
} else {
|
||||||
var indexOf = state.user.mediaProgress.findIndex(lip => lip.id == id)
|
var indexOf = state.user.mediaProgress.findIndex((lip) => lip.id == id)
|
||||||
if (indexOf >= 0) {
|
if (indexOf >= 0) {
|
||||||
state.user.mediaProgress.splice(indexOf, 1, data)
|
state.user.mediaProgress.splice(indexOf, 1, data)
|
||||||
} else {
|
} else {
|
||||||
@ -167,4 +170,4 @@ export const mutations = {
|
|||||||
localStorage.setItem('userSettings', JSON.stringify(settings))
|
localStorage.setItem('userSettings', JSON.stringify(settings))
|
||||||
state.settings = settings
|
state.settings = settings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,59 +1,82 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: ['components/**/*.vue', 'layouts/**/*.vue', 'pages/**/*.vue', 'templates/**/*.vue', 'plugins/**/*.js', 'nuxt.config.js'],
|
||||||
'components/**/*.vue',
|
safelist: ['bg-red-600', 'px-1.5', 'min-w-5', 'border-warning'],
|
||||||
'layouts/**/*.vue',
|
|
||||||
'pages/**/*.vue',
|
|
||||||
'templates/**/*.vue',
|
|
||||||
'plugins/**/*.js',
|
|
||||||
'nuxt.config.js'
|
|
||||||
],
|
|
||||||
safelist: [
|
|
||||||
'bg-red-600',
|
|
||||||
'px-1.5',
|
|
||||||
'min-w-5',
|
|
||||||
'border-warning'
|
|
||||||
],
|
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
height: {
|
height: {
|
||||||
'7.5': '1.75rem',
|
7.5: '1.75em',
|
||||||
'18': '4.5rem',
|
18: '4.5em',
|
||||||
'45': '11.25rem'
|
45: '11.25em'
|
||||||
},
|
},
|
||||||
width: {
|
width: {
|
||||||
'18': '4.5rem'
|
18: '4.5em'
|
||||||
},
|
},
|
||||||
maxWidth: {
|
maxWidth: {
|
||||||
'6': '1.5rem',
|
6: '1.5em',
|
||||||
'12': '3rem',
|
12: '3em',
|
||||||
'16': '4rem',
|
16: '4em',
|
||||||
'20': '5rem',
|
20: '5em',
|
||||||
'24': '6rem',
|
24: '6em',
|
||||||
'32': '8rem',
|
32: '8em',
|
||||||
'40': '10rem',
|
40: '10em',
|
||||||
'48': '12rem',
|
48: '12em',
|
||||||
'52': '13rem',
|
52: '13em',
|
||||||
'64': '16rem',
|
64: '16em',
|
||||||
'80': '20rem'
|
80: '20em'
|
||||||
},
|
},
|
||||||
minWidth: {
|
minWidth: {
|
||||||
'5': '1.25rem',
|
5: '1.25em',
|
||||||
'6': '1.5rem',
|
6: '1.5em',
|
||||||
'8': '2rem',
|
8: '2em',
|
||||||
'10': '2.5rem',
|
10: '2.5em',
|
||||||
'12': '3rem',
|
12: '3em',
|
||||||
'16': '4rem',
|
16: '4em',
|
||||||
'20': '5rem',
|
20: '5em',
|
||||||
'24': '6rem',
|
24: '6em',
|
||||||
'26': '6.5rem',
|
26: '6.5em',
|
||||||
'32': '8rem',
|
32: '8em',
|
||||||
'48': '12rem',
|
48: '12em',
|
||||||
'64': '16rem',
|
64: '16em',
|
||||||
'80': '20rem'
|
80: '20em'
|
||||||
},
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
'18': '4.5rem',
|
18: '4.5em',
|
||||||
'-54': '-13.5rem'
|
'18r': '4.5rem',
|
||||||
|
'-54': '-13.5em',
|
||||||
|
'54r': '13.5rem',
|
||||||
|
'0.5r': '0.125rem',
|
||||||
|
'1r': '0.25rem',
|
||||||
|
'1.5r': '0.375rem',
|
||||||
|
'2r': '0.5rem',
|
||||||
|
'2.5r': '0.625rem',
|
||||||
|
'3r': '0.75rem',
|
||||||
|
'3.5r': '0.875rem',
|
||||||
|
'4r': '1rem',
|
||||||
|
'5r': '1.25rem',
|
||||||
|
'6r': '1.5rem',
|
||||||
|
'7r': '1.75rem',
|
||||||
|
'8r': '2rem',
|
||||||
|
'9r': '2.25rem',
|
||||||
|
'10r': '2.5rem',
|
||||||
|
'11r': '2.75rem',
|
||||||
|
'12r': '3rem',
|
||||||
|
'14r': '3.5rem',
|
||||||
|
'16r': '4rem',
|
||||||
|
'20r': '5rem',
|
||||||
|
'24r': '6rem',
|
||||||
|
'28r': '7rem',
|
||||||
|
'32r': '8rem',
|
||||||
|
'36r': '9rem',
|
||||||
|
'40r': '10rem',
|
||||||
|
'44r': '11rem',
|
||||||
|
'48r': '12rem',
|
||||||
|
'52r': '13rem',
|
||||||
|
'56r': '14rem',
|
||||||
|
'60r': '15rem',
|
||||||
|
'64r': '16rem',
|
||||||
|
'72r': '18rem',
|
||||||
|
'80r': '20rem',
|
||||||
|
'96r': '24rem'
|
||||||
},
|
},
|
||||||
rotate: {
|
rotate: {
|
||||||
'-60': '-60deg'
|
'-60': '-60deg'
|
||||||
@ -90,13 +113,65 @@ module.exports = {
|
|||||||
'4.5xl': '2.625rem'
|
'4.5xl': '2.625rem'
|
||||||
},
|
},
|
||||||
zIndex: {
|
zIndex: {
|
||||||
'50': 50,
|
50: 50,
|
||||||
'60': 60
|
60: 60
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
spacing: {
|
||||||
|
px: '1px',
|
||||||
|
0: '0px',
|
||||||
|
0.5: '0.125em',
|
||||||
|
1: '0.25em',
|
||||||
|
1.5: '0.375em',
|
||||||
|
2: '0.5em',
|
||||||
|
2.5: '0.625em',
|
||||||
|
3: '0.75em',
|
||||||
|
3.5: '0.875em',
|
||||||
|
4: '1em',
|
||||||
|
5: '1.25em',
|
||||||
|
6: '1.5em',
|
||||||
|
7: '1.75em',
|
||||||
|
8: '2em',
|
||||||
|
9: '2.25em',
|
||||||
|
10: '2.5em',
|
||||||
|
11: '2.75em',
|
||||||
|
12: '3em',
|
||||||
|
14: '3.5em',
|
||||||
|
16: '4em',
|
||||||
|
20: '5em',
|
||||||
|
24: '6em',
|
||||||
|
28: '7em',
|
||||||
|
32: '8em',
|
||||||
|
36: '9em',
|
||||||
|
40: '10em',
|
||||||
|
44: '11em',
|
||||||
|
48: '12em',
|
||||||
|
52: '13em',
|
||||||
|
56: '14em',
|
||||||
|
60: '15em',
|
||||||
|
64: '16em',
|
||||||
|
72: '18em',
|
||||||
|
80: '20em',
|
||||||
|
96: '24em'
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
xs: '0.75em',
|
||||||
|
sm: '0.875em',
|
||||||
|
base: '1em',
|
||||||
|
lg: '1.125em',
|
||||||
|
xl: '1.25em',
|
||||||
|
'2xl': '1.5em',
|
||||||
|
'3xl': '1.875em',
|
||||||
|
'4xl': '2.25em',
|
||||||
|
'5xl': '3em',
|
||||||
|
'6xl': '3.75em',
|
||||||
|
'7xl': '4.5em',
|
||||||
|
'8xl': '6em',
|
||||||
|
'9xl': '8em'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
extend: {},
|
extend: {}
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: []
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user