mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-03 10:28:51 +02:00
Merge pull request #1861 from lkiesow/landscape-player
Player view in landscape mode
This commit is contained in:
+110
-30
@@ -1,20 +1,31 @@
|
||||
<template>
|
||||
<!-- Main container - only shows when playback session exists, applies fullscreen/platform classes -->
|
||||
<div v-if="playbackSession" id="streamContainer" class="fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none" :class="{ fullscreen: showFullscreen, 'ios-player': $platform === 'ios', 'web-player': $platform === 'web' }">
|
||||
<!-- Fullscreen overlays: colored background and menu overlays -->
|
||||
<div v-if="showFullscreen" class="w-full h-full z-10 absolute top-0 left-0 pointer-events-auto" :style="{ backgroundColor: coverRgb }">
|
||||
<!-- Background gradient for player -->
|
||||
<div class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: var(--gradient-audio-player)" />
|
||||
|
||||
<!-- Collapse button - minimizes player -->
|
||||
<div class="top-4 left-4 absolute cursor-pointer">
|
||||
<span class="material-symbols text-5xl" :class="{ 'text-black text-opacity-75': coverBgIsLight && theme !== 'black' }" @click="collapseFullscreen">keyboard_arrow_down</span>
|
||||
</div>
|
||||
|
||||
<!-- Cast button - Chromecast toggle -->
|
||||
<div v-show="showCastBtn" class="top-6 right-16 absolute cursor-pointer">
|
||||
<span class="material-symbols text-3xl" :class="coverBgIsLight && theme !== 'black' ? 'text-black' : ''" @click="castClick">{{ isCasting ? 'cast_connected' : 'cast' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Player options menu button -->
|
||||
<div class="top-6 right-4 absolute cursor-pointer">
|
||||
<span class="material-symbols text-3xl" :class="{ 'text-black text-opacity-75': coverBgIsLight && theme !== 'black' }" @click="showMoreMenuDialog = true">more_vert</span>
|
||||
</div>
|
||||
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" :class="{ 'text-black text-opacity-75': coverBgIsLight && theme !== 'black' }" style="font-size: 10px">{{ isDirectPlayMethod ? $strings.LabelPlaybackDirect : isLocalPlayMethod ? $strings.LabelPlaybackLocal : $strings.LabelPlaybackTranscode }}</p>
|
||||
|
||||
<!-- Playback method indicator (Direct/Local/Transcode) -->
|
||||
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75 z-50" :class="{ 'text-black text-opacity-75': coverBgIsLight && theme !== 'black' }" style="font-size: 10px">{{ isDirectPlayMethod ? $strings.LabelPlaybackDirect : isLocalPlayMethod ? $strings.LabelPlaybackLocal : $strings.LabelPlaybackTranscode }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Overall book progress bar -->
|
||||
<div v-if="playerSettings.useChapterTrack && playerSettings.useTotalTrack && showFullscreen" class="absolute total-track w-full z-30 px-6">
|
||||
<div class="flex">
|
||||
<p class="font-mono text-fg" style="font-size: 0.8rem">{{ currentTimePretty }}</p>
|
||||
@@ -22,7 +33,7 @@
|
||||
<p class="font-mono text-fg" style="font-size: 0.8rem">{{ totalTimeRemainingPretty }}</p>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="h-1 w-full bg-track/50 relative rounded-full">
|
||||
<div class="h-1 w-full bg-track/50 relative rounded-full overflow-hidden">
|
||||
<div ref="totalReadyTrack" class="h-full bg-track-buffered absolute top-0 left-0 pointer-events-none rounded-full" />
|
||||
<div ref="totalBufferedTrack" class="h-full bg-track absolute top-0 left-0 pointer-events-none rounded-full" />
|
||||
<div ref="totalPlayedTrack" class="h-full bg-track-cursor absolute top-0 left-0 pointer-events-none rounded-full" />
|
||||
@@ -30,16 +41,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Book cover wrapper -->
|
||||
<div class="cover-wrapper absolute z-30 pointer-events-auto" @click="clickContainer">
|
||||
<div class="w-full h-full flex justify-center">
|
||||
<covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" ref="cover" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" raw @imageLoaded="coverImageLoaded" />
|
||||
</div>
|
||||
|
||||
<!-- Sync failed indicator - shows error icon if progress sync failed -->
|
||||
<div v-if="syncStatus === $constants.SyncStatus.FAILED" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-30" @click.stop="showSyncsFailedDialog">
|
||||
<span class="material-symbols text-error text-3xl">error</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Title and author text -->
|
||||
<div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor">
|
||||
<div ref="titlewrapper" class="overflow-hidden relative">
|
||||
<p class="title-text whitespace-nowrap"></p>
|
||||
@@ -48,6 +62,7 @@
|
||||
</div>
|
||||
|
||||
<div id="playerContent" class="playerContainer w-full z-20 absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" :style="{ backgroundColor: showFullscreen ? '' : coverRgb }" @click="clickContainer">
|
||||
<!-- Top controls bar - fullscreen only: bookmarks, speed, sleep timer, chapters -->
|
||||
<div v-if="showFullscreen" class="absolute bottom-4 left-0 right-0 w-full pb-4 pt-2 mx-auto px-6" style="max-width: 414px">
|
||||
<div class="flex items-center justify-between pointer-events-auto">
|
||||
<span v-if="!isPodcast && serverLibraryItemId && socketConnected" class="material-symbols text-3xl text-fg-muted cursor-pointer" :class="{ fill: bookmarks.length }" @click="$emit('showBookmarks')">bookmark</span>
|
||||
@@ -67,6 +82,7 @@
|
||||
</div>
|
||||
<div v-else class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: var(--gradient-minimized-audio-player)" />
|
||||
|
||||
<!-- Playback controls - jump buttons, play/pause, chapter navigation -->
|
||||
<div id="playerControls" class="absolute right-0 bottom-0 mx-auto" style="max-width: 414px">
|
||||
<div class="flex items-center max-w-full" :class="playerSettings.lockUi ? 'justify-center' : 'justify-between'">
|
||||
<span v-show="showFullscreen && !playerSettings.lockUi" class="material-symbols next-icon text-fg cursor-pointer" :class="showLoadingState ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
|
||||
@@ -88,13 +104,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chapter progress bar -->
|
||||
<div id="playerTrack" class="absolute left-0 w-full px-6">
|
||||
<div class="flex pointer-events-none">
|
||||
<p class="font-mono text-fg" style="font-size: 0.8rem" ref="currentTimestamp">0:00</p>
|
||||
<div class="flex-grow" />
|
||||
<p class="font-mono text-fg" style="font-size: 0.8rem">{{ timeRemainingPretty }}</p>
|
||||
</div>
|
||||
<div ref="track" class="h-1.5 w-full bg-track/50 relative rounded-full" :class="{ 'animate-pulse': showLoadingState }" @click.stop>
|
||||
<div ref="track" class="h-1.5 w-full bg-track/50 relative rounded-full overflow-hidden" :class="{ 'animate-pulse': showLoadingState }" @click.stop>
|
||||
<div ref="readyTrack" class="h-full bg-track-buffered absolute top-0 left-0 rounded-full pointer-events-none" />
|
||||
<div ref="bufferedTrack" class="h-full bg-track absolute top-0 left-0 rounded-full pointer-events-none" />
|
||||
<div ref="playedTrack" class="h-full bg-track-cursor absolute top-0 left-0 rounded-full pointer-events-none" />
|
||||
@@ -105,7 +122,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chapters modal - lists chapters for navigation -->
|
||||
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" :playback-rate="currentPlaybackRate" @select="selectChapter" />
|
||||
<!-- More menu dialog - player settings and options -->
|
||||
<modals-dialog v-model="showMoreMenuDialog" :items="menuItems" width="80vw" @action="clickMenuAction" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -449,9 +468,8 @@ export default {
|
||||
this.showFullscreen = true
|
||||
if (this.titleMarquee) this.titleMarquee.reset()
|
||||
|
||||
// Update track for total time bar if useChapterTrack is set
|
||||
this.$nextTick(() => {
|
||||
this.updateTrack()
|
||||
this.measureAndUpdateTrackWidth()
|
||||
})
|
||||
},
|
||||
collapseFullscreen() {
|
||||
@@ -531,13 +549,17 @@ export default {
|
||||
this.updateReadyTrack()
|
||||
},
|
||||
updateReadyTrack() {
|
||||
if (!this.$refs.readyTrack) return
|
||||
|
||||
if (this.playerSettings.useChapterTrack) {
|
||||
if (this.$refs.totalReadyTrack) {
|
||||
this.$refs.totalReadyTrack.style.width = this.readyTrackWidth + 'px'
|
||||
if (this.$refs.totalReadyTrack && this.trackWidth) {
|
||||
const readyPercent = (this.readyTrackWidth / this.trackWidth) * 100
|
||||
this.$refs.totalReadyTrack.style.width = readyPercent + '%'
|
||||
}
|
||||
this.$refs.readyTrack.style.width = this.trackWidth + 'px'
|
||||
} else {
|
||||
this.$refs.readyTrack.style.width = this.readyTrackWidth + 'px'
|
||||
this.$refs.readyTrack.style.width = '100%'
|
||||
} else if (this.trackWidth) {
|
||||
const readyPercent = (this.readyTrackWidth / this.trackWidth) * 100
|
||||
this.$refs.readyTrack.style.width = readyPercent + '%'
|
||||
}
|
||||
},
|
||||
updateTimestamp() {
|
||||
@@ -589,21 +611,23 @@ export default {
|
||||
bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration))
|
||||
}
|
||||
|
||||
const ptWidth = Math.round(percentDone * this.trackWidth)
|
||||
const playedPercent = percentDone * 100
|
||||
const bufferedPercentWidth = bufferedPercent * 100
|
||||
if (this.$refs.playedTrack) {
|
||||
this.$refs.playedTrack.style.width = ptWidth + 'px'
|
||||
this.$refs.playedTrack.style.width = playedPercent + '%'
|
||||
}
|
||||
if (this.$refs.bufferedTrack) {
|
||||
this.$refs.bufferedTrack.style.width = Math.round(bufferedPercent * this.trackWidth) + 'px'
|
||||
this.$refs.bufferedTrack.style.width = bufferedPercentWidth + '%'
|
||||
}
|
||||
|
||||
if (this.$refs.trackCursor) {
|
||||
this.$refs.trackCursor.style.left = ptWidth - 14 + 'px'
|
||||
if (this.$refs.trackCursor && this.$refs.track) {
|
||||
const trackWidth = this.$refs.track.clientWidth
|
||||
this.$refs.trackCursor.style.left = Math.round(percentDone * trackWidth) - 14 + 'px'
|
||||
}
|
||||
|
||||
if (this.playerSettings.useChapterTrack) {
|
||||
if (this.$refs.totalPlayedTrack) this.$refs.totalPlayedTrack.style.width = Math.round(totalPercentDone * this.trackWidth) + 'px'
|
||||
if (this.$refs.totalBufferedTrack) this.$refs.totalBufferedTrack.style.width = Math.round(totalBufferedPercent * this.trackWidth) + 'px'
|
||||
if (this.$refs.totalPlayedTrack) this.$refs.totalPlayedTrack.style.width = totalPercentDone * 100 + '%'
|
||||
if (this.$refs.totalBufferedTrack) this.$refs.totalBufferedTrack.style.width = totalBufferedPercent * 100 + '%'
|
||||
}
|
||||
},
|
||||
seek(time) {
|
||||
@@ -621,8 +645,7 @@ export default {
|
||||
|
||||
if (this.$refs.playedTrack) {
|
||||
const perc = time / this.totalDuration
|
||||
const ptWidth = Math.round(perc * this.trackWidth)
|
||||
this.$refs.playedTrack.style.width = ptWidth + 'px'
|
||||
this.$refs.playedTrack.style.width = perc * 100 + '%'
|
||||
|
||||
this.$refs.playedTrack.classList.remove('bg-gray-200')
|
||||
this.$refs.playedTrack.classList.add('bg-yellow-300')
|
||||
@@ -731,7 +754,8 @@ export default {
|
||||
maxTime = minTime + duration
|
||||
}
|
||||
|
||||
const timePerPixel = duration / this.trackWidth
|
||||
const trackWidth = this.$refs.track?.clientWidth || this.trackWidth
|
||||
const timePerPixel = duration / trackWidth
|
||||
const newTime = this.draggingTouchStartTime + timePerPixel * distanceMoved
|
||||
this.draggingCurrentTime = Math.min(maxTime, Math.max(minTime, newTime))
|
||||
|
||||
@@ -869,11 +893,7 @@ export default {
|
||||
this.titleMarquee = new WrappingMarquee(this.$refs.titlewrapper)
|
||||
this.titleMarquee.init(this.title)
|
||||
|
||||
if (this.$refs.track) {
|
||||
this.trackWidth = this.$refs.track.clientWidth
|
||||
} else {
|
||||
console.error('Track not loaded', this.$refs)
|
||||
}
|
||||
this.measureAndUpdateTrackWidth()
|
||||
})
|
||||
},
|
||||
onPlaybackClosed() {
|
||||
@@ -919,15 +939,26 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// playerContainer width transition is 150ms, remeasure after layout settles
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
this.refreshUI()
|
||||
|
||||
this.isRefreshingUI = false
|
||||
},
|
||||
measureAndUpdateTrackWidth() {
|
||||
if (!this.$refs.track) {
|
||||
console.error('Track not loaded', this.$refs)
|
||||
return
|
||||
}
|
||||
this.trackWidth = this.$refs.track.clientWidth
|
||||
this.updateTrack()
|
||||
this.updateReadyTrack()
|
||||
},
|
||||
refreshUI() {
|
||||
this.updateScreenSize()
|
||||
if (this.$refs.track) {
|
||||
this.trackWidth = this.$refs.track.clientWidth
|
||||
this.updateTrack()
|
||||
this.updateReadyTrack()
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.measureAndUpdateTrackWidth()
|
||||
})
|
||||
},
|
||||
updateScreenSize() {
|
||||
setTimeout(() => {
|
||||
@@ -1018,6 +1049,12 @@ export default {
|
||||
.fullscreen .playerContainer {
|
||||
height: 200px;
|
||||
}
|
||||
@media (orientation: landscape) {
|
||||
.fullscreen .playerContainer {
|
||||
width: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
}
|
||||
#playerContent {
|
||||
box-shadow: 0px -8px 8px #11111155;
|
||||
}
|
||||
@@ -1029,6 +1066,7 @@ export default {
|
||||
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
||||
transition-property: margin;
|
||||
bottom: 35px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.fullscreen #playerTrack {
|
||||
bottom: unset;
|
||||
@@ -1045,13 +1083,45 @@ export default {
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@media (orientation: landscape) {
|
||||
.fullscreen .cover-wrapper {
|
||||
left: 25px !important;
|
||||
top: 50px;
|
||||
width: calc(50% - 25px) !important;
|
||||
height: calc(100% - 85px) !important;
|
||||
}
|
||||
.fullscreen .cover-wrapper > div > div {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
max-width: 100% !important;
|
||||
max-height: 100% !important;
|
||||
}
|
||||
.fullscreen .cover-wrapper > div > div > div {
|
||||
background-color: transparent;
|
||||
}
|
||||
.fullscreen .cover-wrapper img {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.total-track {
|
||||
bottom: 215px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
@media (orientation: landscape) {
|
||||
.fullscreen .total-track {
|
||||
left: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: landscape) {
|
||||
.fullscreen .title-author-texts {
|
||||
left: 50% !important;
|
||||
width: 50% !important;
|
||||
}
|
||||
}
|
||||
.title-author-texts {
|
||||
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
||||
transition-property: left, bottom, width, height;
|
||||
@@ -1068,12 +1138,22 @@ export default {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@media (orientation: landscape) {
|
||||
.fullscreen .title-author-texts .title-text {
|
||||
font-size: larger !important;
|
||||
}
|
||||
}
|
||||
.title-author-texts .author-text {
|
||||
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
||||
transition-property: font-size;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
@media (orientation: landscape) {
|
||||
.fullscreen .title-author-texts .author-text {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
.fullscreen .title-author-texts {
|
||||
bottom: calc(50% - var(--cover-image-height) / 2 + 50px);
|
||||
|
||||
Reference in New Issue
Block a user