mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-27 22:04:08 +02:00
This commit is contained in:
+123
-24
@@ -7,8 +7,24 @@
|
|||||||
<div v-show="showCastBtn" class="top-3.5 right-20 absolute cursor-pointer">
|
<div v-show="showCastBtn" class="top-3.5 right-20 absolute cursor-pointer">
|
||||||
<span class="material-icons text-3xl" @click="castClick">cast</span>
|
<span class="material-icons text-3xl" @click="castClick">cast</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="top-3 right-4 absolute cursor-pointer">
|
<div class="top-4 right-4 absolute cursor-pointer">
|
||||||
<span class="material-icons text-4xl" @click="$emit('close')">close</span>
|
<ui-dropdown-menu :items="menuItems" @action="clickMenuAction">
|
||||||
|
<span class="material-icons text-3xl">more_vert</span>
|
||||||
|
</ui-dropdown-menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="useChapterTrack && showFullscreen" class="absolute total-track w-full px-3 z-30">
|
||||||
|
<div class="flex">
|
||||||
|
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ currentTimePretty }}</p>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ totalTimeRemainingPretty }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="h-1 w-full bg-gray-500 bg-opacity-50 relative">
|
||||||
|
<div ref="totalReadyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
||||||
|
<div ref="totalPlayedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -55,7 +71,6 @@
|
|||||||
<div id="playerTrack" class="absolute bottom-0 left-0 w-full px-3">
|
<div id="playerTrack" class="absolute bottom-0 left-0 w-full px-3">
|
||||||
<div ref="track" class="h-2 w-full bg-gray-500 bg-opacity-50 relative" :class="loading ? 'animate-pulse' : ''" @click="clickTrack">
|
<div ref="track" class="h-2 w-full bg-gray-500 bg-opacity-50 relative" :class="loading ? 'animate-pulse' : ''" @click="clickTrack">
|
||||||
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
||||||
<div ref="bufferTrack" class="h-full bg-gray-400 absolute top-0 left-0 pointer-events-none" />
|
|
||||||
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex pt-0.5">
|
<div class="flex pt-0.5">
|
||||||
@@ -110,7 +125,6 @@ export default {
|
|||||||
src: null,
|
src: null,
|
||||||
volume: 0.5,
|
volume: 0.5,
|
||||||
readyTrackWidth: 0,
|
readyTrackWidth: 0,
|
||||||
bufferTrackWidth: 0,
|
|
||||||
playedTrackWidth: 0,
|
playedTrackWidth: 0,
|
||||||
seekedTime: 0,
|
seekedTime: 0,
|
||||||
seekLoading: false,
|
seekLoading: false,
|
||||||
@@ -122,7 +136,8 @@ export default {
|
|||||||
touchEndY: 0,
|
touchEndY: 0,
|
||||||
listenTimeInterval: null,
|
listenTimeInterval: null,
|
||||||
listeningTimeSinceLastUpdate: 0,
|
listeningTimeSinceLastUpdate: 0,
|
||||||
totalListeningTimeInSession: 0
|
totalListeningTimeInSession: 0,
|
||||||
|
useChapterTrack: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -134,6 +149,20 @@ export default {
|
|||||||
this.$emit('update:playing', val)
|
this.$emit('update:playing', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
menuItems() {
|
||||||
|
var items = []
|
||||||
|
items.push({
|
||||||
|
text: 'Chapter Track',
|
||||||
|
value: 'chapter_track',
|
||||||
|
icon: this.useChapterTrack ? 'check_box' : 'check_box_outline_blank'
|
||||||
|
})
|
||||||
|
items.push({
|
||||||
|
text: 'Close Player',
|
||||||
|
value: 'close',
|
||||||
|
icon: 'close'
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
@@ -157,24 +186,43 @@ export default {
|
|||||||
},
|
},
|
||||||
currentChapter() {
|
currentChapter() {
|
||||||
if (!this.audiobook || !this.chapters.length) return null
|
if (!this.audiobook || !this.chapters.length) return null
|
||||||
return this.chapters.find((ch) => ch.start <= this.currentTime && ch.end > this.currentTime)
|
return this.chapters.find((ch) => Number(ch.start.toFixed(2)) <= this.currentTime && Number(ch.end.toFixed(2)) > this.currentTime)
|
||||||
},
|
},
|
||||||
nextChapter() {
|
nextChapter() {
|
||||||
if (!this.chapters.length) return
|
if (!this.chapters.length) return
|
||||||
return this.chapters.find((c) => c.start >= this.currentTime)
|
return this.chapters.find((c) => Number(c.start.toFixed(2)) > this.currentTime)
|
||||||
},
|
},
|
||||||
currentChapterTitle() {
|
currentChapterTitle() {
|
||||||
return this.currentChapter ? this.currentChapter.title : ''
|
return this.currentChapter ? this.currentChapter.title : ''
|
||||||
},
|
},
|
||||||
|
currentChapterDuration() {
|
||||||
|
return this.currentChapter ? this.currentChapter.end - this.currentChapter.start : this.totalDuration
|
||||||
|
},
|
||||||
downloadedCover() {
|
downloadedCover() {
|
||||||
return this.download ? this.download.cover : null
|
return this.download ? this.download.cover : null
|
||||||
},
|
},
|
||||||
totalDurationPretty() {
|
totalDurationPretty() {
|
||||||
return this.$secondsToTimestamp(this.totalDuration)
|
return this.$secondsToTimestamp(this.totalDuration)
|
||||||
},
|
},
|
||||||
|
currentTimePretty() {
|
||||||
|
return this.$secondsToTimestamp(this.currentTime)
|
||||||
|
},
|
||||||
timeRemaining() {
|
timeRemaining() {
|
||||||
|
if (this.useChapterTrack && this.currentChapter) {
|
||||||
|
var currChapTime = this.currentTime - this.currentChapter.start
|
||||||
|
return (this.currentChapterDuration - currChapTime) / this.currentPlaybackRate
|
||||||
|
}
|
||||||
|
return this.totalTimeRemaining
|
||||||
|
},
|
||||||
|
totalTimeRemaining() {
|
||||||
return (this.totalDuration - this.currentTime) / this.currentPlaybackRate
|
return (this.totalDuration - this.currentTime) / this.currentPlaybackRate
|
||||||
},
|
},
|
||||||
|
totalTimeRemainingPretty() {
|
||||||
|
if (this.totalTimeRemaining < 0) {
|
||||||
|
return this.$secondsToTimestamp(this.totalTimeRemaining * -1)
|
||||||
|
}
|
||||||
|
return '-' + this.$secondsToTimestamp(this.totalTimeRemaining)
|
||||||
|
},
|
||||||
timeRemainingPretty() {
|
timeRemainingPretty() {
|
||||||
if (this.timeRemaining < 0) {
|
if (this.timeRemaining < 0) {
|
||||||
return this.$secondsToTimestamp(this.timeRemaining * -1)
|
return this.$secondsToTimestamp(this.timeRemaining * -1)
|
||||||
@@ -262,6 +310,11 @@ export default {
|
|||||||
},
|
},
|
||||||
clickContainer() {
|
clickContainer() {
|
||||||
this.showFullscreen = true
|
this.showFullscreen = true
|
||||||
|
|
||||||
|
// Update track for total time bar if useChapterTrack is set
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateTrack()
|
||||||
|
})
|
||||||
},
|
},
|
||||||
collapseFullscreen() {
|
collapseFullscreen() {
|
||||||
this.showFullscreen = false
|
this.showFullscreen = false
|
||||||
@@ -309,7 +362,7 @@ export default {
|
|||||||
},
|
},
|
||||||
setStreamReady() {
|
setStreamReady() {
|
||||||
this.readyTrackWidth = this.trackWidth
|
this.readyTrackWidth = this.trackWidth
|
||||||
this.$refs.readyTrack.style.width = this.trackWidth + 'px'
|
this.updateReadyTrack()
|
||||||
},
|
},
|
||||||
setChunksReady(chunks, numSegments) {
|
setChunksReady(chunks, numSegments) {
|
||||||
var largestSeg = 0
|
var largestSeg = 0
|
||||||
@@ -329,7 +382,17 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.readyTrackWidth = widthReady
|
this.readyTrackWidth = widthReady
|
||||||
this.$refs.readyTrack.style.width = widthReady + 'px'
|
this.updateReadyTrack()
|
||||||
|
},
|
||||||
|
updateReadyTrack() {
|
||||||
|
if (this.useChapterTrack) {
|
||||||
|
if (this.$refs.totalReadyTrack) {
|
||||||
|
this.$refs.totalReadyTrack.style.width = this.readyTrackWidth + 'px'
|
||||||
|
}
|
||||||
|
this.$refs.readyTrack.style.width = this.trackWidth + 'px'
|
||||||
|
} else {
|
||||||
|
this.$refs.readyTrack.style.width = this.readyTrackWidth + 'px'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
updateTimestamp() {
|
updateTimestamp() {
|
||||||
var ts = this.$refs.currentTimestamp
|
var ts = this.$refs.currentTimestamp
|
||||||
@@ -337,8 +400,14 @@ export default {
|
|||||||
console.error('No timestamp el')
|
console.error('No timestamp el')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var currTimeClean = this.$secondsToTimestamp(this.currentTime)
|
var currTimeStr = ''
|
||||||
ts.innerText = currTimeClean
|
if (this.useChapterTrack && this.currentChapter) {
|
||||||
|
var currChapTime = Math.max(0, this.currentTime - this.currentChapter.start)
|
||||||
|
currTimeStr = this.$secondsToTimestamp(currChapTime)
|
||||||
|
} else {
|
||||||
|
currTimeStr = this.$secondsToTimestamp(this.currentTime)
|
||||||
|
}
|
||||||
|
ts.innerText = currTimeStr
|
||||||
},
|
},
|
||||||
timeupdate() {
|
timeupdate() {
|
||||||
if (!this.$refs.playedTrack) {
|
if (!this.$refs.playedTrack) {
|
||||||
@@ -356,16 +425,25 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.updateTimestamp()
|
this.updateTimestamp()
|
||||||
// if (this.noSyncUpdateTime) this.noSyncUpdateTime = false
|
this.updateTrack()
|
||||||
// else this.sendStreamUpdate()
|
},
|
||||||
|
updateTrack() {
|
||||||
var perc = this.currentTime / this.totalDuration
|
var percentDone = this.currentTime / this.totalDuration
|
||||||
var ptWidth = Math.round(perc * this.trackWidth)
|
var totalPercentDone = percentDone
|
||||||
|
if (this.useChapterTrack && this.currentChapter) {
|
||||||
|
var currChapTime = this.currentTime - this.currentChapter.start
|
||||||
|
percentDone = currChapTime / this.currentChapterDuration
|
||||||
|
}
|
||||||
|
var ptWidth = Math.round(percentDone * this.trackWidth)
|
||||||
if (this.playedTrackWidth === ptWidth) {
|
if (this.playedTrackWidth === ptWidth) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$refs.playedTrack.style.width = ptWidth + 'px'
|
this.$refs.playedTrack.style.width = ptWidth + 'px'
|
||||||
this.playedTrackWidth = ptWidth
|
this.playedTrackWidth = ptWidth
|
||||||
|
|
||||||
|
if (this.useChapterTrack) {
|
||||||
|
this.$refs.totalPlayedTrack.style.width = Math.round(totalPercentDone * this.trackWidth) + 'px'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
seek(time) {
|
seek(time) {
|
||||||
if (this.loading) return
|
if (this.loading) return
|
||||||
@@ -398,7 +476,12 @@ export default {
|
|||||||
|
|
||||||
var offsetX = e.offsetX
|
var offsetX = e.offsetX
|
||||||
var perc = offsetX / this.trackWidth
|
var perc = offsetX / this.trackWidth
|
||||||
var time = perc * this.totalDuration
|
var time = 0
|
||||||
|
if (this.useChapterTrack && this.currentChapter) {
|
||||||
|
time = perc * this.currentChapterDuration + this.currentChapter.start
|
||||||
|
} else {
|
||||||
|
time = perc * this.totalDuration
|
||||||
|
}
|
||||||
if (isNaN(time) || time === null) {
|
if (isNaN(time) || time === null) {
|
||||||
console.error('Invalid time', perc, time)
|
console.error('Invalid time', perc, time)
|
||||||
return
|
return
|
||||||
@@ -548,7 +631,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onMetadata(data) {
|
onMetadata(data) {
|
||||||
console.log('Native Audio On Metadata', JSON.stringify(data))
|
|
||||||
this.totalDuration = Number((data.duration / 1000).toFixed(2))
|
this.totalDuration = Number((data.duration / 1000).toFixed(2))
|
||||||
this.$emit('setTotalDuration', this.totalDuration)
|
this.$emit('setTotalDuration', this.totalDuration)
|
||||||
this.currentTime = Number((data.currentTime / 1000).toFixed(2))
|
this.currentTime = Number((data.currentTime / 1000).toFixed(2))
|
||||||
@@ -558,11 +640,11 @@ export default {
|
|||||||
this.setFromObj()
|
this.setFromObj()
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (this.stateName === 'ready_no_sync' || this.stateName === 'buffering_no_sync') this.noSyncUpdateTime = true
|
|
||||||
|
|
||||||
this.timeupdate()
|
this.timeupdate()
|
||||||
},
|
},
|
||||||
init() {
|
async init() {
|
||||||
|
this.useChapterTrack = await this.$localStore.getUseChapterTrack()
|
||||||
|
|
||||||
this.onPlayingUpdateListener = MyNativeAudio.addListener('onPlayingUpdate', this.onPlayingUpdate)
|
this.onPlayingUpdateListener = MyNativeAudio.addListener('onPlayingUpdate', this.onPlayingUpdate)
|
||||||
this.onMetadataListener = MyNativeAudio.addListener('onMetadata', this.onMetadata)
|
this.onMetadataListener = MyNativeAudio.addListener('onMetadata', this.onMetadata)
|
||||||
|
|
||||||
@@ -575,10 +657,7 @@ export default {
|
|||||||
handleGesture() {
|
handleGesture() {
|
||||||
var touchDistance = this.touchEndY - this.touchStartY
|
var touchDistance = this.touchEndY - this.touchStartY
|
||||||
if (touchDistance > 100) {
|
if (touchDistance > 100) {
|
||||||
console.log('Collapsing')
|
|
||||||
this.collapseFullscreen()
|
this.collapseFullscreen()
|
||||||
} else {
|
|
||||||
console.log('Not collapsing touch distance =', touchDistance)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
touchstart(e) {
|
touchstart(e) {
|
||||||
@@ -601,6 +680,20 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.handleGesture()
|
this.handleGesture()
|
||||||
|
},
|
||||||
|
clickMenuAction(action) {
|
||||||
|
if (action === 'chapter_track') {
|
||||||
|
this.useChapterTrack = !this.useChapterTrack
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateTimestamp()
|
||||||
|
this.updateTrack()
|
||||||
|
this.updateReadyTrack()
|
||||||
|
})
|
||||||
|
this.$localStore.setUseChapterTrack(this.useChapterTrack)
|
||||||
|
} else if (action === 'close') {
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -654,6 +747,12 @@ export default {
|
|||||||
height: 60px;
|
height: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.total-track {
|
||||||
|
bottom: 215px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.title-author-texts {
|
.title-author-texts {
|
||||||
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
||||||
transition-property: left, bottom, width, height;
|
transition-property: left, bottom, width, height;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-speed.sync="playbackSpeed" @change="changePlaybackSpeed" />
|
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-rate.sync="playbackSpeed" @update:playbackRate="updatePlaybackSpeed" @change="changePlaybackSpeed" />
|
||||||
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" />
|
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" />
|
||||||
<modals-sleep-timer-modal v-model="showSleepTimerModal" :current-time="sleepTimeRemaining" :sleep-timer-running="isSleepTimerRunning" :current-end-of-chapter-time="currentEndOfChapterTime" @change="selectSleepTimeout" @cancel="cancelSleepTimer" @increase="increaseSleepTimer" @decrease="decreaseSleepTimer" />
|
<modals-sleep-timer-modal v-model="showSleepTimerModal" :current-time="sleepTimeRemaining" :sleep-timer-running="isSleepTimerRunning" :current-end-of-chapter-time="currentEndOfChapterTime" @change="selectSleepTimeout" @cancel="cancelSleepTimer" @increase="increaseSleepTimer" @decrease="decreaseSleepTimer" />
|
||||||
<modals-bookmarks-modal v-model="showBookmarksModal" :audiobook-id="audiobookId" :bookmarks="bookmarks" :current-time="currentTime" @select="selectBookmark" />
|
<modals-bookmarks-modal v-model="showBookmarksModal" :audiobook-id="audiobookId" :bookmarks="bookmarks" :current-time="currentTime" @select="selectBookmark" />
|
||||||
@@ -411,11 +411,14 @@ export default {
|
|||||||
this.streamOpen(this.$server.stream)
|
this.streamOpen(this.$server.stream)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
changePlaybackSpeed(speed) {
|
updatePlaybackSpeed(speed) {
|
||||||
console.log(`[AudioPlayerContainer] Change Playback Speed: ${speed}`)
|
|
||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
|
console.log(`[AudioPlayerContainer] Update Playback Speed: ${speed}`)
|
||||||
this.$refs.audioPlayer.setPlaybackSpeed(speed)
|
this.$refs.audioPlayer.setPlaybackSpeed(speed)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
changePlaybackSpeed(speed) {
|
||||||
|
console.log(`[AudioPlayerContainer] Change Playback Speed: ${speed}`)
|
||||||
this.$store.dispatch('user/updateUserSettings', { playbackRate: speed })
|
this.$store.dispatch('user/updateUserSettings', { playbackRate: speed })
|
||||||
},
|
},
|
||||||
settingsUpdated(settings) {
|
settingsUpdated(settings) {
|
||||||
|
|||||||
@@ -11,10 +11,9 @@
|
|||||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="chapter in chapters">
|
<template v-for="chapter in chapters">
|
||||||
<li :key="chapter.id" :id="`chapter-row-${chapter.id}`" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" :class="currentChapterId === chapter.id ? 'bg-bg bg-opacity-80' : ''" role="option" @click="clickedOption(chapter)">
|
<li :key="chapter.id" :id="`chapter-row-${chapter.id}`" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" :class="currentChapterId === chapter.id ? 'bg-bg bg-opacity-80' : ''" role="option" @click="clickedOption(chapter)">
|
||||||
<div class="relative flex items-center justify-center pl-2 pr-16">
|
<div class="relative flex items-center pl-3" style="padding-right: 4.5rem">
|
||||||
<p class="font-normal block truncate text-sm text-white text-opacity-80">{{ chapter.title }}</p>
|
<p class="font-normal block truncate text-sm text-white text-opacity-80">{{ chapter.title }}</p>
|
||||||
<!-- <div class="flex-grow" /> -->
|
<div class="absolute top-0 right-3 -mt-0.5">
|
||||||
<div class="absolute top-0 right-2 -mt-0.5">
|
|
||||||
<span class="font-mono text-white text-opacity-90 leading-3" style="letter-spacing: -0.5px">{{ $secondsToTimestamp(chapter.start) }}</span>
|
<span class="font-mono text-white text-opacity-90 leading-3" style="letter-spacing: -0.5px">{{ $secondsToTimestamp(chapter.start) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="closeMenu">
|
||||||
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
|
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
|
||||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul class="w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="rate in rates">
|
<template v-for="rate in rates">
|
||||||
<li :key="rate" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" :class="rate === selected ? 'bg-bg bg-opacity-80' : ''" role="option" @click="clickedOption(rate)">
|
<li :key="rate" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" :class="rate === selected ? 'bg-bg bg-opacity-80' : ''" role="option" @click="clickedOption(rate)">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
@@ -17,6 +17,17 @@
|
|||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
|
<div class="flex items-center justify-center py-3 border-t border-white border-opacity-10">
|
||||||
|
<button :disabled="!canDecrement" @click="decrement" class="icon-num-btn w-8 h-8 text-white text-opacity-75 rounded border border-white border-opacity-20 flex items-center justify-center">
|
||||||
|
<span class="material-icons">remove</span>
|
||||||
|
</button>
|
||||||
|
<div class="w-24 text-center">
|
||||||
|
<p class="text-xl">{{ playbackRate }}<span class="text-lg">⨯</span></p>
|
||||||
|
</div>
|
||||||
|
<button :disabled="!canIncrement" @click="increment" class="icon-num-btn w-8 h-8 text-white text-opacity-75 rounded border border-white border-opacity-20 flex items-center justify-center">
|
||||||
|
<span class="material-icons">add</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
@@ -26,10 +37,21 @@
|
|||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: Boolean,
|
value: Boolean,
|
||||||
playbackSpeed: Number
|
playbackRate: Number
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
currentPlaybackRate: 0,
|
||||||
|
MIN_SPEED: 0.5,
|
||||||
|
MAX_SPEED: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.currentPlaybackRate = this.selected
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
show: {
|
show: {
|
||||||
@@ -42,27 +64,56 @@ export default {
|
|||||||
},
|
},
|
||||||
selected: {
|
selected: {
|
||||||
get() {
|
get() {
|
||||||
return this.playbackSpeed
|
return this.playbackRate
|
||||||
},
|
},
|
||||||
set(val) {
|
set(val) {
|
||||||
this.$emit('update:playbackSpeed', val)
|
this.$emit('update:playbackRate', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rates() {
|
rates() {
|
||||||
return [0.25, 0.5, 0.8, 1, 1.3, 1.5, 2, 2.5, 3]
|
return [0.5, 1, 1.2, 1.5, 2]
|
||||||
|
},
|
||||||
|
canIncrement() {
|
||||||
|
return this.playbackRate + 0.1 <= this.MAX_SPEED
|
||||||
|
},
|
||||||
|
canDecrement() {
|
||||||
|
return this.playbackRate - 0.1 >= this.MIN_SPEED
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clickedOption(speed) {
|
increment() {
|
||||||
if (this.selected === speed) {
|
if (this.selected + 0.1 > this.MAX_SPEED) return
|
||||||
this.show = false
|
var newPlaybackRate = this.selected + 0.1
|
||||||
return
|
this.selected = Number(newPlaybackRate.toFixed(1))
|
||||||
|
},
|
||||||
|
decrement() {
|
||||||
|
if (this.selected - 0.1 < this.MIN_SPEED) return
|
||||||
|
var newPlaybackRate = this.selected - 0.1
|
||||||
|
this.selected = Number(newPlaybackRate.toFixed(1))
|
||||||
|
},
|
||||||
|
closeMenu() {
|
||||||
|
if (this.currentPlaybackRate !== this.selected) {
|
||||||
|
this.$emit('change', this.selected)
|
||||||
}
|
}
|
||||||
this.selected = speed
|
|
||||||
this.show = false
|
this.show = false
|
||||||
this.$nextTick(() => this.$emit('change', speed))
|
},
|
||||||
|
clickedOption(rate) {
|
||||||
|
this.selected = Number(rate)
|
||||||
|
this.$nextTick(this.closeMenu)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
button.icon-num-btn:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
button.icon-num-btn:disabled::before {
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
button.icon-num-btn:disabled span {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="wrapper" v-click-outside="clickOutside">
|
||||||
|
<div @click.stop="toggleMenu">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<transition name="menu">
|
||||||
|
<ul ref="menu" v-show="showMenu" class="absolute z-50 -mt-px bg-primary border border-gray-600 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox" aria-activedescendant="listbox-option-3" style="width: 160px">
|
||||||
|
<template v-for="item in items">
|
||||||
|
<nuxt-link :key="item.value" v-if="item.to" :to="item.to">
|
||||||
|
<li :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
|
||||||
|
<div class="flex items-center px-2">
|
||||||
|
<span v-if="item.icon" class="material-icons-outlined text-lg mr-2" :class="item.iconClass ? item.iconClass : ''">{{ item.icon }}</span>
|
||||||
|
<span class="font-normal block truncate font-sans text-center">{{ item.text }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</nuxt-link>
|
||||||
|
<li v-else :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
|
||||||
|
<div class="flex items-center px-2">
|
||||||
|
<span v-if="item.icon" class="material-icons-outlined text-lg mr-2" :class="item.iconClass ? item.iconClass : ''">{{ item.icon }}</span>
|
||||||
|
<span class="font-normal block truncate font-sans text-center">{{ item.text }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
menu: null,
|
||||||
|
showMenu: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleMenu() {
|
||||||
|
if (!this.showMenu) {
|
||||||
|
this.openMenu()
|
||||||
|
} else {
|
||||||
|
this.closeMenu()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openMenu() {
|
||||||
|
this.showMenu = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (!this.menu) this.unmountMountMenu()
|
||||||
|
this.recalcMenuPos()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
closeMenu() {
|
||||||
|
this.showMenu = false
|
||||||
|
},
|
||||||
|
recalcMenuPos() {
|
||||||
|
if (!this.menu) return
|
||||||
|
var boundingBox = this.$refs.wrapper.getBoundingClientRect()
|
||||||
|
if (boundingBox.y > window.innerHeight - 8) {
|
||||||
|
// Input is off the page
|
||||||
|
return this.closeMenu()
|
||||||
|
}
|
||||||
|
var menuHeight = this.menu.clientHeight
|
||||||
|
var top = boundingBox.y + boundingBox.height - 4
|
||||||
|
if (top + menuHeight > window.innerHeight - 20) {
|
||||||
|
// Reverse menu to open upwards
|
||||||
|
top = boundingBox.y - menuHeight - 4
|
||||||
|
}
|
||||||
|
|
||||||
|
var left = boundingBox.x
|
||||||
|
if (left + this.menu.clientWidth > window.innerWidth - 20) {
|
||||||
|
// Shift left
|
||||||
|
left = boundingBox.x + boundingBox.width - this.menu.clientWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
this.menu.style.top = top + 'px'
|
||||||
|
this.menu.style.left = left + 'px'
|
||||||
|
},
|
||||||
|
unmountMountMenu() {
|
||||||
|
if (!this.$refs.menu) return
|
||||||
|
this.menu = this.$refs.menu
|
||||||
|
this.menu.remove()
|
||||||
|
document.body.appendChild(this.menu)
|
||||||
|
},
|
||||||
|
clickOutside() {
|
||||||
|
this.closeMenu()
|
||||||
|
},
|
||||||
|
clickedOption(itemValue) {
|
||||||
|
this.$emit('action', itemValue)
|
||||||
|
this.closeMenu()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -167,6 +167,24 @@ class LocalStorage {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setUseChapterTrack(useChapterTrack) {
|
||||||
|
try {
|
||||||
|
await Storage.set({ key: 'useChapterTrack', value: useChapterTrack ? '1' : '0' })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[LocalStorage] Failed to set use chapter track', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUseChapterTrack() {
|
||||||
|
try {
|
||||||
|
var obj = await Storage.get({ key: 'useChapterTrack' }) || {}
|
||||||
|
return obj.value === '1'
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[LocalStorage] Failed to get use chapter track', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user