mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-27 05:43:58 +02:00
Merge branch 'master' into ios-downloads
This commit is contained in:
@@ -56,6 +56,9 @@ export default {
|
||||
},
|
||||
altViewEnabled() {
|
||||
return this.$store.getters['getAltViewEnabled']
|
||||
},
|
||||
localMediaProgress() {
|
||||
return this.$store.state.globals.localMediaProgress
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -69,6 +72,7 @@ export default {
|
||||
var podcasts = []
|
||||
localMedia.forEach((item) => {
|
||||
if (item.mediaType == 'book') {
|
||||
item.progress = this.localMediaProgress.find((lmp) => lmp.id === item.id)
|
||||
books.push(item)
|
||||
} else if (item.mediaType == 'podcast') {
|
||||
podcasts.push(item)
|
||||
@@ -80,7 +84,11 @@ export default {
|
||||
id: 'local-books',
|
||||
label: 'Local Books',
|
||||
type: 'book',
|
||||
entities: books.slice(0, 10)
|
||||
entities: books.sort((a, b) => {
|
||||
if (a.progress && a.progress.isFinished) return 1
|
||||
else if (b.progress && b.progress.isFinished) return -1
|
||||
return 0
|
||||
})
|
||||
})
|
||||
}
|
||||
if (podcasts.length) {
|
||||
@@ -88,7 +96,7 @@ export default {
|
||||
id: 'local-podcasts',
|
||||
label: 'Local Podcasts',
|
||||
type: 'podcast',
|
||||
entities: podcasts.slice(0, 10)
|
||||
entities: podcasts
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+61
-13
@@ -9,20 +9,21 @@
|
||||
</div>
|
||||
<div class="title-container flex-grow pl-2">
|
||||
<div class="flex relative pr-6">
|
||||
<h1 class="text-base">{{ title }}</h1>
|
||||
<h1 class="text-base font-semibold">{{ title }}</h1>
|
||||
|
||||
<button class="absolute top-0 right-0 h-full px-1 outline-none" @click="moreButtonPress">
|
||||
<span class="material-icons text-xl">more_vert</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="subtitle" class="text-gray-100 text-sm py-0.5">{{ subtitle }}</p>
|
||||
<p v-if="seriesList && seriesList.length" class="text-sm text-gray-300 py-0.5">
|
||||
<template v-for="(series, index) in seriesList"
|
||||
><nuxt-link :key="series.id" :to="`/bookshelf/series/${series.id}`">{{ series.text }}</nuxt-link
|
||||
><span :key="`${series.id}-comma`" v-if="index < seriesList.length - 1">, </span></template
|
||||
>
|
||||
</p>
|
||||
<p v-if="podcastAuthor" class="text-sm text-gray-400 py-0.5">By {{ podcastAuthor }}</p>
|
||||
<p v-else-if="bookAuthors && bookAuthors.length" class="text-sm text-gray-400 py-0.5">
|
||||
<p v-if="podcastAuthor" class="text-sm text-gray-300 py-0.5">By {{ podcastAuthor }}</p>
|
||||
<p v-else-if="bookAuthors && bookAuthors.length" class="text-sm text-gray-300 py-0.5">
|
||||
By
|
||||
<template v-for="(author, index) in bookAuthors"
|
||||
><nuxt-link :key="author.id" :to="`/bookshelf/library?filter=authors.${$encode(author.id)}`">{{ author.name }}</nuxt-link
|
||||
@@ -84,7 +85,7 @@
|
||||
<div v-else-if="(user && (showPlay || showRead)) || hasLocal" class="flex mt-4">
|
||||
<ui-btn v-if="showPlay" color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick">
|
||||
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
|
||||
<span class="px-1 text-sm">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : hasLocal ? 'Play' : 'Stream' }}</span>
|
||||
<span class="px-1 text-sm">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : isPodcast ? 'Next Episode' : hasLocal ? 'Play' : 'Stream' }}</span>
|
||||
</ui-btn>
|
||||
<ui-btn v-if="showRead && user" color="info" class="flex items-center justify-center mr-2" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
|
||||
<span class="material-icons">auto_stories</span>
|
||||
@@ -218,6 +219,9 @@ export default {
|
||||
title() {
|
||||
return this.mediaMetadata.title
|
||||
},
|
||||
subtitle() {
|
||||
return this.mediaMetadata.subtitle
|
||||
},
|
||||
podcastAuthor() {
|
||||
if (!this.isPodcast) return null
|
||||
return this.mediaMetadata.author || ''
|
||||
@@ -301,13 +305,13 @@ export default {
|
||||
return this.libraryItem.isIncomplete
|
||||
},
|
||||
showPlay() {
|
||||
return !this.isMissing && !this.isIncomplete && this.numTracks
|
||||
return !this.isMissing && !this.isIncomplete && (this.numTracks || this.episodes.length)
|
||||
},
|
||||
showRead() {
|
||||
return this.ebookFile && this.ebookFormat !== 'pdf'
|
||||
},
|
||||
showDownload() {
|
||||
// if (this.isIos) return false
|
||||
if (this.isPodcast) return false
|
||||
return this.user && this.userCanDownload && this.showPlay && !this.hasLocal
|
||||
},
|
||||
ebookFile() {
|
||||
@@ -364,14 +368,58 @@ export default {
|
||||
this.$store.commit('openReader', this.libraryItem)
|
||||
},
|
||||
playClick() {
|
||||
// Todo: Allow playing local or streaming
|
||||
if (this.hasLocal && this.serverLibraryItemId && this.isCasting) {
|
||||
// If casting and connected to server for local library item then send server library item id
|
||||
this.$eventBus.$emit('play-item', { libraryItemId: this.serverLibraryItemId })
|
||||
return
|
||||
var episodeId = null
|
||||
|
||||
if (this.isPodcast) {
|
||||
this.episodes.sort((a, b) => {
|
||||
return String(b.publishedAt).localeCompare(String(a.publishedAt), undefined, { numeric: true, sensitivity: 'base' })
|
||||
})
|
||||
|
||||
var episode = this.episodes.find((ep) => {
|
||||
var podcastProgress = null
|
||||
if (!this.isLocal) {
|
||||
podcastProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, ep.id)
|
||||
} else {
|
||||
podcastProgress = this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, ep.id)
|
||||
}
|
||||
return !podcastProgress || !podcastProgress.isFinished
|
||||
})
|
||||
|
||||
if (!episode) episode = this.episodes[0]
|
||||
|
||||
episodeId = episode.id
|
||||
|
||||
var localEpisode = null
|
||||
if (this.hasLocal && !this.isLocal) {
|
||||
localEpisode = this.localLibraryItem.media.episodes.find((ep) => ep.serverEpisodeId == episodeId)
|
||||
} else if (this.isLocal) {
|
||||
localEpisode = episode
|
||||
}
|
||||
var serverEpisodeId = !this.isLocal ? episodeId : localEpisode ? localEpisode.serverEpisodeId : null
|
||||
|
||||
if (serverEpisodeId && this.serverLibraryItemId && this.isCasting) {
|
||||
// If casting and connected to server for local library item then send server library item id
|
||||
this.$eventBus.$emit('play-item', { libraryItemId: this.serverLibraryItemId, episodeId: serverEpisodeId })
|
||||
return
|
||||
}
|
||||
if (localEpisode) {
|
||||
this.$eventBus.$emit('play-item', { libraryItemId: this.localLibraryItem.id, episodeId: localEpisode.id, serverLibraryItemId: this.serverLibraryItemId, serverEpisodeId })
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Audiobook
|
||||
if (this.hasLocal && this.serverLibraryItemId && this.isCasting) {
|
||||
// If casting and connected to server for local library item then send server library item id
|
||||
this.$eventBus.$emit('play-item', { libraryItemId: this.serverLibraryItemId })
|
||||
return
|
||||
}
|
||||
if (this.hasLocal) {
|
||||
this.$eventBus.$emit('play-item', { libraryItemId: this.localLibraryItem.id, serverLibraryItemId: this.serverLibraryItemId })
|
||||
return
|
||||
}
|
||||
}
|
||||
if (this.hasLocal) return this.$eventBus.$emit('play-item', { libraryItemId: this.localLibraryItem.id, serverLibraryItemId: this.serverLibraryItemId })
|
||||
this.$eventBus.$emit('play-item', { libraryItemId: this.libraryItemId })
|
||||
|
||||
this.$eventBus.$emit('play-item', { libraryItemId: this.libraryItemId, episodeId })
|
||||
},
|
||||
async clearProgressClick() {
|
||||
const { value } = await Dialog.confirm({
|
||||
|
||||
+41
-4
@@ -1,16 +1,19 @@
|
||||
<template>
|
||||
<div class="w-full h-full p-8">
|
||||
<p class="uppercase text-xs font-semibold text-gray-300 mb-2">Display Settings</p>
|
||||
<div class="flex items-center py-3" @click="toggleEnableAltView">
|
||||
<div class="w-10 flex justify-center">
|
||||
<ui-toggle-switch v-model="settings.enableAltView" @input="saveSettings" />
|
||||
</div>
|
||||
<p class="pl-4">Alternative Bookshelf View</p>
|
||||
<p class="pl-4">Alternative bookshelf view</p>
|
||||
</div>
|
||||
<div v-if="$platform !== 'ios'" class="flex items-center py-3" @click="toggleDisableAutoRewind">
|
||||
|
||||
<p class="uppercase text-xs font-semibold text-gray-300 mb-2 mt-6">Playback Settings</p>
|
||||
<div v-if="!isiOS" class="flex items-center py-3" @click="toggleDisableAutoRewind">
|
||||
<div class="w-10 flex justify-center">
|
||||
<ui-toggle-switch v-model="settings.disableAutoRewind" @input="saveSettings" />
|
||||
</div>
|
||||
<p class="pl-4">Disable Auto Rewind</p>
|
||||
<p class="pl-4">Disable auto rewind</p>
|
||||
</div>
|
||||
<div class="flex items-center py-3" @click="toggleJumpBackwards">
|
||||
<div class="w-10 flex justify-center">
|
||||
@@ -24,10 +27,21 @@
|
||||
</div>
|
||||
<p class="pl-4">Jump forwards time</p>
|
||||
</div>
|
||||
|
||||
<p v-if="!isiOS" class="uppercase text-xs font-semibold text-gray-300 mb-2 mt-6">Sleep Timer Settings</p>
|
||||
<div v-if="!isiOS" class="flex items-center py-3" @click="toggleDisableShakeToResetSleepTimer">
|
||||
<div class="w-10 flex justify-center">
|
||||
<ui-toggle-switch v-model="settings.disableShakeToResetSleepTimer" @input="saveSettings" />
|
||||
</div>
|
||||
<p class="pl-4">Disable shake to reset</p>
|
||||
<span class="material-icons-outlined ml-2" @click.stop="showInfo('disableShakeToResetSleepTimer')">info</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Dialog } from '@capacitor/dialog'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -36,11 +50,21 @@ export default {
|
||||
disableAutoRewind: false,
|
||||
enableAltView: false,
|
||||
jumpForwardTime: 10,
|
||||
jumpBackwardsTime: 10
|
||||
jumpBackwardsTime: 10,
|
||||
disableShakeToResetSleepTimer: false
|
||||
},
|
||||
settingInfo: {
|
||||
disableShakeToResetSleepTimer: {
|
||||
name: 'Disable shake to reset sleep timer',
|
||||
message: 'The sleep timer will start fading out when 30s is remaining. Shaking your device will reset the timer if it is within 30s OR has finished less than 2 mintues ago. Enable this setting to disable that feature.'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isiOS() {
|
||||
return this.$platform === 'ios'
|
||||
},
|
||||
jumpForwardItems() {
|
||||
return this.$store.state.globals.jumpForwardItems || []
|
||||
},
|
||||
@@ -63,6 +87,18 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showInfo(setting) {
|
||||
if (this.settingInfo[setting]) {
|
||||
Dialog.alert({
|
||||
title: this.settingInfo[setting].name,
|
||||
message: this.settingInfo[setting].message
|
||||
})
|
||||
}
|
||||
},
|
||||
toggleDisableShakeToResetSleepTimer() {
|
||||
this.settings.disableShakeToResetSleepTimer = !this.settings.disableShakeToResetSleepTimer
|
||||
this.saveSettings()
|
||||
},
|
||||
toggleDisableAutoRewind() {
|
||||
this.settings.disableAutoRewind = !this.settings.disableAutoRewind
|
||||
this.saveSettings()
|
||||
@@ -99,6 +135,7 @@ export default {
|
||||
this.settings.enableAltView = !!deviceSettings.enableAltView
|
||||
this.settings.jumpForwardTime = deviceSettings.jumpForwardTime || 10
|
||||
this.settings.jumpBackwardsTime = deviceSettings.jumpBackwardsTime || 10
|
||||
this.settings.disableShakeToResetSleepTimer = !!deviceSettings.disableShakeToResetSleepTimer
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div class="w-full h-full px-0 py-4 overflow-y-auto">
|
||||
<h1 class="text-xl px-4">
|
||||
Stats for <b>{{ username }}</b>
|
||||
</h1>
|
||||
|
||||
<div class="flex text-center justify-center">
|
||||
<div class="flex p-2">
|
||||
<div class="px-3">
|
||||
<p class="text-4xl md:text-5xl font-bold">{{ userItemsFinished.length }}</p>
|
||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Items Finished</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex p-2">
|
||||
<div class="px-1">
|
||||
<p class="text-4xl md:text-5xl font-bold">{{ totalDaysListened }}</p>
|
||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Days Listened</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex p-2">
|
||||
<div class="px-1">
|
||||
<p class="text-4xl md:text-5xl font-bold">{{ totalMinutesListening }}</p>
|
||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Minutes Listening</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row overflow-hidden max-w-full">
|
||||
<stats-daily-listening-chart :listening-stats="listeningStats" class="lg:scale-100 transform scale-90 px-0" />
|
||||
<div class="w-80 my-6 mx-auto">
|
||||
<div class="flex mb-4 items-center">
|
||||
<h1 class="text-2xl font-book">Recent Sessions</h1>
|
||||
<div class="flex-grow" />
|
||||
</div>
|
||||
<p v-if="!mostRecentListeningSessions.length">No Listening Sessions</p>
|
||||
<template v-for="(item, index) in mostRecentListeningSessions">
|
||||
<div :key="item.id" class="w-full py-0.5">
|
||||
<div class="flex items-center mb-1">
|
||||
<p class="text-sm font-book text-white text-opacity-70 w-8">{{ index + 1 }}. </p>
|
||||
<div class="w-56">
|
||||
<p class="text-sm font-book text-white text-opacity-80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
|
||||
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div class="w-18 text-right">
|
||||
<p class="text-sm font-bold">{{ $elapsedPretty(item.timeListening) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
listeningStats: null,
|
||||
windowWidth: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentLibraryId(newVal) {
|
||||
if (newVal) {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
user() {
|
||||
return this.$store.state.user.user
|
||||
},
|
||||
username() {
|
||||
return this.user ? this.user.username : ''
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
userMediaProgress() {
|
||||
return this.user ? this.user.mediaProgress : []
|
||||
},
|
||||
userItemsFinished() {
|
||||
return this.userMediaProgress.filter((lip) => !!lip.isFinished)
|
||||
},
|
||||
mostRecentListeningSessions() {
|
||||
if (!this.listeningStats) return []
|
||||
return this.listeningStats.recentSessions || []
|
||||
},
|
||||
totalMinutesListening() {
|
||||
if (!this.listeningStats) return 0
|
||||
return Math.round(this.listeningStats.totalTime / 60)
|
||||
},
|
||||
totalDaysListened() {
|
||||
if (!this.listeningStats) return 0
|
||||
return Object.values(this.listeningStats.days).length
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async init() {
|
||||
this.listeningStats = await this.$axios.$get(`/api/me/listening-stats`).catch((err) => {
|
||||
console.error('Failed to load listening sesions', err)
|
||||
return []
|
||||
})
|
||||
console.log('Loaded user listening data', this.listeningStats)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user