mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-04-11 01:17:50 +02:00
Add: Experimental collections edit, book list, collection cover #151
This commit is contained in:
parent
28ccd4e568
commit
465e6869c0
@ -151,3 +151,18 @@ input[type=number] {
|
||||
.box-shadow-side {
|
||||
box-shadow: 5px 0px 5px #11111166;
|
||||
}
|
||||
|
||||
/*
|
||||
Bookshelf Label
|
||||
*/
|
||||
.categoryPlacard {
|
||||
background-image: url(https://image.freepik.com/free-photo/brown-wooden-textured-flooring-background_53876-128537.jpg);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.shinyBlack {
|
||||
background-color: #2d3436;
|
||||
background-image: linear-gradient(315deg, #19191a 0%, rgb(15, 15, 15) 74%);
|
||||
border-color: rgba(255, 244, 182, 0.6);
|
||||
border-style: solid;
|
||||
color: #fce3a6;
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="body-cell min-w-6 max-w-6">
|
||||
<cards-hover-book-cover :audiobook="book" />
|
||||
<covers-hover-book-cover :audiobook="book" />
|
||||
</td>
|
||||
<td class="body-cell min-w-64 max-w-64 px-2">
|
||||
<nuxt-link :to="`/audiobook/${book.id}`" class="hover:underline">
|
||||
|
@ -32,12 +32,14 @@
|
||||
<div :key="index" class="w-full bookshelfRow relative">
|
||||
<div class="flex justify-center items-center">
|
||||
<template v-for="entity in shelf">
|
||||
<cards-group-card v-if="showGroups" :key="entity.id" :width="bookCoverWidth" :group="entity" @click="clickGroup" />
|
||||
<cards-collection-card v-if="isCollections" :key="entity.id" :width="bookCoverWidth" :collection="entity" @click="clickGroup" />
|
||||
<cards-group-card v-else-if="showGroups" :key="entity.id" :width="bookCoverWidth" :group="entity" @click="clickGroup" />
|
||||
|
||||
<!-- <cards-book-3d :key="entity.id" v-else :width="100" :src="$store.getters['audiobooks/getBookCoverSrc'](entity.book)" /> -->
|
||||
<cards-book-card v-else :key="entity.id" :show-volume-number="!!selectedSeries" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @edit="editBook" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
|
||||
<div class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-10" :class="isCollections ? 'h-6' : 'h-4'" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@ -131,7 +133,9 @@ export default {
|
||||
return 16 * this.sizeMultiplier
|
||||
},
|
||||
bookWidth() {
|
||||
var _width = this.bookCoverWidth + this.paddingX * 2
|
||||
var coverWidth = this.bookCoverWidth
|
||||
if (this.page === 'collections') coverWidth *= 2
|
||||
var _width = coverWidth + this.paddingX * 2
|
||||
return this.showGroups ? _width * 1.6 : _width
|
||||
},
|
||||
isSelectionMode() {
|
||||
@ -149,6 +153,9 @@ export default {
|
||||
showGroups() {
|
||||
return this.page !== '' && this.page !== 'search' && !this.selectedSeries
|
||||
},
|
||||
isCollections() {
|
||||
return this.page === 'collections'
|
||||
},
|
||||
categorizedShelves() {
|
||||
if (this.page !== 'search') return []
|
||||
var audiobookSearchResults = this.searchResults ? this.searchResults.audiobooks || [] : []
|
||||
@ -198,12 +205,7 @@ export default {
|
||||
var audiobookSearchResults = this.searchResults ? this.searchResults.audiobooks || [] : []
|
||||
return audiobookSearchResults.map((absr) => absr.audiobook)
|
||||
} else if (this.page === 'collections') {
|
||||
return (this.$store.state.user.collections || []).map((c) => {
|
||||
return {
|
||||
type: 'collection',
|
||||
...c
|
||||
}
|
||||
})
|
||||
return this.$store.state.user.collections || []
|
||||
} else {
|
||||
var seriesGroups = this.$store.getters['audiobooks/getSeriesGroups']()
|
||||
if (this.selectedSeries) {
|
||||
@ -299,6 +301,11 @@ export default {
|
||||
console.log('[AudioBookshelf] Audiobooks Updated')
|
||||
this.setBookshelfEntities()
|
||||
},
|
||||
collectionsUpdated() {
|
||||
if (!this.isCollections) return
|
||||
console.log('[AudioBookshelf] Collections Updated')
|
||||
this.setBookshelfEntities()
|
||||
},
|
||||
buildSearchParams() {
|
||||
if (this.page === 'search' || this.page === 'series' || this.page === 'collections') {
|
||||
return ''
|
||||
@ -357,6 +364,7 @@ export default {
|
||||
window.addEventListener('resize', this.resize)
|
||||
this.$store.commit('audiobooks/addListener', { id: 'bookshelf', meth: this.audiobooksUpdated })
|
||||
this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
|
||||
this.$store.commit('user/addCollectionsListener', { id: 'bookshelf', meth: this.collectionsUpdated })
|
||||
|
||||
this.init()
|
||||
},
|
||||
@ -364,6 +372,7 @@ export default {
|
||||
window.removeEventListener('resize', this.resize)
|
||||
this.$store.commit('audiobooks/removeListener', 'bookshelf')
|
||||
this.$store.commit('user/removeSettingsListener', 'bookshelf')
|
||||
this.$store.commit('user/removeCollectionsListener', 'bookshelf')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -139,17 +139,7 @@ export default {
|
||||
/* background: linear-gradient(180deg, rgb(114, 85, 59) 0%, rgb(73, 48, 22) 17%, rgb(71, 43, 15) 88%, rgb(61, 41, 20) 100%); */
|
||||
box-shadow: 2px 14px 8px #111111aa;
|
||||
}
|
||||
.categoryPlacard {
|
||||
background-image: url(https://image.freepik.com/free-photo/brown-wooden-textured-flooring-background_53876-128537.jpg);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.shinyBlack {
|
||||
background-color: #2d3436;
|
||||
background-image: linear-gradient(315deg, #19191a 0%, rgb(15, 15, 15) 74%);
|
||||
border-color: rgba(255, 244, 182, 0.6);
|
||||
border-style: solid;
|
||||
color: #fce3a6;
|
||||
}
|
||||
|
||||
.book-shelf-arrow-right {
|
||||
height: calc(100% - 24px);
|
||||
background: rgb(48, 48, 48);
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-if="streamAudiobook" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-40 bg-primary px-4 pb-4 pt-2">
|
||||
<nuxt-link :to="`/audiobook/${streamAudiobook.id}`" class="absolute -top-16 left-4 cursor-pointer">
|
||||
<cards-book-cover :audiobook="streamAudiobook" :width="bookCoverWidth" />
|
||||
<covers-book-cover :audiobook="streamAudiobook" :width="bookCoverWidth" />
|
||||
</nuxt-link>
|
||||
<div class="flex items-start pl-24 mb-6 md:mb-0">
|
||||
<div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex h-full px-1 overflow-hidden">
|
||||
<cards-book-cover :audiobook="audiobook" :width="50" />
|
||||
<covers-book-cover :audiobook="audiobook" :width="50" />
|
||||
<div class="flex-grow px-2 audiobookSearchCardContent">
|
||||
<p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p>
|
||||
<p v-else class="truncate text-sm" v-html="matchHtml" />
|
||||
|
@ -11,7 +11,7 @@
|
||||
<div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `${paddingY}px ${paddingX}px` }" @click.stop>
|
||||
<nuxt-link :to="isSelectionMode ? '' : `/audiobook/${audiobookId}`" class="cursor-pointer">
|
||||
<div class="w-full relative box-shadow-book" :style="{ height: height + 'px' }" @click="clickCard" @mouseover="isHovering = true" @mouseleave="isHovering = false">
|
||||
<cards-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" />
|
||||
<covers-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" />
|
||||
|
||||
<!-- Hidden SM and DOWN -->
|
||||
<div v-show="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black rounded hidden md:block" :class="overlayWrapperClasslist">
|
||||
|
112
client/components/cards/CollectionCard.vue
Normal file
112
client/components/cards/CollectionCard.vue
Normal file
@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div class="rounded-sm h-full relative" :style="{ padding: `${paddingY}px ${paddingX}px` }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard">
|
||||
<nuxt-link :to="groupTo" class="cursor-pointer">
|
||||
<div class="w-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'" :style="{ height: coverHeight + 'px', width: coverWidth + 'px' }">
|
||||
<covers-collection-cover ref="groupcover" :book-items="bookItems" :width="coverWidth" :height="coverHeight" />
|
||||
|
||||
<div v-show="isHovering" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
|
||||
<!-- <div class="absolute pointer-events-auto" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem' }" @click.stop.prevent="toggleSelected">
|
||||
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">radio_button_unchecked</span>
|
||||
</div> -->
|
||||
<div class="absolute pointer-events-auto" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickEdit">
|
||||
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
|
||||
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto bottom-0 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, coverWidth) + 'px' }">
|
||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${1 * sizeMultiplier}rem` }">
|
||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ collectionName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
collection: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 120
|
||||
},
|
||||
paddingY: {
|
||||
type: Number,
|
||||
default: 24
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isHovering: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
width(newVal) {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.groupcover && this.$refs.groupcover.init) {
|
||||
this.$refs.groupcover.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labelFontSize() {
|
||||
if (this.coverWidth < 160) return 0.75
|
||||
return 0.875
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
_collection() {
|
||||
return this.collection || {}
|
||||
},
|
||||
groupTo() {
|
||||
return `/collection/${this._collection.id}`
|
||||
},
|
||||
coverWidth() {
|
||||
return this.width * 2
|
||||
},
|
||||
coverHeight() {
|
||||
return this.width * 1.6
|
||||
},
|
||||
sizeMultiplier() {
|
||||
return this.width / 120
|
||||
},
|
||||
paddingX() {
|
||||
return 16 * this.sizeMultiplier
|
||||
},
|
||||
bookItems() {
|
||||
return this._collection.books || []
|
||||
},
|
||||
collectionName() {
|
||||
return this._collection.name || 'No Name'
|
||||
},
|
||||
showExperimentalFeatures() {
|
||||
return this.$store.state.showExperimentalFeatures
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleSelected() {
|
||||
// Selected
|
||||
},
|
||||
clickEdit() {
|
||||
this.$store.commit('globals/setEditCollection', this.collection)
|
||||
},
|
||||
mouseoverCard() {
|
||||
this.isHovering = true
|
||||
},
|
||||
mouseleaveCard() {
|
||||
this.isHovering = false
|
||||
},
|
||||
clickCard() {
|
||||
this.$emit('click', this.collection)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -2,10 +2,10 @@
|
||||
<div class="relative">
|
||||
<div class="rounded-sm h-full relative" :style="{ padding: `16px ${paddingX}px` }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard">
|
||||
<nuxt-link :to="groupTo" class="cursor-pointer">
|
||||
<div class="w-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'" :style="{ height: height + 'px', width: height + 'px' }">
|
||||
<cards-group-cover ref="groupcover" :name="groupName" :group-to="groupTo" :type="groupType" :book-items="bookItems" :width="height" :height="height" />
|
||||
<div class="w-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'" :style="{ height: coverHeight + 'px', width: coverWidth + 'px' }">
|
||||
<covers-group-cover ref="groupcover" :name="groupName" :group-to="groupTo" :type="groupType" :book-items="bookItems" :width="coverWidth" :height="coverHeight" />
|
||||
|
||||
<div v-if="hasValidCovers && !showExperimentalFeatures && groupType !== 'collection'" 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 && !showExperimentalFeatures" 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 class="font-book" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ groupName }}</p>
|
||||
</div>
|
||||
|
||||
@ -66,7 +66,10 @@ export default {
|
||||
return `/library/${this.currentLibraryId}/bookshelf?filter=tags.${this.groupEncode}`
|
||||
}
|
||||
},
|
||||
height() {
|
||||
coverWidth() {
|
||||
return this.coverHeight
|
||||
},
|
||||
coverHeight() {
|
||||
return this.width * 1.6
|
||||
},
|
||||
sizeMultiplier() {
|
||||
@ -90,9 +93,6 @@ export default {
|
||||
groupName() {
|
||||
return this._group.name || 'No Name'
|
||||
},
|
||||
groupType() {
|
||||
return this._group.type
|
||||
},
|
||||
groupEncode() {
|
||||
return this.$encode(this.groupName)
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex h-full px-1 overflow-hidden">
|
||||
<cards-group-cover :name="series" :book-items="bookItems" :width="60" :height="60" />
|
||||
<covers-group-cover :name="series" :book-items="bookItems" :width="60" :height="60" />
|
||||
<div class="flex-grow px-2 seriesSearchCardContent h-full">
|
||||
<p class="truncate text-sm">{{ series }}</p>
|
||||
</div>
|
||||
|
63
client/components/covers/CollectionCover.vue
Normal file
63
client/components/covers/CollectionCover.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="relative rounded-sm overflow-hidden" :style="{ width: width + 'px', height: height + 'px' }">
|
||||
<!-- <div class="absolute top-0 left-0 w-full h-full rounded-sm overflow-hidden z-10">
|
||||
<div class="w-full h-full border border-white border-opacity-10" />
|
||||
</div> -->
|
||||
|
||||
<div v-if="hasOwnCover" class="w-full h-full relative rounded-sm">
|
||||
<div v-if="showCoverBg" class="bg-primary absolute top-0 left-0 w-full h-full">
|
||||
<div class="w-full h-full z-0" ref="coverBg" />
|
||||
</div>
|
||||
<img ref="cover" :src="fullCoverUrl" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0" :class="showCoverBg ? 'object-contain' : 'object-cover'" />
|
||||
</div>
|
||||
<div v-else-if="books.length" class="flex justify-center h-full relative bg-primary bg-opacity-95 rounded-sm">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||
|
||||
<covers-book-cover :audiobook="books[0]" :width="width / 2" />
|
||||
<covers-book-cover v-if="books.length > 1" :audiobook="books[1]" :width="width / 2" />
|
||||
</div>
|
||||
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||
|
||||
<p class="font-book text-white text-opacity-60 text-center" :style="{ fontSize: Math.min(1, sizeMultiplier) + 'rem' }">Empty Collection</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
bookItems: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
width: Number,
|
||||
height: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
imageFailed: false,
|
||||
showCoverBg: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sizeMultiplier() {
|
||||
return this.width / 120
|
||||
},
|
||||
hasOwnCover() {
|
||||
return false
|
||||
},
|
||||
fullCoverUrl() {
|
||||
return null
|
||||
},
|
||||
books() {
|
||||
return this.bookItems || []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
imageError() {},
|
||||
imageLoaded() {}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div ref="container" @mouseover="mouseover" @mouseleave="mouseleave" class="relative">
|
||||
<cards-book-cover :width="24" :audiobook="audiobook" />
|
||||
<covers-book-cover :width="24" :audiobook="audiobook" />
|
||||
</div>
|
||||
</template>
|
||||
|
120
client/components/modals/EditCollectionModal.vue
Normal file
120
client/components/modals/EditCollectionModal.vue
Normal file
@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<modals-modal v-model="show" name="edit-collection" :width="700" :height="'unset'" :processing="processing">
|
||||
<template #outer>
|
||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||
<p class="font-book text-3xl text-white truncate">Collection</p>
|
||||
</div>
|
||||
</template>
|
||||
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
||||
<form @submit.prevent="submitForm">
|
||||
<div class="flex">
|
||||
<covers-collection-cover :book-items="books" :width="200" :height="100 * 1.6" />
|
||||
<div class="flex-grow px-4">
|
||||
<ui-text-input-with-label v-model="newCollectionName" label="Name" class="mb-2" />
|
||||
|
||||
<ui-textarea-with-label v-model="newCollectionDescription" label="Description" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 right-0 w-full py-2 px-4 flex">
|
||||
<ui-btn small color="error" type="button" @click.stop="removeClick">Remove</ui-btn>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" type="submit">Save</ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
newCollectionName: null,
|
||||
newCollectionDescription: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show: {
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.$store.state.globals.showEditCollectionModal
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit('globals/setShowEditCollectionModal', val)
|
||||
}
|
||||
},
|
||||
collection() {
|
||||
return this.$store.state.globals.selectedCollection || {}
|
||||
},
|
||||
collectionName() {
|
||||
return this.collection.name
|
||||
},
|
||||
books() {
|
||||
return this.collection.books || []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.newCollectionName = this.collectionName
|
||||
this.newCollectionDescription = this.collection.description || ''
|
||||
},
|
||||
removeClick() {
|
||||
if (confirm(`Are you sure you want to remove collection "${this.collectionName}"?`)) {
|
||||
this.processing = true
|
||||
var collectionName = this.collectionName
|
||||
this.$axios
|
||||
.$delete(`/api/collection/${this.collection.id}`)
|
||||
.then(() => {
|
||||
this.processing = false
|
||||
this.show = false
|
||||
this.$toast.success(`Collection "${collectionName}" Removed`)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove collection', error)
|
||||
this.processing = false
|
||||
this.$toast.error(`Failed to remove collection`)
|
||||
})
|
||||
}
|
||||
},
|
||||
submitForm() {
|
||||
if (this.newCollectionName === this.collectionName && this.newCollectionDescription === this.collection.description) {
|
||||
return
|
||||
}
|
||||
if (!this.newCollectionName) {
|
||||
return this.$toast.error('Collection must have a name')
|
||||
}
|
||||
|
||||
this.processing = true
|
||||
|
||||
var collectionUpdate = {
|
||||
name: this.newCollectionName,
|
||||
description: this.newCollectionDescription || null
|
||||
}
|
||||
this.$axios
|
||||
.$patch(`/api/collection/${this.collection.id}`, collectionUpdate)
|
||||
.then((collection) => {
|
||||
console.log('Collection Updated', collection)
|
||||
this.processing = false
|
||||
this.show = false
|
||||
this.$toast.success(`Collection "${collection.name}" Updated`)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update collection', error)
|
||||
this.processing = false
|
||||
this.$toast.error(`Failed to update collection`)
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {},
|
||||
beforeDestroy() {}
|
||||
}
|
||||
</script>
|
@ -8,7 +8,7 @@
|
||||
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
|
||||
<transition-group name="list-complete" tag="div">
|
||||
<template v-for="collection in sortedCollections">
|
||||
<modals-collections-user-collection-item :key="collection.id" :collection="collection" class="list-complete-item" @add="addToCollection" @remove="removeFromCollection" />
|
||||
<modals-collections-user-collection-item :key="collection.id" :collection="collection" class="list-complete-item" @add="addToCollection" @remove="removeFromCollection" @close="show = false" />
|
||||
</template>
|
||||
</transition-group>
|
||||
</div>
|
||||
|
@ -2,9 +2,9 @@
|
||||
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-bg" :class="wrapperClass" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<div v-if="isBookIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
|
||||
<!-- <span class="material-icons" :class="highlight ? 'text-success' : 'text-white text-opacity-80'">{{ highlight ? 'bookmark' : 'bookmark_border' }}</span> -->
|
||||
<div class="w-16 max-w-16 text-center">
|
||||
<div class="w-20 max-w-20 text-center">
|
||||
<!-- <img src="/Logo.png" /> -->
|
||||
<cards-group-cover :name="collection.name" :book-items="books" :width="64" :height="64" type="collection" />
|
||||
<covers-collection-cover :book-items="books" :width="80" :height="40 * 1.6" />
|
||||
</div>
|
||||
<div class="flex-grow overflow-hidden px-2">
|
||||
<!-- <template v-if="isEditing">
|
||||
@ -20,7 +20,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</template> -->
|
||||
<p class="pl-2 pr-2 truncate">{{ collection.name }}</p>
|
||||
<nuxt-link :to="`/collection/${collection.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ collection.name }}</nuxt-link>
|
||||
</div>
|
||||
<div v-if="!isEditing" class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
||||
<ui-btn v-if="!isBookIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-icons pt-px">add</span></ui-btn>
|
||||
@ -61,6 +61,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickNuxtLink() {
|
||||
this.$emit('close')
|
||||
},
|
||||
mouseover() {
|
||||
if (this.isEditing) return
|
||||
this.isHovering = true
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6 relative">
|
||||
<div class="flex">
|
||||
<div class="relative">
|
||||
<cards-book-cover :audiobook="audiobook" />
|
||||
<covers-book-cover :audiobook="audiobook" />
|
||||
<!-- book cover overlay -->
|
||||
<div v-if="book.cover" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
|
||||
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
|
||||
@ -57,7 +57,7 @@
|
||||
<p v-if="!coversFound.length">No Covers Found</p>
|
||||
<template v-for="cover in coversFound">
|
||||
<div :key="cover" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="updateCover(cover)">
|
||||
<cards-preview-cover :src="cover" :width="80" show-open-new-tab />
|
||||
<covers-preview-cover :src="cover" :width="80" show-open-new-tab />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@ -68,7 +68,7 @@
|
||||
<p class="text-lg">Preview Cover</p>
|
||||
<span class="absolute top-4 right-4 material-icons text-2xl cursor-pointer" @click="resetCoverPreview">close</span>
|
||||
<div class="flex justify-center py-4">
|
||||
<cards-preview-cover :src="previewUpload" :width="240" />
|
||||
<covers-preview-cover :src="previewUpload" :width="240" />
|
||||
</div>
|
||||
<div class="absolute bottom-0 right-0 flex py-4 px-5">
|
||||
<ui-btn :disabled="processingUpload" class="mx-2" @click="resetCoverPreview">Clear</ui-btn>
|
||||
|
43
client/components/tables/CollectionBooksTable.vue
Normal file
43
client/components/tables/CollectionBooksTable.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="w-full bg-primary bg-opacity-40">
|
||||
<div class="w-full h-14 flex items-center px-4 bg-primary">
|
||||
<p>Collection List</p>
|
||||
<div class="w-6 h-6 bg-white bg-opacity-10 flex items-center justify-center rounded-full ml-2">
|
||||
<p class="font-mono text-sm">{{ books.length }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<p v-if="totalDuration">{{ totalDurationPretty }}</p>
|
||||
</div>
|
||||
<template v-for="book in books">
|
||||
<tables-collection-book-table-row :key="book.id" :book="book" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
books: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
totalDuration() {
|
||||
var _total = 0
|
||||
this.books.forEach((book) => {
|
||||
_total += book.duration
|
||||
})
|
||||
return _total
|
||||
},
|
||||
totalDurationPretty() {
|
||||
return this.$elapsedPretty(this.totalDuration)
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
58
client/components/tables/collection/BookTableRow.vue
Normal file
58
client/components/tables/collection/BookTableRow.vue
Normal file
@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="w-full px-6 py-2" @mouseover="mouseover" @mouseleave="mouseleave" :class="isHovering ? 'bg-white bg-opacity-5' : ''">
|
||||
<div v-if="book" class="flex h-20">
|
||||
<covers-book-cover :audiobook="book" :width="50" />
|
||||
<div class="w-80 h-full px-2 flex items-center">
|
||||
<div>
|
||||
<p class="truncate">{{ bookTitle }}</p>
|
||||
<p class="truncate text-gray-400 text-sm">{{ bookAuthor }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow flex items-center">
|
||||
<p>{{ bookDuration }}</p>
|
||||
</div>
|
||||
<!-- <div class="w-12 flex items-center justify-center">
|
||||
<span class="material-icons text-lg text-white text-opacity-70 hover:text-opacity-100 cursor-pointer">radio_button_unchecked</span>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
book: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isHovering: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
_book() {
|
||||
return this.book.book || {}
|
||||
},
|
||||
bookTitle() {
|
||||
return this._book.title || ''
|
||||
},
|
||||
bookAuthor() {
|
||||
return this._book.authorFL || ''
|
||||
},
|
||||
bookDuration() {
|
||||
return this.$secondsToTimestamp(this.book.duration)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
mouseover() {
|
||||
this.isHovering = true
|
||||
},
|
||||
mouseleave() {
|
||||
this.isHovering = false
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
@ -8,6 +8,7 @@
|
||||
|
||||
<modals-edit-modal />
|
||||
<modals-user-collections-modal />
|
||||
<modals-edit-collection-modal />
|
||||
<readers-reader />
|
||||
</div>
|
||||
</template>
|
||||
@ -189,6 +190,11 @@ export default {
|
||||
this.$store.commit('user/addUpdateCollection', collection)
|
||||
},
|
||||
collectionRemoved(collection) {
|
||||
if (this.$route.name.startsWith('collection')) {
|
||||
if (this.$route.params.id === collection.id) {
|
||||
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}/bookshelf/collections`)
|
||||
}
|
||||
}
|
||||
this.$store.commit('user/removeCollection', collection)
|
||||
},
|
||||
downloadToastClick(download) {
|
||||
|
@ -12,7 +12,7 @@ export default function (context) {
|
||||
}
|
||||
|
||||
if (route.name.startsWith('config') || route.name === 'upload' || route.name === 'account' || route.name.startsWith('audiobook-id') || route.name.startsWith('collection-id')) {
|
||||
if (from.name !== route.name && from.name !== 'audiobook-id-edit' && !from.name.startsWith('config') && from.name !== 'upload' && from.name !== 'account') {
|
||||
if (from.name !== route.name && from.name !== 'audiobook-id-edit' && from.name !== 'collection-id' && !from.name.startsWith('config') && from.name !== 'upload' && from.name !== 'account') {
|
||||
var _history = [...store.state.routeHistory]
|
||||
if (!_history.length || _history[_history.length - 1] !== from.fullPath) {
|
||||
_history.push(from.fullPath)
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="flex flex-col sm:flex-row max-w-6xl mx-auto">
|
||||
<div class="w-full flex justify-center md:block sm:w-32 md:w-52" style="min-width: 208px">
|
||||
<div class="relative" style="height: fit-content">
|
||||
<cards-book-cover :audiobook="audiobook" :width="bookCoverWidth" />
|
||||
<covers-book-cover :audiobook="audiobook" :width="bookCoverWidth" />
|
||||
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<template v-for="audiobook in audiobookCopies">
|
||||
<div :key="audiobook.id" class="w-full max-w-3xl border border-black-300 p-6 -ml-px -mt-px flex">
|
||||
<div class="w-32">
|
||||
<cards-book-cover :audiobook="audiobook.originalAudiobook" :width="120" />
|
||||
<covers-book-cover :audiobook="audiobook.originalAudiobook" :width="120" />
|
||||
</div>
|
||||
<div class="flex-grow pl-4">
|
||||
<ui-text-input-with-label v-model="audiobook.book.title" label="Title" />
|
||||
|
@ -2,24 +2,34 @@
|
||||
<div id="page-wrapper" class="bg-bg page overflow-hidden" :class="streamAudiobook ? 'streaming' : ''">
|
||||
<div class="w-full h-full overflow-y-auto px-2 py-6 md:p-8">
|
||||
<div class="flex flex-col sm:flex-row max-w-6xl mx-auto">
|
||||
<div class="w-full flex justify-center md:block sm:w-32 md:w-52" style="min-width: 208px">
|
||||
<div class="w-full flex justify-center md:block sm:w-32 md:w-52" style="min-width: 240px">
|
||||
<div class="relative" style="height: fit-content">
|
||||
<cards-group-cover :name="collectionName" type="collection" :book-items="bookItems" :width="176" :height="176" />
|
||||
<covers-collection-cover :book-items="bookItems" :width="240" :height="120 * 1.6" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow px-2 py-6 md:py-0 md:px-10">
|
||||
<div class="flex">
|
||||
<div class="mb-4">
|
||||
<div class="flex sm:items-end flex-col sm:flex-row">
|
||||
<h1 class="text-2xl md:text-3xl font-sans">
|
||||
{{ collectionName }}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex sm:items-end flex-col sm:flex-row">
|
||||
<h1 class="text-2xl md:text-3xl font-sans">
|
||||
{{ collectionName }}
|
||||
</h1>
|
||||
<div class="flex-grow" />
|
||||
|
||||
<ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" />
|
||||
|
||||
<ui-icon-btn icon="delete" class="mx-0.5" @click="removeClick" />
|
||||
</div>
|
||||
|
||||
<div class="my-8 max-w-2xl">
|
||||
<p class="text-base text-gray-100">{{ description }}</p>
|
||||
</div>
|
||||
|
||||
<tables-collection-books-table :books="bookItems" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="processingRemove" class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-40 flex items-center justify-center">
|
||||
<ui-loading-indicator />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -42,7 +52,9 @@ export default {
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
return {
|
||||
processingRemove: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
streamAudiobook() {
|
||||
@ -53,9 +65,33 @@ export default {
|
||||
},
|
||||
collectionName() {
|
||||
return this.collection.name || ''
|
||||
},
|
||||
description() {
|
||||
return this.collection.description || ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
editClick() {
|
||||
this.$store.commit('globals/setEditCollection', this.collection)
|
||||
},
|
||||
removeClick() {
|
||||
if (confirm(`Are you sure you want to remove collection "${this.collectionName}"?`)) {
|
||||
this.processingRemove = true
|
||||
var collectionName = this.collectionName
|
||||
this.$axios
|
||||
.$delete(`/api/collection/${this.collection.id}`)
|
||||
.then(() => {
|
||||
this.processingRemove = false
|
||||
this.$toast.success(`Collection "${collectionName}" Removed`)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove collection', error)
|
||||
this.processingRemove = false
|
||||
this.$toast.error(`Failed to remove collection`)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
@ -26,7 +26,7 @@
|
||||
</tr>
|
||||
<tr v-for="ab in userAudiobooks" :key="ab.audiobookId" :class="!ab.isRead ? '' : 'isRead'">
|
||||
<td>
|
||||
<cards-book-cover :width="50" :audiobook="ab" />
|
||||
<covers-book-cover :width="50" :audiobook="ab" />
|
||||
</td>
|
||||
<td class="font-book">
|
||||
<p>{{ ab.book ? ab.book.title : ab.audiobookTitle || 'Unknown' }}</p>
|
||||
|
@ -1,6 +1,8 @@
|
||||
|
||||
export const state = () => ({
|
||||
showUserCollectionsModal: false
|
||||
showUserCollectionsModal: false,
|
||||
showEditCollectionModal: false,
|
||||
selectedCollection: null
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
@ -14,5 +16,12 @@ export const actions = {
|
||||
export const mutations = {
|
||||
setShowUserCollectionsModal(state, val) {
|
||||
state.showUserCollectionsModal = val
|
||||
},
|
||||
setShowEditCollectionModal(state, val) {
|
||||
state.showEditCollectionModal = val
|
||||
},
|
||||
setEditCollection(state, collection) {
|
||||
state.selectedCollection = collection
|
||||
state.showEditCollectionModal = true
|
||||
}
|
||||
}
|
@ -12,7 +12,8 @@ export const state = () => ({
|
||||
},
|
||||
settingsListeners: [],
|
||||
collections: [],
|
||||
collectionsLoaded: false
|
||||
collectionsLoaded: false,
|
||||
collectionsListeners: []
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
@ -140,8 +141,18 @@ export const mutations = {
|
||||
} else {
|
||||
state.collections.push(collection)
|
||||
}
|
||||
state.collectionsListeners.forEach((listener) => listener.meth())
|
||||
},
|
||||
removeCollection(state, collection) {
|
||||
state.collections = state.collections.filter(c => c.id !== collection.id)
|
||||
}
|
||||
state.collectionsListeners.forEach((listener) => listener.meth())
|
||||
},
|
||||
addCollectionsListener(state, listener) {
|
||||
var index = state.collectionsListeners.findIndex(l => l.id === listener.id)
|
||||
if (index >= 0) state.collectionsListeners.splice(index, 1, listener)
|
||||
else state.collectionsListeners.push(listener)
|
||||
},
|
||||
removeCollectionsListener(state, listenerId) {
|
||||
state.collectionsListeners = state.collectionsListeners.filter(l => l.id !== listenerId)
|
||||
},
|
||||
}
|
@ -21,6 +21,7 @@ module.exports = {
|
||||
'6': '1.5rem',
|
||||
'12': '3rem',
|
||||
'16': '4rem',
|
||||
'20': '5rem',
|
||||
'24': '6rem',
|
||||
'32': '8rem',
|
||||
'48': '12rem',
|
||||
|
@ -40,7 +40,7 @@ class UserCollection {
|
||||
var json = this.toJSON()
|
||||
json.books = json.books.map(bookId => {
|
||||
var _ab = audiobooks.find(ab => ab.id === bookId)
|
||||
return _ab ? _ab.toJSON() : null
|
||||
return _ab ? _ab.toJSONExpanded() : null
|
||||
}).filter(b => !!b)
|
||||
return json
|
||||
}
|
||||
@ -84,5 +84,24 @@ class UserCollection {
|
||||
this.books = this.books.filter(bid => bid !== bookId)
|
||||
this.lastUpdate = Date.now()
|
||||
}
|
||||
|
||||
update(payload) {
|
||||
let hasUpdates = false
|
||||
for (const key in payload) {
|
||||
if (key === 'books') {
|
||||
if (payload.books && this.books.join(',') !== payload.books.join(',')) {
|
||||
this.books = [...payload.books]
|
||||
hasUpdates = true
|
||||
}
|
||||
} else if (this[key] !== undefined && this[key] !== payload[key]) {
|
||||
hasUpdates = true
|
||||
this[key] = payload[key]
|
||||
}
|
||||
}
|
||||
if (hasUpdates) {
|
||||
this.lastUpdate = Date.now()
|
||||
}
|
||||
return hasUpdates
|
||||
}
|
||||
}
|
||||
module.exports = UserCollection
|
Loading…
Reference in New Issue
Block a user