mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-22 19:59:37 +00:00
Init
This commit is contained in:
commit
6930e69b55
106 changed files with 26925 additions and 0 deletions
88
client/components/app/Appbar.vue
Normal file
88
client/components/app/Appbar.vue
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<template>
|
||||
<div class="w-full h-16 bg-primary relative">
|
||||
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-6 py-1 z-10">
|
||||
<div class="flex h-full items-center">
|
||||
<img v-if="!showBack" src="/LogoTransparent.png" class="w-12 h-12 mr-4" />
|
||||
<a v-if="showBack" @click="back" class="rounded-full h-12 w-12 flex items-center justify-center hover:bg-white hover:bg-opacity-10 mr-4 cursor-pointer">
|
||||
<span class="material-icons text-4xl text-white">arrow_back</span>
|
||||
</a>
|
||||
<h1 class="text-2xl font-book">AudioBookshelf</h1>
|
||||
|
||||
<div class="flex-grow" />
|
||||
|
||||
<!-- <button class="px-4 py-2 bg-blue-500 rounded-xs" @click="scan">Scan</button> -->
|
||||
<nuxt-link to="/config" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center">
|
||||
<span class="material-icons">settings</span>
|
||||
</nuxt-link>
|
||||
|
||||
<ui-menu :label="username" :items="menuItems" @action="menuAction" class="ml-5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
menuItems: [
|
||||
// {
|
||||
// value: 'settings',
|
||||
// text: 'Settings'
|
||||
// },
|
||||
{
|
||||
value: 'logout',
|
||||
text: 'Logout'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showBack() {
|
||||
return this.$route.name !== 'index'
|
||||
},
|
||||
user() {
|
||||
return this.$store.state.user
|
||||
},
|
||||
username() {
|
||||
return this.user ? this.user.username : 'err'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
back() {
|
||||
if (this.$route.name === 'audiobook-id-edit') {
|
||||
this.$router.push(`/audiobook/${this.$route.params.id}`)
|
||||
} else {
|
||||
this.$router.push('/')
|
||||
}
|
||||
},
|
||||
scan() {
|
||||
console.log('Call Start Init')
|
||||
this.$root.socket.emit('scan')
|
||||
},
|
||||
logout() {
|
||||
this.$axios.$post('/logout').catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
if (localStorage.getItem('token')) {
|
||||
localStorage.removeItem('token')
|
||||
}
|
||||
this.$router.push('/login')
|
||||
},
|
||||
menuAction(action) {
|
||||
if (action === 'logout') {
|
||||
this.logout()
|
||||
} else if (action === 'settings') {
|
||||
// Show settings modal
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#appbar {
|
||||
box-shadow: 0px 8px 8px #111111aa;
|
||||
}
|
||||
</style>
|
||||
102
client/components/app/BookShelf.vue
Normal file
102
client/components/app/BookShelf.vue
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<template>
|
||||
<div id="bookshelf" ref="wrapper" class="w-full h-full overflow-y-auto">
|
||||
<div class="w-full flex flex-col items-center">
|
||||
<template v-for="(shelf, index) in groupedBooks">
|
||||
<div :key="index" class="w-full bookshelfRow relative">
|
||||
<div class="flex justify-center items-center">
|
||||
<template v-for="audiobook in shelf">
|
||||
<cards-book-card :ref="`audiobookCard-${audiobook.id}`" :key="audiobook.id" :user-progress="userAudiobooks[audiobook.id]" :audiobook="audiobook" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
width: 0,
|
||||
bookWidth: 176,
|
||||
booksPerRow: 0,
|
||||
groupedBooks: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userAudiobooks() {
|
||||
return this.$store.state.user ? this.$store.state.user.audiobooks || {} : {}
|
||||
},
|
||||
audiobooks() {
|
||||
return this.$store.state.audiobooks.audiobooks
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setGroupedBooks() {
|
||||
var groups = []
|
||||
var currentRow = 0
|
||||
var currentGroup = []
|
||||
for (let i = 0; i < this.audiobooks.length; i++) {
|
||||
var row = Math.floor(i / this.booksPerRow)
|
||||
if (row > currentRow) {
|
||||
groups.push([...currentGroup])
|
||||
currentRow = row
|
||||
currentGroup = []
|
||||
}
|
||||
currentGroup.push(this.audiobooks[i])
|
||||
}
|
||||
if (currentGroup.length) {
|
||||
groups.push([...currentGroup])
|
||||
}
|
||||
this.groupedBooks = groups
|
||||
},
|
||||
calculateBookshelf() {
|
||||
this.width = this.$refs.wrapper.clientWidth
|
||||
var booksPerRow = Math.floor(this.width / this.bookWidth)
|
||||
this.booksPerRow = booksPerRow
|
||||
},
|
||||
getAudiobookCard(id) {
|
||||
if (this.$refs[`audiobookCard-${id}`] && this.$refs[`audiobookCard-${id}`].length) {
|
||||
return this.$refs[`audiobookCard-${id}`][0]
|
||||
}
|
||||
return null
|
||||
},
|
||||
init() {
|
||||
this.calculateBookshelf()
|
||||
},
|
||||
resize() {
|
||||
this.$nextTick(() => {
|
||||
this.calculateBookshelf()
|
||||
this.setGroupedBooks()
|
||||
})
|
||||
},
|
||||
audiobooksUpdated() {
|
||||
console.log('[AudioBookshelf] Audiobooks Updated')
|
||||
this.setGroupedBooks()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$store.commit('audiobooks/addListener', { id: 'bookshelf', meth: this.audiobooksUpdated })
|
||||
this.$store.dispatch('audiobooks/load')
|
||||
this.init()
|
||||
window.addEventListener('resize', this.resize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$store.commit('audiobooks/removeListener', 'bookshelf')
|
||||
window.removeEventListener('resize', this.resize)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.bookshelfRow {
|
||||
background-image: url(/wood_panels.jpg);
|
||||
}
|
||||
.bookshelfDivider {
|
||||
background: rgb(149, 119, 90);
|
||||
background: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%);
|
||||
box-shadow: 2px 14px 8px #111111aa;
|
||||
}
|
||||
</style>
|
||||
142
client/components/app/StreamContainer.vue
Normal file
142
client/components/app/StreamContainer.vue
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
<template>
|
||||
<div v-if="streamAudiobook" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-40 z-20 bg-primary p-4">
|
||||
<div class="absolute -top-16 left-4">
|
||||
<cards-book-cover :audiobook="streamAudiobook" :width="88" />
|
||||
</div>
|
||||
<div class="flex items-center pl-24">
|
||||
<div>
|
||||
<h1>
|
||||
{{ title }} <span v-if="stream" class="text-xs text-gray-400">({{ stream.id }})</span>
|
||||
</h1>
|
||||
<p class="text-gray-400 text-sm">by {{ author }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<span v-if="stream" class="material-icons px-4 cursor-pointer" @click="cancelStream">close</span>
|
||||
</div>
|
||||
|
||||
<audio-player ref="audioPlayer" :loading="!stream" @updateTime="updateTime" @hook:mounted="audioPlayerMounted" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
lastServerUpdateSentSeconds: 0,
|
||||
stream: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
cover() {
|
||||
if (this.streamAudiobook && this.streamAudiobook.cover) return this.streamAudiobook.cover
|
||||
return 'Logo.png'
|
||||
},
|
||||
user() {
|
||||
return this.$store.state.user
|
||||
},
|
||||
streamAudiobook() {
|
||||
return this.$store.state.streamAudiobook
|
||||
},
|
||||
book() {
|
||||
return this.streamAudiobook ? this.streamAudiobook.book || {} : {}
|
||||
},
|
||||
title() {
|
||||
return this.book.title || 'No Title'
|
||||
},
|
||||
author() {
|
||||
return this.book.author || 'Unknown'
|
||||
},
|
||||
streamId() {
|
||||
return this.stream ? this.stream.id : null
|
||||
},
|
||||
playlistUrl() {
|
||||
return this.stream ? this.stream.clientPlaylistUri : null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
audioPlayerMounted() {
|
||||
if (this.stream) {
|
||||
// this.$refs.audioPlayer.set(this.playlistUrl)
|
||||
this.openStream()
|
||||
}
|
||||
},
|
||||
cancelStream() {
|
||||
this.$root.socket.emit('close_stream')
|
||||
},
|
||||
terminateStream() {
|
||||
if (this.$refs.audioPlayer) {
|
||||
this.$refs.audioPlayer.terminateStream()
|
||||
}
|
||||
},
|
||||
openStream() {
|
||||
var playOnLoad = this.$store.state.playOnLoad
|
||||
console.log(`[StreamContainer] openStream PlayOnLoad`, playOnLoad)
|
||||
if (!this.$refs.audioPlayer) {
|
||||
console.error('NO Audio Player')
|
||||
return
|
||||
}
|
||||
var currentTime = this.stream.clientCurrentTime || 0
|
||||
this.$refs.audioPlayer.set(this.playlistUrl, currentTime, playOnLoad)
|
||||
},
|
||||
streamProgress(data) {
|
||||
if (!data.numSegments) return
|
||||
var chunks = data.chunks
|
||||
if (this.$refs.audioPlayer) {
|
||||
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
|
||||
}
|
||||
},
|
||||
streamOpen(stream) {
|
||||
this.stream = stream
|
||||
this.$nextTick(() => {
|
||||
this.openStream()
|
||||
})
|
||||
},
|
||||
streamClosed(streamId) {
|
||||
if (this.stream && this.stream.id === streamId) {
|
||||
this.terminateStream()
|
||||
this.$store.commit('clearStreamAudiobook', this.stream.audiobook.id)
|
||||
}
|
||||
},
|
||||
streamReady() {
|
||||
if (this.$refs.audioPlayer) {
|
||||
this.$refs.audioPlayer.setStreamReady()
|
||||
}
|
||||
},
|
||||
updateTime(currentTime) {
|
||||
var diff = currentTime - this.lastServerUpdateSentSeconds
|
||||
if (diff > 4 || diff < 0) {
|
||||
this.lastServerUpdateSentSeconds = currentTime
|
||||
var updatePayload = {
|
||||
currentTime,
|
||||
streamId: this.streamId
|
||||
}
|
||||
this.$root.socket.emit('stream_update', updatePayload)
|
||||
}
|
||||
},
|
||||
streamReset({ startTime, streamId }) {
|
||||
if (streamId !== this.streamId) {
|
||||
console.error('resetStream StreamId Mismatch', streamId, this.streamId)
|
||||
return
|
||||
}
|
||||
if (this.$refs.audioPlayer) {
|
||||
console.log(`[STREAM-CONTAINER] streamReset Received for time ${startTime}`)
|
||||
this.$refs.audioPlayer.resetStream(startTime)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.stream) {
|
||||
console.log('[STREAM_CONTAINER] Mounted with STREAM', this.stream)
|
||||
this.$nextTick(() => {
|
||||
this.openStream()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#streamContainer {
|
||||
box-shadow: 0px -6px 8px #1111113f;
|
||||
}
|
||||
</style>
|
||||
67
client/components/app/TracksTable.vue
Normal file
67
client/components/app/TracksTable.vue
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<div class="w-full my-2">
|
||||
<div class="w-full bg-primary px-6 py-2 flex items-center cursor-pointer" @click.stop="clickBar">
|
||||
<p class="pr-4">Audio Tracks</p>
|
||||
<span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ tracks.length }}</span>
|
||||
<div class="flex-grow" />
|
||||
<nuxt-link :to="`/audiobook/${audiobookId}/edit`" class="mr-4">
|
||||
<ui-btn small color="primary">Edit Track Order</ui-btn>
|
||||
</nuxt-link>
|
||||
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''">
|
||||
<span class="material-icons text-4xl">expand_more</span>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="slide">
|
||||
<div class="w-full" v-show="showTracks">
|
||||
<table class="text-sm tracksTable">
|
||||
<tr class="font-book">
|
||||
<th>#</th>
|
||||
<th class="text-left">Filename</th>
|
||||
<th class="text-left">Size</th>
|
||||
<th class="text-left">Duration</th>
|
||||
</tr>
|
||||
<template v-for="track in tracks">
|
||||
<tr :key="track.index">
|
||||
<td class="text-center">
|
||||
<p>{{ track.index }}</p>
|
||||
</td>
|
||||
<td class="font-book">
|
||||
{{ track.filename }}
|
||||
</td>
|
||||
<td class="font-mono">
|
||||
{{ $bytesPretty(track.size) }}
|
||||
</td>
|
||||
<td class="font-mono">
|
||||
{{ $secondsToTimestamp(track.duration) }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
tracks: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
audiobookId: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showTracks: false
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
clickBar() {
|
||||
this.showTracks = !this.showTracks
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue