mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-02-10 15:29:57 +01:00
Side rail, book group cards, fix dropdown select
This commit is contained in:
parent
31e109d0f0
commit
37c38e69df
@ -103,6 +103,10 @@
|
|||||||
box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166;
|
box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.box-shadow-book3d {
|
||||||
|
box-shadow: 4px 1px 8px #11111166, 1px -4px 8px #11111166;
|
||||||
|
}
|
||||||
|
|
||||||
.box-shadow-side {
|
.box-shadow-side {
|
||||||
box-shadow: 4px 0px 4px #11111166;
|
box-shadow: 4px 0px 4px #11111166;
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
showBack() {
|
showBack() {
|
||||||
return this.$route.name !== 'index'
|
return this.$route.name !== 'library-id'
|
||||||
},
|
},
|
||||||
user() {
|
user() {
|
||||||
return this.$store.state.user.user
|
return this.$store.state.user.user
|
||||||
@ -114,7 +114,7 @@ export default {
|
|||||||
if (this.$route.name === 'audiobook-id-edit') {
|
if (this.$route.name === 'audiobook-id-edit') {
|
||||||
this.$router.push(`/audiobook/${this.$route.params.id}`)
|
this.$router.push(`/audiobook/${this.$route.params.id}`)
|
||||||
} else {
|
} else {
|
||||||
this.$router.push('/')
|
this.$router.push('/library')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cancelSelectionMode() {
|
cancelSelectionMode() {
|
||||||
|
@ -16,21 +16,22 @@
|
|||||||
<ui-btn color="success" class="w-52" @click="scan">Scan Audiobooks</ui-btn>
|
<ui-btn color="success" class="w-52" @click="scan">Scan Audiobooks</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full flex flex-col items-center">
|
<div v-else id="bookshelf" class="w-full flex flex-col items-center">
|
||||||
<template v-for="(shelf, index) in entities">
|
<template v-for="(shelf, index) in shelves">
|
||||||
<div :key="index" class="w-full bookshelfRow relative">
|
<div :key="index" class="w-full bookshelfRow relative">
|
||||||
<div class="flex justify-center items-center">
|
<div class="flex justify-center items-center">
|
||||||
<template v-for="entity in shelf">
|
<template v-for="entity in shelf">
|
||||||
<cards-group-card v-if="page !== ''" :key="entity.id" :width="bookCoverWidth" :group="entity" />
|
<cards-group-card v-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" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" />
|
<cards-book-card v-else :key="entity.id" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
|
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-show="!entities.length" class="w-full py-16 text-center text-xl">
|
<div v-show="!shelves.length" class="w-full py-16 text-center text-xl">
|
||||||
<div class="py-4">No Audiobooks</div>
|
<div class="py-4">No {{ showGroups ? 'Series' : 'Audiobooks' }}</div>
|
||||||
<ui-btn v-if="filterBy !== 'all' || keywordFilter" @click="clearFilter">Clear Filter</ui-btn>
|
<ui-btn v-if="!showGroups && (filterBy !== 'all' || keywordFilter)" @click="clearFilter">Clear Filter</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -39,13 +40,12 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
page: String
|
page: String,
|
||||||
|
selectedSeries: String
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
width: 0,
|
shelves: [],
|
||||||
booksPerRow: 0,
|
|
||||||
entities: [],
|
|
||||||
currFilterOrderKey: null,
|
currFilterOrderKey: null,
|
||||||
availableSizes: [60, 80, 100, 120, 140, 160, 180, 200, 220],
|
availableSizes: [60, 80, 100, 120, 140, 160, 180, 200, 220],
|
||||||
selectedSizeIndex: 3,
|
selectedSizeIndex: 3,
|
||||||
@ -57,6 +57,11 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
keywordFilter() {
|
keywordFilter() {
|
||||||
this.checkKeywordFilter()
|
this.checkKeywordFilter()
|
||||||
|
},
|
||||||
|
selectedSeries() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.setBookshelfEntities()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -89,9 +94,30 @@ export default {
|
|||||||
},
|
},
|
||||||
filterBy() {
|
filterBy() {
|
||||||
return this.$store.getters['user/getUserSetting']('filterBy')
|
return this.$store.getters['user/getUserSetting']('filterBy')
|
||||||
|
},
|
||||||
|
showGroups() {
|
||||||
|
return this.page !== '' && !this.selectedSeries
|
||||||
|
},
|
||||||
|
entities() {
|
||||||
|
if (this.page === '') {
|
||||||
|
return this.$store.getters['audiobooks/getFilteredAndSorted']()
|
||||||
|
} else {
|
||||||
|
var seriesGroups = this.$store.getters['audiobooks/getSeriesGroups']()
|
||||||
|
if (this.selectedSeries) {
|
||||||
|
var group = seriesGroups.find((group) => group.name === this.selectedSeries)
|
||||||
|
return group.books
|
||||||
|
}
|
||||||
|
return seriesGroups
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
clickGroup(group) {
|
||||||
|
this.$emit('update:selectedSeries', group.name)
|
||||||
|
},
|
||||||
|
changeRotation() {
|
||||||
|
this.rotation = 'show-right'
|
||||||
|
},
|
||||||
clearFilter() {
|
clearFilter() {
|
||||||
this.$store.commit('audiobooks/setKeywordFilter', null)
|
this.$store.commit('audiobooks/setKeywordFilter', null)
|
||||||
if (this.filterBy !== 'all') {
|
if (this.filterBy !== 'all') {
|
||||||
@ -119,22 +145,16 @@ export default {
|
|||||||
this.$store.dispatch('user/updateUserSettings', { bookshelfCoverSize: this.bookCoverWidth })
|
this.$store.dispatch('user/updateUserSettings', { bookshelfCoverSize: this.bookCoverWidth })
|
||||||
},
|
},
|
||||||
setBookshelfEntities() {
|
setBookshelfEntities() {
|
||||||
if (this.page === '') {
|
var width = Math.max(0, this.$refs.wrapper.clientWidth - this.rowPaddingX * 2)
|
||||||
var audiobooksSorted = this.$store.getters['audiobooks/getFilteredAndSorted']()
|
var booksPerRow = Math.floor(width / this.bookWidth)
|
||||||
this.currFilterOrderKey = this.filterOrderKey
|
|
||||||
this.setGroupedBooks(audiobooksSorted)
|
var entities = this.entities
|
||||||
} else {
|
|
||||||
var entities = this.$store.getters['audiobooks/getSeriesGroups']()
|
|
||||||
this.setGroupedBooks(entities)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setGroupedBooks(entities) {
|
|
||||||
var groups = []
|
var groups = []
|
||||||
var currentRow = 0
|
var currentRow = 0
|
||||||
var currentGroup = []
|
var currentGroup = []
|
||||||
|
|
||||||
for (let i = 0; i < entities.length; i++) {
|
for (let i = 0; i < entities.length; i++) {
|
||||||
var row = Math.floor(i / this.booksPerRow)
|
var row = Math.floor(i / booksPerRow)
|
||||||
if (row > currentRow) {
|
if (row > currentRow) {
|
||||||
groups.push([...currentGroup])
|
groups.push([...currentGroup])
|
||||||
currentRow = row
|
currentRow = row
|
||||||
@ -145,23 +165,20 @@ export default {
|
|||||||
if (currentGroup.length) {
|
if (currentGroup.length) {
|
||||||
groups.push([...currentGroup])
|
groups.push([...currentGroup])
|
||||||
}
|
}
|
||||||
this.entities = groups
|
this.shelves = groups
|
||||||
},
|
},
|
||||||
calculateBookshelf() {
|
async init() {
|
||||||
this.width = this.$refs.wrapper.clientWidth
|
|
||||||
this.width = Math.max(0, this.width - this.rowPaddingX * 2)
|
|
||||||
var booksPerRow = Math.floor(this.width / this.bookWidth)
|
|
||||||
this.booksPerRow = booksPerRow
|
|
||||||
},
|
|
||||||
init() {
|
|
||||||
var bookshelfCoverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
var bookshelfCoverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
||||||
var sizeIndex = this.availableSizes.findIndex((s) => s === bookshelfCoverSize)
|
var sizeIndex = this.availableSizes.findIndex((s) => s === bookshelfCoverSize)
|
||||||
if (!isNaN(sizeIndex)) this.selectedSizeIndex = sizeIndex
|
if (!isNaN(sizeIndex)) this.selectedSizeIndex = sizeIndex
|
||||||
this.calculateBookshelf()
|
|
||||||
|
var isLoading = await this.$store.dispatch('audiobooks/load')
|
||||||
|
if (!isLoading) {
|
||||||
|
this.setBookshelfEntities()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
resize() {
|
resize() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.calculateBookshelf()
|
|
||||||
this.setBookshelfEntities()
|
this.setBookshelfEntities()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -186,17 +203,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
window.addEventListener('resize', this.resize)
|
||||||
this.$store.commit('audiobooks/addListener', { id: 'bookshelf', meth: this.audiobooksUpdated })
|
this.$store.commit('audiobooks/addListener', { id: 'bookshelf', meth: this.audiobooksUpdated })
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
|
this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
|
||||||
|
|
||||||
this.$store.dispatch('audiobooks/load')
|
|
||||||
this.init()
|
this.init()
|
||||||
window.addEventListener('resize', this.resize)
|
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.resize)
|
||||||
this.$store.commit('audiobooks/removeListener', 'bookshelf')
|
this.$store.commit('audiobooks/removeListener', 'bookshelf')
|
||||||
this.$store.commit('user/removeSettingsListener', 'bookshelf')
|
this.$store.commit('user/removeSettingsListener', 'bookshelf')
|
||||||
window.removeEventListener('resize', this.resize)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,20 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-10 relative">
|
<div class="w-full h-10 relative">
|
||||||
<div id="toolbar" class="absolute top-0 left-0 w-full h-full z-20 flex items-center px-8">
|
<div id="toolbar" class="absolute top-0 left-0 w-full h-full z-20 flex items-center px-8">
|
||||||
<p class="font-book">{{ numShowing }} Audiobooks</p>
|
<p v-if="!selectedSeries" class="font-book">{{ numShowing }} {{ entityName }}</p>
|
||||||
|
<div v-else class="flex items-center">
|
||||||
|
<div @click="seriesBackArrow" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 cursor-pointer">
|
||||||
|
<span class="material-icons text-3xl text-white">west</span>
|
||||||
|
</div>
|
||||||
|
<!-- <span class="material-icons text-2xl cursor-pointer" @click="seriesBackArrow">west</span> -->
|
||||||
|
<p class="pl-4 font-book text-lg">
|
||||||
|
{{ selectedSeries }} <span class="ml-3 font-mono text-lg bg-black bg-opacity-30 rounded-lg px-1 py-0.5">{{ numShowing }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<ui-text-input v-model="_keywordFilter" placeholder="Keyword Filter" :padding-y="1.5" class="text-xs w-40" />
|
<ui-text-input v-show="showSortFilters" v-model="_keywordFilter" placeholder="Keyword Filter" :padding-y="1.5" class="text-xs w-40" />
|
||||||
|
<controls-filter-select v-show="showSortFilters" v-model="settings.filterBy" class="w-48 h-7.5 ml-4" @change="updateFilter" />
|
||||||
<controls-filter-select v-model="settings.filterBy" class="w-48 h-7.5 ml-4" @change="updateFilter" />
|
<controls-order-select v-show="showSortFilters" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-48 h-7.5 ml-4" @change="updateOrder" />
|
||||||
|
|
||||||
<controls-order-select v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-48 h-7.5 ml-4" @change="updateOrder" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
props: {
|
||||||
|
page: String,
|
||||||
|
selectedSeries: String
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
settings: {},
|
settings: {},
|
||||||
@ -22,8 +33,27 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
showSortFilters() {
|
||||||
|
return this.page === ''
|
||||||
|
},
|
||||||
numShowing() {
|
numShowing() {
|
||||||
|
if (this.page === '') {
|
||||||
return this.$store.getters['audiobooks/getFiltered']().length
|
return this.$store.getters['audiobooks/getFiltered']().length
|
||||||
|
} else {
|
||||||
|
var groups = this.$store.getters['audiobooks/getSeriesGroups']()
|
||||||
|
if (this.selectedSeries) {
|
||||||
|
var group = groups.find((g) => g.name === this.selectedSeries)
|
||||||
|
if (group) return group.books.length
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return groups.length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
entityName() {
|
||||||
|
if (!this.page) return 'Audiobooks'
|
||||||
|
if (this.page === 'series') return 'Series'
|
||||||
|
if (this.page === 'collections') return 'Collections'
|
||||||
|
return ''
|
||||||
},
|
},
|
||||||
_keywordFilter: {
|
_keywordFilter: {
|
||||||
get() {
|
get() {
|
||||||
@ -35,6 +65,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
seriesBackArrow() {
|
||||||
|
this.$router.replace('/library/series')
|
||||||
|
this.$emit('update:selectedSeries', null)
|
||||||
|
},
|
||||||
updateOrder() {
|
updateOrder() {
|
||||||
this.saveSettings()
|
this.saveSettings()
|
||||||
},
|
},
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-20 border-r border-primary bg-bg h-full relative box-shadow-side z-20">
|
<div class="w-20 border-r border-primary bg-bg h-full relative box-shadow-side z-40" style="min-width: 80px">
|
||||||
<nuxt-link to="/library" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === '' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link to="/library" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === '' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<p class="font-book pt-1.5" style="font-size: 0.8rem">Library</p>
|
<p class="font-book pt-1.5" style="font-size: 1rem">Library</p>
|
||||||
|
|
||||||
<div v-show="paramId === ''" class="h-0.5 w-full bg-yellow-400 absolute bottom-0 left-0" />
|
<div v-show="paramId === ''" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link to="/library/series" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'series' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link to="/library/series" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'series' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
@ -15,40 +15,40 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<p class="font-book pt-1.5" style="font-size: 0.8rem">Series</p>
|
<p class="font-book pt-1.5" style="font-size: 1rem">Series</p>
|
||||||
|
|
||||||
<div v-show="paramId === 'series'" class="h-0.5 w-full bg-yellow-400 absolute bottom-0 left-0" />
|
<div v-show="paramId === 'series'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link to="/library/collections" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<!-- <nuxt-link to="/library/collections" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<p class="font-book pt-1.5" style="font-size: 0.8rem">Collections</p>
|
<p class="font-book pt-1.5" style="font-size: 0.8rem">Collections</p>
|
||||||
|
|
||||||
<div v-show="paramId === 'collections'" class="h-0.5 w-full bg-yellow-400 absolute bottom-0 left-0" />
|
<div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link> -->
|
||||||
|
|
||||||
<nuxt-link to="/library/tags" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'tags' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<!-- <nuxt-link to="/library/tags" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'tags' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<p class="font-book pt-1.5" style="font-size: 0.8rem">Tags</p>
|
<p class="font-book pt-1.5" style="font-size: 0.8rem">Tags</p>
|
||||||
|
|
||||||
<div v-show="paramId === 'tags'" class="h-0.5 w-full bg-yellow-400 absolute bottom-0 left-0" />
|
<div v-show="paramId === 'tags'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link> -->
|
||||||
|
|
||||||
<nuxt-link to="/library/authors" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'authors' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<!-- <nuxt-link to="/library/authors" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'authors' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<p class="font-book pt-1.5" style="font-size: 0.8rem">Authors</p>
|
<p class="font-book pt-1.5" style="font-size: 0.8rem">Authors</p>
|
||||||
|
|
||||||
<div v-show="paramId === 'authors'" class="h-0.5 w-full bg-yellow-400 absolute bottom-0 left-0" />
|
<div v-show="paramId === 'authors'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
filterByAuthor() {
|
filterByAuthor() {
|
||||||
if (this.$route.name !== 'index') {
|
if (this.$route.name !== 'index') {
|
||||||
this.$router.push('/')
|
this.$router.push('/library')
|
||||||
}
|
}
|
||||||
var settingsUpdate = {
|
var settingsUpdate = {
|
||||||
filterBy: `authors.${this.$encode(this.author)}`
|
filterBy: `authors.${this.$encode(this.author)}`
|
||||||
|
254
client/components/cards/Book3d.vue
Normal file
254
client/components/cards/Book3d.vue
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="wrapper" class="relative pointer-events-none" :style="{ width: standardWidth * 0.8 * 1.1 * scale + 'px', height: standardHeight * 1.1 * scale + 'px', marginBottom: 20 + 'px', marginTop: 15 + 'px' }">
|
||||||
|
<div ref="card" class="wrap absolute origin-center transform duration-200" :style="{ transform: `scale(${scale * scaleMultiplier}) translateY(${hover2 ? '-40%' : '-50%'})` }">
|
||||||
|
<div class="perspective">
|
||||||
|
<div class="book-wrap transform duration-100 pointer-events-auto" :class="hover2 ? 'z-80' : 'rotate'" @mouseover="hover = true" @mouseout="hover = false">
|
||||||
|
<div class="book book-1 box-shadow-book3d" ref="front"></div>
|
||||||
|
<div class="title book-1 pointer-events-none" ref="left"></div>
|
||||||
|
<div class="bottom book-1 pointer-events-none" ref="bottom"></div>
|
||||||
|
<div class="book-back book-1 pointer-events-none">
|
||||||
|
<div class="text pointer-events-none">
|
||||||
|
<h3 class="mb-4">Book Back</h3>
|
||||||
|
<p>
|
||||||
|
<span>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Sunt earum doloremque aliquam culpa dolor nostrum consequatur quas dicta? Molestias repellendus minima pariatur libero vel, reiciendis optio magnam rerum, labore corporis.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
src: String,
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hover: false,
|
||||||
|
hover2: false,
|
||||||
|
standardWidth: 200,
|
||||||
|
standardHeight: 320,
|
||||||
|
isAttached: true,
|
||||||
|
pageX: 0,
|
||||||
|
pageY: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
src(newVal) {
|
||||||
|
this.setCover()
|
||||||
|
},
|
||||||
|
width(newVal) {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
hover(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.unattach()
|
||||||
|
} else {
|
||||||
|
this.attach()
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.hover2 = newVal
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
scaleMultiplier() {
|
||||||
|
return this.hover2 ? 1.25 : 1
|
||||||
|
},
|
||||||
|
scale() {
|
||||||
|
var scale = this.width / this.standardWidth
|
||||||
|
return scale
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
unattach() {
|
||||||
|
if (this.$refs.card && this.isAttached) {
|
||||||
|
var bookshelf = document.getElementById('bookshelf')
|
||||||
|
if (bookshelf) {
|
||||||
|
var pos = this.$refs.wrapper.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.pageX = pos.x
|
||||||
|
this.pageY = pos.y
|
||||||
|
document.body.appendChild(this.$refs.card)
|
||||||
|
this.$refs.card.style.left = this.pageX + 'px'
|
||||||
|
this.$refs.card.style.top = this.pageY + 'px'
|
||||||
|
this.$refs.card.style.zIndex = 50
|
||||||
|
this.isAttached = false
|
||||||
|
} else if (bookshelf) {
|
||||||
|
console.log(this.pageX, this.pageY)
|
||||||
|
this.isAttached = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
attach() {
|
||||||
|
if (this.$refs.card && !this.isAttached) {
|
||||||
|
if (this.$refs.wrapper) {
|
||||||
|
this.isAttached = true
|
||||||
|
|
||||||
|
this.$refs.wrapper.appendChild(this.$refs.card)
|
||||||
|
this.$refs.card.style.left = '0px'
|
||||||
|
this.$refs.card.style.top = '0px'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Is attached already', this.isAttached)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
var standardWidth = this.standardWidth
|
||||||
|
document.documentElement.style.setProperty('--book-w', standardWidth + 'px')
|
||||||
|
document.documentElement.style.setProperty('--book-wx', standardWidth + 1 + 'px')
|
||||||
|
document.documentElement.style.setProperty('--book-h', standardWidth * 1.6 + 'px')
|
||||||
|
document.documentElement.style.setProperty('--book-d', 40 + 'px')
|
||||||
|
},
|
||||||
|
setElBg(el) {
|
||||||
|
el.style.backgroundImage = `url("${this.src}")`
|
||||||
|
el.style.backgroundSize = 'cover'
|
||||||
|
el.style.backgroundPosition = 'center center'
|
||||||
|
el.style.backgroundRepeat = 'no-repeat'
|
||||||
|
},
|
||||||
|
setCover() {
|
||||||
|
if (this.$refs.front) {
|
||||||
|
this.setElBg(this.$refs.front)
|
||||||
|
}
|
||||||
|
if (this.$refs.bottom) {
|
||||||
|
this.setElBg(this.$refs.bottom)
|
||||||
|
this.$refs.bottom.style.backgroundSize = '2000%'
|
||||||
|
this.$refs.bottom.style.filter = 'blur(1px)'
|
||||||
|
}
|
||||||
|
if (this.$refs.left) {
|
||||||
|
this.setElBg(this.$refs.left)
|
||||||
|
this.$refs.left.style.backgroundSize = '2000%'
|
||||||
|
this.$refs.left.style.filter = 'blur(1px)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.setCover()
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* :root {
|
||||||
|
--book-w: 200px;
|
||||||
|
--book-h: 320px;
|
||||||
|
--book-d: 30px;
|
||||||
|
--book-wx: 201px;
|
||||||
|
} */
|
||||||
|
/*
|
||||||
|
.wrap {
|
||||||
|
width: calc(1.1 * var(--book-w));
|
||||||
|
height: calc(1.1 * var(--book-h));
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.perspective {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
perspective: 600px;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-wrap {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
transition: 'all ease-out 0.6s';
|
||||||
|
}
|
||||||
|
|
||||||
|
.book {
|
||||||
|
width: var(--book-w);
|
||||||
|
height: var(--book-h);
|
||||||
|
background: url(https://covers.openlibrary.org/b/id/8303020-L.jpg) no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
content: '';
|
||||||
|
height: var(--book-h);
|
||||||
|
width: var(--book-d);
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
left: calc(var(--book-wx) * -1);
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
background: #444;
|
||||||
|
transform: rotateY(-80deg) translateX(-14px);
|
||||||
|
|
||||||
|
background: url(https://covers.openlibrary.org/b/id/8303020-L.jpg) no-repeat center center;
|
||||||
|
background-size: 5000%;
|
||||||
|
filter: blur(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom {
|
||||||
|
content: '';
|
||||||
|
height: var(--book-d);
|
||||||
|
width: var(--book-w);
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: var(--book-h);
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
margin: auto;
|
||||||
|
background: #444;
|
||||||
|
transform: rotateY(0deg) rotateX(90deg) translateY(-15px) translateX(-2.5px) skewX(10deg);
|
||||||
|
|
||||||
|
background: url(https://covers.openlibrary.org/b/id/8303020-L.jpg) no-repeat center center;
|
||||||
|
background-size: 5000%;
|
||||||
|
filter: blur(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-back {
|
||||||
|
width: var(--book-w);
|
||||||
|
height: var(--book-h);
|
||||||
|
background-color: #444;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
transform: rotate(180deg) translateZ(-30px) translateX(5px);
|
||||||
|
}
|
||||||
|
.book-back .text {
|
||||||
|
transform: rotateX(180deg);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.book-back .text h3 {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.book-back .text span {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-wrap.rotate {
|
||||||
|
transform: rotateY(30deg) rotateX(0deg);
|
||||||
|
}
|
||||||
|
.book-wrap.flip {
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
} */
|
||||||
|
</style>
|
@ -79,15 +79,7 @@ export default {
|
|||||||
return '/book_placeholder.jpg'
|
return '/book_placeholder.jpg'
|
||||||
},
|
},
|
||||||
fullCoverUrl() {
|
fullCoverUrl() {
|
||||||
if (!this.cover || this.cover === this.placeholderUrl) return this.placeholderUrl
|
return this.$store.getters['audiobooks/getBookCoverSrc'](this.book, this.placeholderUrl)
|
||||||
if (this.cover.startsWith('http:') || this.cover.startsWith('https:')) return this.cover
|
|
||||||
try {
|
|
||||||
var url = new URL(this.cover, document.baseURI)
|
|
||||||
return url.href + `?token=${this.userToken}&ts=${this.bookLastUpdate}`
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
cover() {
|
cover() {
|
||||||
return this.book.cover || this.placeholderUrl
|
return this.book.cover || this.placeholderUrl
|
||||||
|
@ -1,13 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `16px ${paddingX}px` }" @mouseover="isHovering = true" @mouseleave="isHovering = false" @click="clickCard">
|
<div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `16px ${paddingX}px` }" @mouseover="isHovering = true" @mouseleave="isHovering = false" @click="clickCard">
|
||||||
<nuxt-link :to="`/library`" class="cursor-pointer">
|
<nuxt-link :to="`/library/series?${groupType}=${groupEncode}`" class="cursor-pointer">
|
||||||
<div class="w-full relative box-shadow-book bg-primary" :style="{ height: height + 'px', width: height + 'px' }"></div>
|
<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" :book-items="bookItems" :width="height" :height="height" />
|
||||||
|
|
||||||
|
<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" :class="isHovering ? '' : 'opacity-0'">
|
||||||
|
<p class="truncate font-book" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ groupName }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute top-2 right-2 w-7 h-7 rounded-lg bg-black bg-opacity-90 text-gray-300 box-shadow-book flex items-center justify-center border border-white border-opacity-25 pointer-events-none">
|
||||||
|
<p class="font-book text-xl">{{ bookItems.length }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div :style="{ width: height + 'px', height: height + 'px' }" class="box-shadow-book bg-primary">
|
|
||||||
<p class="text-white">{{ groupName }}</p>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -28,6 +35,15 @@ export default {
|
|||||||
isHovering: false
|
isHovering: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
width(newVal) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.groupcover) {
|
||||||
|
this.$refs.groupcover.init()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
_group() {
|
_group() {
|
||||||
return this.group || {}
|
return this.group || {}
|
||||||
@ -41,15 +57,30 @@ export default {
|
|||||||
paddingX() {
|
paddingX() {
|
||||||
return 16 * this.sizeMultiplier
|
return 16 * this.sizeMultiplier
|
||||||
},
|
},
|
||||||
books() {
|
bookItems() {
|
||||||
return this._group.books || []
|
return this._group.books || []
|
||||||
},
|
},
|
||||||
groupName() {
|
groupName() {
|
||||||
return this._group.name || 'No Name'
|
return this._group.name || 'No Name'
|
||||||
|
},
|
||||||
|
groupType() {
|
||||||
|
return this._group.type
|
||||||
|
},
|
||||||
|
groupEncode() {
|
||||||
|
return this.$encode(this.groupName)
|
||||||
|
},
|
||||||
|
filter() {
|
||||||
|
return `${this.groupType}.${this.$encode(this.groupName)}`
|
||||||
|
},
|
||||||
|
hasValidCovers() {
|
||||||
|
var validCovers = this.bookItems.map((bookItem) => bookItem.book.cover)
|
||||||
|
return !!validCovers.length
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clickCard() {}
|
clickCard() {
|
||||||
|
this.$emit('click', this.group)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
|
139
client/components/cards/GroupCover.vue
Normal file
139
client/components/cards/GroupCover.vue
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="wrapper" :style="{ height: height + 'px', width: width + 'px' }" class="relative">
|
||||||
|
<div v-if="noValidCovers" class="absolute top-0 left-0 w-full h-full flex items-center justify-center box-shadow-book">
|
||||||
|
<p :style="{ fontSize: sizeMultiplier + 'rem' }">{{ name }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
name: String,
|
||||||
|
bookItems: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
width: Number,
|
||||||
|
height: Number
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
noValidCovers: false,
|
||||||
|
coverDiv: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
bookItems: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
// ensure wrapper is initialized
|
||||||
|
this.$nextTick(this.init)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
sizeMultiplier() {
|
||||||
|
return this.width / 192
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getCoverUrl(book) {
|
||||||
|
return this.$store.getters['audiobooks/getBookCoverSrc'](book, '')
|
||||||
|
},
|
||||||
|
async buildCoverImg(src, bgCoverWidth, offsetLeft, forceCoverBg = false) {
|
||||||
|
var showCoverBg =
|
||||||
|
forceCoverBg ||
|
||||||
|
(await new Promise((resolve) => {
|
||||||
|
var image = new Image()
|
||||||
|
|
||||||
|
image.onload = () => {
|
||||||
|
var { naturalWidth, naturalHeight } = image
|
||||||
|
var aspectRatio = naturalHeight / naturalWidth
|
||||||
|
var arDiff = Math.abs(aspectRatio - 1.6)
|
||||||
|
|
||||||
|
// If image aspect ratio is <= 1.45 or >= 1.75 then use cover bg, otherwise stretch to fit
|
||||||
|
if (arDiff > 0.15) {
|
||||||
|
resolve(true)
|
||||||
|
} else {
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
image.onerror = (err) => {
|
||||||
|
console.error(err)
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
image.src = src
|
||||||
|
}))
|
||||||
|
|
||||||
|
var imgdiv = document.createElement('div')
|
||||||
|
imgdiv.style.height = this.height + 'px'
|
||||||
|
imgdiv.style.width = bgCoverWidth + 'px'
|
||||||
|
imgdiv.style.left = offsetLeft + 'px'
|
||||||
|
imgdiv.className = 'absolute top-0 box-shadow-book'
|
||||||
|
imgdiv.style.boxShadow = '-4px 0px 4px #11111166'
|
||||||
|
// imgdiv.style.transform = 'skew(0deg, 15deg)'
|
||||||
|
|
||||||
|
if (showCoverBg) {
|
||||||
|
var coverbgwrapper = document.createElement('div')
|
||||||
|
coverbgwrapper.className = 'absolute top-0 left-0 w-full h-full bg-primary'
|
||||||
|
|
||||||
|
var coverbg = document.createElement('div')
|
||||||
|
coverbg.className = 'w-full h-full'
|
||||||
|
coverbg.style.backgroundImage = `url("${src}")`
|
||||||
|
coverbg.style.backgroundSize = 'cover'
|
||||||
|
coverbg.style.backgroundPosition = 'center'
|
||||||
|
coverbg.style.opacity = 0.25
|
||||||
|
coverbg.style.filter = 'blur(1px)'
|
||||||
|
|
||||||
|
coverbgwrapper.appendChild(coverbg)
|
||||||
|
imgdiv.appendChild(coverbgwrapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
var img = document.createElement('img')
|
||||||
|
img.src = src
|
||||||
|
img.className = 'absolute top-0 left-0 w-full h-full'
|
||||||
|
img.style.objectFit = showCoverBg ? 'contain' : 'cover'
|
||||||
|
|
||||||
|
imgdiv.appendChild(img)
|
||||||
|
return imgdiv
|
||||||
|
},
|
||||||
|
async init() {
|
||||||
|
if (this.coverDiv) {
|
||||||
|
this.coverDiv.remove()
|
||||||
|
this.coverDiv = null
|
||||||
|
}
|
||||||
|
var validCovers = this.bookItems.map((bookItem) => this.getCoverUrl(bookItem.book)).filter((b) => b !== '')
|
||||||
|
if (!validCovers.length) {
|
||||||
|
this.noValidCovers = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.noValidCovers = false
|
||||||
|
|
||||||
|
var coverWidth = this.width
|
||||||
|
var widthPer = this.width
|
||||||
|
if (validCovers.length > 1) {
|
||||||
|
coverWidth = this.height / 1.6
|
||||||
|
widthPer = (this.width - coverWidth) / (validCovers.length - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var outerdiv = document.createElement('div')
|
||||||
|
outerdiv.className = 'w-full h-full relative'
|
||||||
|
|
||||||
|
for (let i = 0; i < validCovers.length; i++) {
|
||||||
|
var offsetLeft = widthPer * i
|
||||||
|
var img = await this.buildCoverImg(validCovers[i], coverWidth, offsetLeft, validCovers.length === 1)
|
||||||
|
outerdiv.appendChild(img)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$refs.wrapper) {
|
||||||
|
this.coverDiv = outerdiv
|
||||||
|
this.$refs.wrapper.appendChild(outerdiv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
@ -115,7 +115,10 @@ export default {
|
|||||||
clickedOption(e, item) {
|
clickedOption(e, item) {
|
||||||
this.textInput = null
|
this.textInput = null
|
||||||
this.currentSearch = null
|
this.currentSearch = null
|
||||||
this.input = this.textInput ? this.textInput.trim() : null
|
this.input = item
|
||||||
|
|
||||||
|
// this.input = this.textInput ? this.textInput.trim() : null
|
||||||
|
console.log('Clicked option', item)
|
||||||
if (this.$refs.input) this.$refs.input.blur()
|
if (this.$refs.input) this.$refs.input.blur()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -53,7 +53,7 @@ export default {
|
|||||||
var tooltip = document.createElement('div')
|
var tooltip = document.createElement('div')
|
||||||
tooltip.className = 'absolute px-2 py-1 text-white pointer-events-none text-xs rounded shadow-lg max-w-xs'
|
tooltip.className = 'absolute px-2 py-1 text-white pointer-events-none text-xs rounded shadow-lg max-w-xs'
|
||||||
tooltip.style.zIndex = 100
|
tooltip.style.zIndex = 100
|
||||||
tooltip.style.backgroundColor = 'rgba(0,0,0,0.75)'
|
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
|
||||||
tooltip.innerHTML = this.text
|
tooltip.innerHTML = this.text
|
||||||
|
|
||||||
this.setTooltipPosition(tooltip)
|
this.setTooltipPosition(tooltip)
|
||||||
|
@ -86,7 +86,7 @@ export default {
|
|||||||
audiobookRemoved(audiobook) {
|
audiobookRemoved(audiobook) {
|
||||||
if (this.$route.name.startsWith('audiobook')) {
|
if (this.$route.name.startsWith('audiobook')) {
|
||||||
if (this.$route.params.id === audiobook.id) {
|
if (this.$route.params.id === audiobook.id) {
|
||||||
this.$router.replace('/')
|
this.$router.replace('/library')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.$store.commit('audiobooks/remove', audiobook)
|
this.$store.commit('audiobooks/remove', audiobook)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export default function ({ store, redirect, route }) {
|
export default function ({ store, redirect, route, app }) {
|
||||||
// If the user is not authenticated
|
// If the user is not authenticated
|
||||||
if (!store.state.user.user) {
|
if (!store.state.user.user) {
|
||||||
if (route.name === 'batch') return redirect('/login')
|
if (route.name === 'batch') return redirect('/login')
|
||||||
return redirect(`/login?redirect=${route.path}`)
|
return redirect(`/login?redirect=${route.fullPath}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "1.1.15",
|
"version": "1.2.0",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -130,7 +130,7 @@ export default {
|
|||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
if (data.updates) {
|
if (data.updates) {
|
||||||
this.$toast.success(`Successfully updated ${data.updates} audiobooks`)
|
this.$toast.success(`Successfully updated ${data.updates} audiobooks`)
|
||||||
this.$router.replace('/')
|
this.$router.replace('/library')
|
||||||
} else {
|
} else {
|
||||||
this.$toast.warning('No updates were necessary')
|
this.$toast.warning('No updates were necessary')
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page" :class="streamAudiobook ? 'streaming' : ''">
|
<div class="page" :class="streamAudiobook ? 'streaming' : ''">
|
||||||
<app-book-shelf-toolbar />
|
<!-- <app-book-shelf-toolbar /> -->
|
||||||
<!-- <div class="flex h-full">
|
<!-- <div class="flex h-full">
|
||||||
<app-side-rail />
|
<app-side-rail />
|
||||||
<div class="flex-grow"> -->
|
<div class="flex-grow"> -->
|
||||||
<app-book-shelf />
|
<!-- <app-book-shelf /> -->
|
||||||
<!-- </div> -->
|
<!-- </div> -->
|
||||||
<!-- </div> -->
|
<!-- </div> -->
|
||||||
</div>
|
</div>
|
||||||
@ -12,6 +12,9 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
asyncData({ redirect }) {
|
||||||
|
redirect('/library')
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
<div class="flex h-full">
|
<div class="flex h-full">
|
||||||
<app-side-rail />
|
<app-side-rail />
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
<app-book-shelf-toolbar />
|
<app-book-shelf-toolbar :page="id || ''" :selected-series.sync="selectedSeries" />
|
||||||
<app-book-shelf :page="id || ''" />
|
<app-book-shelf :page="id || ''" :selected-series.sync="selectedSeries" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -12,9 +12,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
asyncData({ params }) {
|
asyncData({ params, query, store, app }) {
|
||||||
|
if (query.filter) {
|
||||||
|
store.dispatch('user/updateUserSettings', { filterBy: query.filter })
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
id: params.id
|
id: params.id,
|
||||||
|
selectedSeries: query.series ? app.$decode(query.series) : null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -37,7 +37,7 @@ export default {
|
|||||||
if (this.$route.query.redirect) {
|
if (this.$route.query.redirect) {
|
||||||
this.$router.replace(this.$route.query.redirect)
|
this.$router.replace(this.$route.query.redirect)
|
||||||
} else {
|
} else {
|
||||||
this.$router.replace('/')
|
this.$router.replace('/library')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,3 +125,6 @@ export {
|
|||||||
encode,
|
encode,
|
||||||
decode
|
decode
|
||||||
}
|
}
|
||||||
|
export default ({ app }, inject) => {
|
||||||
|
app.$decode = decode
|
||||||
|
}
|
@ -5,6 +5,7 @@ const STANDARD_GENRES = ['Adventure', 'Autobiography', 'Biography', 'Childrens',
|
|||||||
|
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
audiobooks: [],
|
audiobooks: [],
|
||||||
|
lastLoad: 0,
|
||||||
listeners: [],
|
listeners: [],
|
||||||
genres: [...STANDARD_GENRES],
|
genres: [...STANDARD_GENRES],
|
||||||
tags: [],
|
tags: [],
|
||||||
@ -88,29 +89,63 @@ export const getters = {
|
|||||||
var _genres = []
|
var _genres = []
|
||||||
state.audiobooks.filter(ab => !!(ab.book && ab.book.genres)).forEach(ab => _genres = _genres.concat(ab.book.genres))
|
state.audiobooks.filter(ab => !!(ab.book && ab.book.genres)).forEach(ab => _genres = _genres.concat(ab.book.genres))
|
||||||
return [...new Set(_genres)].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
|
return [...new Set(_genres)].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
|
||||||
|
},
|
||||||
|
getBookCoverSrc: (state, getters, rootState, rootGetters) => (book, placeholder = '/book_placeholder.jpg') => {
|
||||||
|
if (!book || !book.cover || book.cover === placeholder) return placeholder
|
||||||
|
var cover = book.cover
|
||||||
|
|
||||||
|
// Absolute URL covers
|
||||||
|
if (cover.startsWith('http:') || cover.startsWith('https:')) return cover
|
||||||
|
|
||||||
|
// Server hosted covers
|
||||||
|
try {
|
||||||
|
// Ensure cover is refreshed if cached
|
||||||
|
var bookLastUpdate = book.lastUpdate || Date.now()
|
||||||
|
var userToken = rootGetters['user/getToken']
|
||||||
|
|
||||||
|
var url = new URL(cover, document.baseURI)
|
||||||
|
return url.href + `?token=${userToken}&ts=${bookLastUpdate}`
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
return placeholder
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
load({ commit, rootState }) {
|
// Return true if calling load
|
||||||
|
load({ state, commit, rootState }) {
|
||||||
if (!rootState.user || !rootState.user.user) {
|
if (!rootState.user || !rootState.user.user) {
|
||||||
console.error('audiobooks/load - User not set')
|
console.error('audiobooks/load - User not set')
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't load again if already loaded in the last 5 minutes
|
||||||
|
var lastLoadDiff = Date.now() - state.lastLoad
|
||||||
|
if (lastLoadDiff < 5 * 60 * 1000) {
|
||||||
|
// Already up to date
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/audiobooks`)
|
.$get(`/api/audiobooks`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
commit('set', data)
|
commit('set', data)
|
||||||
|
commit('setLastLoad')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
commit('set', [])
|
commit('set', [])
|
||||||
})
|
})
|
||||||
|
return true
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
|
setLastLoad(state) {
|
||||||
|
state.lastLoad = Date.now()
|
||||||
|
},
|
||||||
setKeywordFilter(state, val) {
|
setKeywordFilter(state, val) {
|
||||||
state.keywordFilter = val
|
state.keywordFilter = val
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "1.1.15",
|
"version": "1.2.0",
|
||||||
"description": "Self-hosted audiobook server for managing and playing audiobooks.",
|
"description": "Self-hosted audiobook server for managing and playing audiobooks.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -8,8 +8,10 @@
|
|||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
"client": "cd client && npm install && npm run generate",
|
"client": "cd client && npm install && npm run generate",
|
||||||
"prod": "npm run client && npm install && node prod.js",
|
"prod": "npm run client && npm install && node prod.js",
|
||||||
"build-win": "cd client && npm run generate && cd .. && pkg -t node12-win-x64 -o ./dist/app .",
|
"generate": "cd client && npm run generate",
|
||||||
"build-linux": "pkg -t node12-linux-arm64 -o ./dist/app ."
|
"build-win": "pkg -t node12-win-x64 -o ./dist/win/audiobookshelf .",
|
||||||
|
"build-linuxarm": "pkg -t node12-linux-arm64 -o ./dist/linuxarm/audiobookshelf .",
|
||||||
|
"build-linuxamd": "pkg -t node12-linux-amd64 -o ./dist/linuxamd/audiobookshelf ."
|
||||||
},
|
},
|
||||||
"bin": "prod.js",
|
"bin": "prod.js",
|
||||||
"pkg": {
|
"pkg": {
|
||||||
|
@ -82,9 +82,6 @@ cd audiobookshelf
|
|||||||
# Directories will be created if they don't exist
|
# Directories will be created if they don't exist
|
||||||
# Paths are relative to the root directory, so "../Audiobooks" would be a valid path
|
# Paths are relative to the root directory, so "../Audiobooks" would be a valid path
|
||||||
npm run prod -- -p [PORT] --audiobooks [AUDIOBOOKS_PATH] --config [CONFIG_PATH] --metadata [METADATA_PATH]
|
npm run prod -- -p [PORT] --audiobooks [AUDIOBOOKS_PATH] --config [CONFIG_PATH] --metadata [METADATA_PATH]
|
||||||
|
|
||||||
# You only need to use `npm run prod` the first time, after that use `npm run start`
|
|
||||||
npm run start -- -p [PORT] --audiobooks [AUDIOBOOKS_PATH] --config [CONFIG_PATH] --metadata [METADATA_PATH]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
@ -199,6 +199,8 @@ class Server {
|
|||||||
|
|
||||||
// Dynamic routes are not generated on client
|
// Dynamic routes are not generated on client
|
||||||
app.get('/audiobook/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
app.get('/audiobook/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||||
|
app.get('/library/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||||
|
app.get('/library', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||||
|
|
||||||
app.use('/api', this.authMiddleware.bind(this), this.apiController.router)
|
app.use('/api', this.authMiddleware.bind(this), this.apiController.router)
|
||||||
app.use('/hls', this.authMiddleware.bind(this), this.hlsController.router)
|
app.use('/hls', this.authMiddleware.bind(this), this.hlsController.router)
|
||||||
|
Loading…
Reference in New Issue
Block a user