mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-25 04:44:11 +02:00
Rotating your phone to landscape mode and opening the player, the view wasn't using the available space very efficiently. Especially the cover image is not very helpful being pushed to a minimal size. This patch redesigned the view in landscape mode, pushing the cover image to the left half of the screen while the controls are moved to the right half of the screen.
1212 lines
45 KiB
Vue
1212 lines
45 KiB
Vue
<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>
|
|
|
|
<!-- 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>
|
|
<div class="flex-grow" />
|
|
<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 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" />
|
|
</div>
|
|
</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>
|
|
</div>
|
|
<p class="author-text text-fg text-opacity-75 truncate">{{ authorName }}</p>
|
|
</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>
|
|
<!-- hidden for podcasts but still using this as a placeholder -->
|
|
<span v-else class="material-symbols text-3xl text-white text-opacity-0">bookmark</span>
|
|
|
|
<span class="font-mono text-fg-muted cursor-pointer" style="font-size: 1.35rem" @click="$emit('selectPlaybackSpeed')">{{ currentPlaybackRate }}x</span>
|
|
<svg v-if="!sleepTimerRunning" xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 text-fg-muted cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor" @click.stop="$emit('showSleepTimer')">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
|
</svg>
|
|
<div v-else class="h-7 w-7 flex items-center justify-around cursor-pointer" @click.stop="$emit('showSleepTimer')">
|
|
<p class="text-xl font-mono text-success">{{ sleepTimeRemainingPretty }}</p>
|
|
</div>
|
|
|
|
<span class="material-symbols text-3xl text-fg cursor-pointer" :class="chapters.length ? 'text-opacity-75' : 'text-opacity-10'" @click="clickChaptersBtn">format_list_bulleted</span>
|
|
</div>
|
|
</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="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
|
|
<div v-show="!playerSettings.lockUi" class="jump-icon text-fg cursor-pointer flex flex-col items-center" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">
|
|
<span class="material-symbols text-3xl leading-none">replay</span>
|
|
<span v-if="showFullscreen" class="jump-label text-[10px] font-semibold leading-tight">{{ jumpBackwardsLabel }}</span>
|
|
</div>
|
|
<div class="play-btn cursor-pointer shadow-sm flex items-center justify-center rounded-full text-primary mx-4 relative overflow-hidden" :style="{ backgroundColor: coverRgb }" :class="{ 'animate-spin': seekLoading }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
|
|
<div v-if="!coverBgIsLight" class="absolute top-0 left-0 w-full h-full bg-white bg-opacity-20 pointer-events-none" />
|
|
|
|
<span v-if="!isLoading" class="material-symbols fill" :class="{ 'text-white': coverRgb && !coverBgIsLight }">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span>
|
|
<widgets-spinner-icon v-else class="h-8 w-8" />
|
|
</div>
|
|
<div v-show="!playerSettings.lockUi" class="jump-icon text-fg cursor-pointer flex flex-col items-center" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">
|
|
<span class="material-symbols text-3xl leading-none">forward_media</span>
|
|
<span v-if="showFullscreen" class="jump-label text-[10px] font-semibold leading-tight">{{ jumpForwardLabel }}</span>
|
|
</div>
|
|
<span v-show="showFullscreen && !playerSettings.lockUi" class="material-symbols next-icon text-fg cursor-pointer" :class="nextChapter && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
|
|
</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': isLoading }" @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" />
|
|
<div ref="trackCursor" class="h-7 w-7 rounded-full absolute pointer-events-auto flex items-center justify-center" :style="{ top: '-11px' }" :class="{ 'opacity-0': playerSettings.lockUi || !showFullscreen }" @touchstart="touchstartCursor">
|
|
<div class="bg-track-cursor rounded-full w-3.5 h-3.5 pointer-events-none" />
|
|
</div>
|
|
</div>
|
|
</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>
|
|
|
|
<script>
|
|
import { Capacitor } from '@capacitor/core'
|
|
import { AbsAudioPlayer } from '@/plugins/capacitor'
|
|
import { Dialog } from '@capacitor/dialog'
|
|
import { FastAverageColor } from 'fast-average-color'
|
|
import WrappingMarquee from '@/assets/WrappingMarquee.js'
|
|
import jumpLabelMixin from '@/mixins/jumpLabel'
|
|
|
|
export default {
|
|
props: {
|
|
bookmarks: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
sleepTimerRunning: Boolean,
|
|
sleepTimeRemaining: Number,
|
|
serverLibraryItemId: String
|
|
},
|
|
mixins: [jumpLabelMixin],
|
|
data() {
|
|
return {
|
|
windowHeight: 0,
|
|
windowWidth: 0,
|
|
playbackSession: null,
|
|
showChapterModal: false,
|
|
showFullscreen: false,
|
|
totalDuration: 0,
|
|
currentPlaybackRate: 1,
|
|
currentTime: 0,
|
|
bufferedTime: 0,
|
|
playInterval: null,
|
|
trackWidth: 0,
|
|
isPlaying: false,
|
|
isEnded: false,
|
|
volume: 0.5,
|
|
readyTrackWidth: 0,
|
|
seekedTime: 0,
|
|
seekLoading: false,
|
|
touchStartY: 0,
|
|
touchStartTime: 0,
|
|
playerSettings: {
|
|
useChapterTrack: false,
|
|
useTotalTrack: true,
|
|
scaleElapsedTimeBySpeed: true,
|
|
lockUi: false
|
|
},
|
|
isLoading: false,
|
|
isDraggingCursor: false,
|
|
draggingTouchStartX: 0,
|
|
draggingTouchStartTime: 0,
|
|
draggingCurrentTime: 0,
|
|
syncStatus: 0,
|
|
showMoreMenuDialog: false,
|
|
coverRgb: 'rgb(55, 56, 56)',
|
|
coverBgIsLight: false,
|
|
titleMarquee: null,
|
|
isRefreshingUI: false
|
|
}
|
|
},
|
|
watch: {
|
|
showFullscreen(val) {
|
|
this.updateScreenSize()
|
|
this.$store.commit('setPlayerFullscreen', !!val)
|
|
document.querySelector('body').style.backgroundColor = this.showFullscreen ? this.coverRgb : ''
|
|
},
|
|
bookCoverAspectRatio() {
|
|
this.updateScreenSize()
|
|
},
|
|
title(val) {
|
|
if (this.titleMarquee) this.titleMarquee.init(val)
|
|
}
|
|
},
|
|
computed: {
|
|
theme() {
|
|
return document.documentElement.dataset.theme || 'dark'
|
|
},
|
|
menuItems() {
|
|
const items = []
|
|
// TODO: Implement on iOS
|
|
if (this.$platform !== 'ios' && !this.isPodcast && this.mediaId) {
|
|
items.push({
|
|
text: this.$strings.ButtonHistory,
|
|
value: 'history',
|
|
icon: 'history'
|
|
})
|
|
}
|
|
|
|
items.push(
|
|
...[
|
|
{
|
|
text: this.$strings.LabelTotalTrack,
|
|
value: 'total_track',
|
|
icon: this.playerSettings.useTotalTrack ? 'check_box' : 'check_box_outline_blank'
|
|
},
|
|
{
|
|
text: this.$strings.LabelChapterTrack,
|
|
value: 'chapter_track',
|
|
icon: this.playerSettings.useChapterTrack ? 'check_box' : 'check_box_outline_blank'
|
|
},
|
|
{
|
|
text: this.$strings.LabelScaleElapsedTimeBySpeed,
|
|
value: 'scale_elapsed_time',
|
|
icon: this.playerSettings.scaleElapsedTimeBySpeed ? 'check_box' : 'check_box_outline_blank'
|
|
},
|
|
{
|
|
text: this.playerSettings.lockUi ? this.$strings.LabelUnlockPlayer : this.$strings.LabelLockPlayer,
|
|
value: 'lock',
|
|
icon: this.playerSettings.lockUi ? 'lock' : 'lock_open'
|
|
},
|
|
{
|
|
text: this.$strings.LabelClosePlayer,
|
|
value: 'close',
|
|
icon: 'close'
|
|
}
|
|
]
|
|
)
|
|
|
|
return items
|
|
},
|
|
jumpForwardLabel() {
|
|
return this.getJumpLabel(this.jumpForwardTime)
|
|
},
|
|
jumpBackwardsLabel() {
|
|
return this.getJumpLabel(this.jumpBackwardsTime)
|
|
},
|
|
jumpForwardTime() {
|
|
return this.$store.getters['getJumpForwardTime']
|
|
},
|
|
jumpBackwardsTime() {
|
|
return this.$store.getters['getJumpBackwardsTime']
|
|
},
|
|
bookCoverAspectRatio() {
|
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
|
},
|
|
bookCoverWidth() {
|
|
if (this.showFullscreen) return this.fullscreenBookCoverWidth
|
|
return 46 / this.bookCoverAspectRatio
|
|
},
|
|
fullscreenBookCoverWidth() {
|
|
if (this.windowWidth < this.windowHeight) {
|
|
// Portrait
|
|
let sideSpace = 20
|
|
if (this.bookCoverAspectRatio === 1.6) sideSpace += (this.windowWidth - sideSpace) * 0.375
|
|
|
|
const availableHeight = this.windowHeight - 400
|
|
let width = this.windowWidth - sideSpace
|
|
const totalHeight = width * this.bookCoverAspectRatio
|
|
if (totalHeight > availableHeight) {
|
|
width = availableHeight / this.bookCoverAspectRatio
|
|
}
|
|
return width
|
|
} else {
|
|
// Landscape
|
|
const heightScale = (this.windowHeight - 200) / 651
|
|
if (this.bookCoverAspectRatio === 1) {
|
|
return 260 * heightScale
|
|
}
|
|
return 190 * heightScale
|
|
}
|
|
},
|
|
showCastBtn() {
|
|
return this.$store.state.isCastAvailable
|
|
},
|
|
isCasting() {
|
|
return this.mediaPlayer === 'cast-player'
|
|
},
|
|
mediaPlayer() {
|
|
return this.playbackSession?.mediaPlayer || null
|
|
},
|
|
mediaType() {
|
|
return this.playbackSession?.mediaType || null
|
|
},
|
|
isPodcast() {
|
|
return this.mediaType === 'podcast'
|
|
},
|
|
mediaMetadata() {
|
|
return this.playbackSession?.mediaMetadata || null
|
|
},
|
|
libraryItem() {
|
|
return this.playbackSession?.libraryItem || null
|
|
},
|
|
localLibraryItem() {
|
|
return this.playbackSession?.localLibraryItem || null
|
|
},
|
|
localLibraryItemCoverSrc() {
|
|
var localItemCover = this.localLibraryItem?.coverContentUrl || null
|
|
if (localItemCover) return Capacitor.convertFileSrc(localItemCover)
|
|
return null
|
|
},
|
|
playMethod() {
|
|
return this.playbackSession?.playMethod || 0
|
|
},
|
|
isLocalPlayMethod() {
|
|
return this.playMethod == this.$constants.PlayMethod.LOCAL
|
|
},
|
|
isDirectPlayMethod() {
|
|
return this.playMethod == this.$constants.PlayMethod.DIRECTPLAY
|
|
},
|
|
title() {
|
|
const mediaItemTitle = this.playbackSession?.displayTitle || this.mediaMetadata?.title || 'Title'
|
|
if (this.currentChapterTitle) {
|
|
if (this.showFullscreen) return this.currentChapterTitle
|
|
return `${mediaItemTitle} | ${this.currentChapterTitle}`
|
|
}
|
|
return mediaItemTitle
|
|
},
|
|
authorName() {
|
|
if (this.playbackSession) return this.playbackSession.displayAuthor
|
|
return this.mediaMetadata?.authorName || 'Author'
|
|
},
|
|
chapters() {
|
|
return this.playbackSession?.chapters || []
|
|
},
|
|
currentChapter() {
|
|
if (!this.chapters.length) return null
|
|
return this.chapters.find((ch) => Number(Number(ch.start).toFixed(2)) <= this.currentTime && Number(Number(ch.end).toFixed(2)) > this.currentTime)
|
|
},
|
|
nextChapter() {
|
|
if (!this.chapters.length) return
|
|
return this.chapters.find((c) => Number(Number(c.start).toFixed(2)) > this.currentTime)
|
|
},
|
|
currentChapterTitle() {
|
|
return this.currentChapter?.title || ''
|
|
},
|
|
currentChapterDuration() {
|
|
return this.currentChapter ? this.currentChapter.end - this.currentChapter.start : this.totalDuration
|
|
},
|
|
totalDurationPretty() {
|
|
return this.$secondsToTimestamp(this.totalDuration)
|
|
},
|
|
currentTimePretty() {
|
|
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
|
if (this.playerSettings.scaleElapsedTimeBySpeed) {
|
|
currentTimeToUse = currentTimeToUse / this.currentPlaybackRate
|
|
}
|
|
return this.$secondsToTimestamp(currentTimeToUse)
|
|
},
|
|
timeRemaining() {
|
|
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
|
if (this.playerSettings.useChapterTrack && this.currentChapter) {
|
|
var currChapTime = currentTimeToUse - this.currentChapter.start
|
|
return (this.currentChapterDuration - currChapTime) / this.currentPlaybackRate
|
|
}
|
|
return this.totalTimeRemaining
|
|
},
|
|
totalTimeRemaining() {
|
|
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
|
return (this.totalDuration - currentTimeToUse) / this.currentPlaybackRate
|
|
},
|
|
totalTimeRemainingPretty() {
|
|
if (this.totalTimeRemaining < 0) {
|
|
return this.$secondsToTimestamp(this.totalTimeRemaining * -1)
|
|
}
|
|
return '-' + this.$secondsToTimestamp(this.totalTimeRemaining)
|
|
},
|
|
timeRemainingPretty() {
|
|
if (this.timeRemaining < 0) {
|
|
return this.$secondsToTimestamp(this.timeRemaining * -1)
|
|
}
|
|
return '-' + this.$secondsToTimestamp(this.timeRemaining)
|
|
},
|
|
sleepTimeRemainingPretty() {
|
|
if (!this.sleepTimeRemaining) return '0s'
|
|
var secondsRemaining = Math.round(this.sleepTimeRemaining)
|
|
if (secondsRemaining > 91) {
|
|
return Math.ceil(secondsRemaining / 60) + 'm'
|
|
} else {
|
|
return secondsRemaining + 's'
|
|
}
|
|
},
|
|
socketConnected() {
|
|
return this.$store.state.socketConnected
|
|
},
|
|
mediaId() {
|
|
if (this.isPodcast || !this.playbackSession) return null
|
|
if (this.playbackSession.libraryItemId) {
|
|
return this.playbackSession.episodeId ? `${this.playbackSession.libraryItemId}-${this.playbackSession.episodeId}` : this.playbackSession.libraryItemId
|
|
}
|
|
const localLibraryItem = this.playbackSession.localLibraryItem
|
|
if (!localLibraryItem) return null
|
|
|
|
return this.playbackSession.localEpisodeId ? `${localLibraryItem.id}-${this.playbackSession.localEpisodeId}` : localLibraryItem.id
|
|
}
|
|
},
|
|
methods: {
|
|
showSyncsFailedDialog() {
|
|
Dialog.alert({
|
|
title: this.$strings.HeaderProgressSyncFailed,
|
|
message: this.$strings.MessageProgressSyncFailed,
|
|
cancelText: this.$strings.ButtonOk
|
|
})
|
|
},
|
|
clickChaptersBtn() {
|
|
if (!this.chapters.length) return
|
|
this.showChapterModal = true
|
|
},
|
|
async coverImageLoaded(fullCoverUrl) {
|
|
if (!fullCoverUrl) return
|
|
|
|
const fac = new FastAverageColor()
|
|
fac
|
|
.getColorAsync(fullCoverUrl)
|
|
.then((color) => {
|
|
this.coverRgb = color.rgba
|
|
this.coverBgIsLight = color.isLight
|
|
})
|
|
.catch((e) => {
|
|
console.log(e)
|
|
})
|
|
},
|
|
clickTitleAndAuthor() {
|
|
if (!this.showFullscreen) return
|
|
const llid = this.serverLibraryItemId || this.libraryItem?.id || this.localLibraryItem?.id
|
|
if (llid) {
|
|
this.$router.push(`/item/${llid}`)
|
|
this.showFullscreen = false
|
|
}
|
|
},
|
|
async selectChapter(chapter) {
|
|
await this.$hapticsImpact()
|
|
this.seek(chapter.start)
|
|
this.showChapterModal = false
|
|
},
|
|
async castClick() {
|
|
await this.$hapticsImpact()
|
|
if (this.isLocalPlayMethod) {
|
|
this.$eventBus.$emit('cast-local-item')
|
|
return
|
|
}
|
|
AbsAudioPlayer.requestSession()
|
|
},
|
|
clickContainer() {
|
|
this.expandToFullscreen()
|
|
},
|
|
expandToFullscreen() {
|
|
this.showFullscreen = true
|
|
if (this.titleMarquee) this.titleMarquee.reset()
|
|
|
|
// Update track for total time bar if useChapterTrack is set
|
|
this.$nextTick(() => {
|
|
this.updateTrack()
|
|
})
|
|
},
|
|
collapseFullscreen() {
|
|
this.showFullscreen = false
|
|
if (this.titleMarquee) this.titleMarquee.reset()
|
|
|
|
this.forceCloseDropdownMenu()
|
|
},
|
|
async jumpNextChapter() {
|
|
await this.$hapticsImpact()
|
|
if (this.isLoading) return
|
|
if (!this.nextChapter) return
|
|
this.seek(this.nextChapter.start)
|
|
},
|
|
async jumpChapterStart() {
|
|
await this.$hapticsImpact()
|
|
if (this.isLoading) return
|
|
if (!this.currentChapter) {
|
|
return this.restart()
|
|
}
|
|
|
|
// If 4 seconds or less into current chapter, then go to previous
|
|
if (this.currentTime - this.currentChapter.start <= 4) {
|
|
const currChapterIndex = this.chapters.findIndex((ch) => Number(ch.start) <= this.currentTime && Number(ch.end) >= this.currentTime)
|
|
if (currChapterIndex > 0) {
|
|
const prevChapter = this.chapters[currChapterIndex - 1]
|
|
this.seek(prevChapter.start)
|
|
}
|
|
} else {
|
|
this.seek(this.currentChapter.start)
|
|
}
|
|
},
|
|
showSleepTimerModal() {
|
|
this.$emit('showSleepTimer')
|
|
},
|
|
async setPlaybackSpeed(speed) {
|
|
console.log(`[AudioPlayer] Set Playback Rate: ${speed}`)
|
|
this.currentPlaybackRate = speed
|
|
this.updateTimestamp()
|
|
AbsAudioPlayer.setPlaybackSpeed({ value: speed })
|
|
},
|
|
restart() {
|
|
this.seek(0)
|
|
},
|
|
async jumpBackwards() {
|
|
await this.$hapticsImpact()
|
|
if (this.isLoading) return
|
|
AbsAudioPlayer.seekBackward({ value: this.jumpBackwardsTime })
|
|
},
|
|
async jumpForward() {
|
|
await this.$hapticsImpact()
|
|
if (this.isLoading) return
|
|
AbsAudioPlayer.seekForward({ value: this.jumpForwardTime })
|
|
},
|
|
setStreamReady() {
|
|
this.readyTrackWidth = this.trackWidth
|
|
this.updateReadyTrack()
|
|
},
|
|
setChunksReady(chunks, numSegments) {
|
|
let largestSeg = 0
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
const chunk = chunks[i]
|
|
if (typeof chunk === 'string') {
|
|
const chunkRange = chunk.split('-').map((c) => Number(c))
|
|
if (chunkRange.length < 2) continue
|
|
if (chunkRange[1] > largestSeg) largestSeg = chunkRange[1]
|
|
} else if (chunk > largestSeg) {
|
|
largestSeg = chunk
|
|
}
|
|
}
|
|
const percentageReady = largestSeg / numSegments
|
|
const widthReady = Math.round(this.trackWidth * percentageReady)
|
|
if (this.readyTrackWidth === widthReady) {
|
|
return
|
|
}
|
|
this.readyTrackWidth = widthReady
|
|
this.updateReadyTrack()
|
|
},
|
|
updateReadyTrack() {
|
|
if (this.playerSettings.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() {
|
|
const ts = this.$refs.currentTimestamp
|
|
if (!ts) {
|
|
console.error('No timestamp el')
|
|
return
|
|
}
|
|
|
|
let currentTime = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
|
if (this.playerSettings.useChapterTrack && this.currentChapter) {
|
|
currentTime = Math.max(0, currentTime - this.currentChapter.start)
|
|
}
|
|
if (this.playerSettings.scaleElapsedTimeBySpeed) {
|
|
currentTime = currentTime / this.currentPlaybackRate
|
|
}
|
|
|
|
ts.innerText = this.$secondsToTimestamp(currentTime)
|
|
},
|
|
timeupdate() {
|
|
if (!this.$refs.playedTrack) {
|
|
console.error('Invalid no played track ref')
|
|
return
|
|
}
|
|
this.$emit('updateTime', this.currentTime)
|
|
|
|
if (this.seekLoading) {
|
|
this.seekLoading = false
|
|
if (this.$refs.playedTrack) {
|
|
this.$refs.playedTrack.classList.remove('bg-yellow-300')
|
|
this.$refs.playedTrack.classList.add('bg-gray-200')
|
|
}
|
|
}
|
|
|
|
this.updateTimestamp()
|
|
this.updateTrack()
|
|
},
|
|
updateTrack() {
|
|
// Update progress track UI
|
|
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
|
let percentDone = currentTimeToUse / this.totalDuration
|
|
const totalPercentDone = percentDone
|
|
let bufferedPercent = this.bufferedTime / this.totalDuration
|
|
const totalBufferedPercent = bufferedPercent
|
|
|
|
if (this.playerSettings.useChapterTrack && this.currentChapter) {
|
|
const currChapTime = currentTimeToUse - this.currentChapter.start
|
|
percentDone = currChapTime / this.currentChapterDuration
|
|
bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration))
|
|
}
|
|
|
|
const ptWidth = Math.round(percentDone * this.trackWidth)
|
|
if (this.$refs.playedTrack) {
|
|
this.$refs.playedTrack.style.width = ptWidth + 'px'
|
|
}
|
|
if (this.$refs.bufferedTrack) {
|
|
this.$refs.bufferedTrack.style.width = Math.round(bufferedPercent * this.trackWidth) + 'px'
|
|
}
|
|
|
|
if (this.$refs.trackCursor) {
|
|
this.$refs.trackCursor.style.left = ptWidth - 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'
|
|
}
|
|
},
|
|
seek(time) {
|
|
if (this.isLoading) return
|
|
if (this.seekLoading) {
|
|
console.error('Already seek loading', this.seekedTime)
|
|
return
|
|
}
|
|
|
|
this.seekedTime = time
|
|
this.seekLoading = true
|
|
|
|
AbsAudioPlayer.seek({ value: Math.floor(time) })
|
|
|
|
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.classList.remove('bg-gray-200')
|
|
this.$refs.playedTrack.classList.add('bg-yellow-300')
|
|
}
|
|
},
|
|
async touchstartCursor(e) {
|
|
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.playerSettings.lockUi) return
|
|
|
|
await this.$hapticsImpact()
|
|
this.isDraggingCursor = true
|
|
this.draggingTouchStartX = e.touches[0].pageX
|
|
this.draggingTouchStartTime = this.currentTime
|
|
this.draggingCurrentTime = this.currentTime
|
|
this.updateTrack()
|
|
},
|
|
async playPauseClick() {
|
|
await this.$hapticsImpact()
|
|
if (this.isLoading) return
|
|
|
|
this.isPlaying = !!((await AbsAudioPlayer.playPause()) || {}).playing
|
|
this.isEnded = false
|
|
},
|
|
play() {
|
|
AbsAudioPlayer.playPlayer()
|
|
this.startPlayInterval()
|
|
this.isPlaying = true
|
|
},
|
|
pause() {
|
|
AbsAudioPlayer.pausePlayer()
|
|
this.stopPlayInterval()
|
|
this.isPlaying = false
|
|
},
|
|
startPlayInterval() {
|
|
clearInterval(this.playInterval)
|
|
this.playInterval = setInterval(async () => {
|
|
var data = await AbsAudioPlayer.getCurrentTime()
|
|
this.currentTime = Number(data.value.toFixed(2))
|
|
this.bufferedTime = Number(data.bufferedTime.toFixed(2))
|
|
this.timeupdate()
|
|
}, 1000)
|
|
},
|
|
stopPlayInterval() {
|
|
clearInterval(this.playInterval)
|
|
},
|
|
resetStream(startTime) {
|
|
this.closePlayback()
|
|
},
|
|
touchstart(e) {
|
|
if (!e.changedTouches || this.$store.state.globals.isModalOpen) return
|
|
const touchPosY = e.changedTouches[0].pageY
|
|
// when minimized only listen to touchstart on the player
|
|
if (!this.showFullscreen && touchPosY < window.innerHeight - 120) return
|
|
|
|
// for ios
|
|
if (!this.showFullscreen && e.pageX < 20) {
|
|
e.preventDefault()
|
|
e.stopImmediatePropagation()
|
|
}
|
|
|
|
this.touchStartY = touchPosY
|
|
this.touchStartTime = Date.now()
|
|
},
|
|
touchend(e) {
|
|
if (!e.changedTouches) return
|
|
const touchDuration = Date.now() - this.touchStartTime
|
|
const touchEndY = e.changedTouches[0].pageY
|
|
const touchDistanceY = touchEndY - this.touchStartY
|
|
|
|
// reset touch start data
|
|
this.touchStartTime = 0
|
|
this.touchStartY = 0
|
|
|
|
if (this.isDraggingCursor) {
|
|
if (this.draggingCurrentTime !== this.currentTime) {
|
|
this.seek(this.draggingCurrentTime)
|
|
}
|
|
this.isDraggingCursor = false
|
|
} else {
|
|
if (touchDuration > 1200) {
|
|
// console.log('touch too long', touchDuration)
|
|
return
|
|
}
|
|
if (this.showFullscreen) {
|
|
// Touch start higher than touchend
|
|
if (touchDistanceY > 100) {
|
|
this.collapseFullscreen()
|
|
}
|
|
} else if (touchDistanceY < -100) {
|
|
this.expandToFullscreen()
|
|
}
|
|
}
|
|
},
|
|
touchmove(e) {
|
|
if (!this.isDraggingCursor || !e.touches) return
|
|
|
|
const distanceMoved = e.touches[0].pageX - this.draggingTouchStartX
|
|
let duration = this.totalDuration
|
|
let minTime = 0
|
|
let maxTime = duration
|
|
if (this.playerSettings.useChapterTrack && this.currentChapter) {
|
|
duration = this.currentChapterDuration
|
|
minTime = this.currentChapter.start
|
|
maxTime = minTime + duration
|
|
}
|
|
|
|
const timePerPixel = duration / this.trackWidth
|
|
const newTime = this.draggingTouchStartTime + timePerPixel * distanceMoved
|
|
this.draggingCurrentTime = Math.min(maxTime, Math.max(minTime, newTime))
|
|
|
|
this.updateTimestamp()
|
|
this.updateTrack()
|
|
},
|
|
async clickMenuAction(action) {
|
|
await this.$hapticsImpact()
|
|
this.showMoreMenuDialog = false
|
|
this.$nextTick(() => {
|
|
if (action === 'history') {
|
|
this.$router.push(`/media/${this.mediaId}/history?title=${this.title}`)
|
|
this.showFullscreen = false
|
|
} else if (action === 'scale_elapsed_time') {
|
|
this.playerSettings.scaleElapsedTimeBySpeed = !this.playerSettings.scaleElapsedTimeBySpeed
|
|
this.updateTimestamp()
|
|
this.savePlayerSettings()
|
|
} else if (action === 'lock') {
|
|
this.playerSettings.lockUi = !this.playerSettings.lockUi
|
|
this.savePlayerSettings()
|
|
} else if (action === 'chapter_track') {
|
|
this.playerSettings.useChapterTrack = !this.playerSettings.useChapterTrack
|
|
this.playerSettings.useTotalTrack = !this.playerSettings.useChapterTrack || this.playerSettings.useTotalTrack
|
|
|
|
this.updateTimestamp()
|
|
this.updateTrack()
|
|
this.updateReadyTrack()
|
|
this.updateUseChapterTrack()
|
|
this.savePlayerSettings()
|
|
} else if (action === 'total_track') {
|
|
this.playerSettings.useTotalTrack = !this.playerSettings.useTotalTrack
|
|
this.playerSettings.useChapterTrack = !this.playerSettings.useTotalTrack || this.playerSettings.useChapterTrack
|
|
|
|
this.updateTimestamp()
|
|
this.updateTrack()
|
|
this.updateReadyTrack()
|
|
this.updateUseChapterTrack()
|
|
this.savePlayerSettings()
|
|
} else if (action === 'close') {
|
|
this.closePlayback()
|
|
}
|
|
})
|
|
},
|
|
updateUseChapterTrack() {
|
|
// Chapter track in NowPlaying only supported on iOS for now
|
|
if (this.$platform === 'ios') {
|
|
AbsAudioPlayer.setChapterTrack({ enabled: this.playerSettings.useChapterTrack })
|
|
}
|
|
},
|
|
forceCloseDropdownMenu() {
|
|
if (this.$refs.dropdownMenu && this.$refs.dropdownMenu.closeMenu) {
|
|
this.$refs.dropdownMenu.closeMenu()
|
|
}
|
|
},
|
|
closePlayback() {
|
|
this.endPlayback()
|
|
AbsAudioPlayer.closePlayback()
|
|
},
|
|
endPlayback() {
|
|
this.$store.commit('setPlaybackSession', null)
|
|
this.showFullscreen = false
|
|
this.isEnded = false
|
|
this.isLoading = false
|
|
this.playbackSession = null
|
|
},
|
|
async loadPlayerSettings() {
|
|
const savedPlayerSettings = await this.$localStore.getPlayerSettings()
|
|
if (!savedPlayerSettings) {
|
|
// In 0.9.72-beta 'useChapterTrack', 'useTotalTrack' and 'playerLock' was replaced with 'playerSettings' JSON object
|
|
// Check if this old key was set and if so migrate them over to 'playerSettings'
|
|
const chapterTrackPref = await this.$localStore.getPreferenceByKey('useChapterTrack')
|
|
if (chapterTrackPref) {
|
|
this.playerSettings.useChapterTrack = chapterTrackPref === '1'
|
|
const totalTrackPref = await this.$localStore.getPreferenceByKey('useTotalTrack')
|
|
this.playerSettings.useTotalTrack = totalTrackPref === '1'
|
|
const playerLockPref = await this.$localStore.getPreferenceByKey('playerLock')
|
|
this.playerSettings.lockUi = playerLockPref === '1'
|
|
}
|
|
this.savePlayerSettings()
|
|
} else {
|
|
this.playerSettings.useChapterTrack = !!savedPlayerSettings.useChapterTrack
|
|
this.playerSettings.useTotalTrack = !!savedPlayerSettings.useTotalTrack
|
|
this.playerSettings.lockUi = !!savedPlayerSettings.lockUi
|
|
this.playerSettings.scaleElapsedTimeBySpeed = !!savedPlayerSettings.scaleElapsedTimeBySpeed
|
|
}
|
|
},
|
|
savePlayerSettings() {
|
|
return this.$localStore.setPlayerSettings({ ...this.playerSettings })
|
|
},
|
|
//
|
|
// Listeners from audio AbsAudioPlayer
|
|
//
|
|
onPlayingUpdate(data) {
|
|
console.log('onPlayingUpdate', JSON.stringify(data))
|
|
this.isPlaying = !!data.value
|
|
this.$store.commit('setPlayerPlaying', this.isPlaying)
|
|
if (this.isPlaying) {
|
|
this.startPlayInterval()
|
|
} else {
|
|
this.stopPlayInterval()
|
|
}
|
|
},
|
|
onMetadata(data) {
|
|
console.log('onMetadata', JSON.stringify(data))
|
|
this.totalDuration = Number(data.duration.toFixed(2))
|
|
this.currentTime = Number(data.currentTime.toFixed(2))
|
|
|
|
// Done loading
|
|
if (data.playerState !== 'BUFFERING' && data.playerState !== 'IDLE') {
|
|
this.isLoading = false
|
|
}
|
|
|
|
if (data.playerState === 'ENDED') {
|
|
console.log('[AudioPlayer] Playback ended')
|
|
}
|
|
this.isEnded = data.playerState === 'ENDED'
|
|
|
|
console.log('received metadata update', data)
|
|
|
|
this.timeupdate()
|
|
},
|
|
// When a playback session is started the native android/ios will send the session
|
|
onPlaybackSession(playbackSession) {
|
|
console.log('onPlaybackSession received', JSON.stringify(playbackSession))
|
|
this.playbackSession = playbackSession
|
|
|
|
this.isEnded = false
|
|
this.isLoading = true
|
|
this.syncStatus = 0
|
|
this.$store.commit('setPlaybackSession', this.playbackSession)
|
|
|
|
// Set track width
|
|
this.$nextTick(() => {
|
|
if (this.titleMarquee) this.titleMarquee.reset()
|
|
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)
|
|
}
|
|
})
|
|
},
|
|
onPlaybackClosed() {
|
|
this.endPlayback()
|
|
},
|
|
onPlaybackFailed(data) {
|
|
console.log('Received onPlaybackFailed evt')
|
|
var errorMessage = data.value || 'Unknown Error'
|
|
this.$toast.error(`Playback Failed: ${errorMessage}`)
|
|
this.endPlayback()
|
|
},
|
|
onPlaybackSpeedChanged(data) {
|
|
if (!data.value || isNaN(data.value)) return
|
|
this.currentPlaybackRate = Number(data.value)
|
|
this.updateTimestamp()
|
|
},
|
|
async init() {
|
|
await this.loadPlayerSettings()
|
|
|
|
AbsAudioPlayer.addListener('onPlaybackSession', this.onPlaybackSession)
|
|
AbsAudioPlayer.addListener('onPlaybackClosed', this.onPlaybackClosed)
|
|
AbsAudioPlayer.addListener('onPlaybackFailed', this.onPlaybackFailed)
|
|
AbsAudioPlayer.addListener('onPlayingUpdate', this.onPlayingUpdate)
|
|
AbsAudioPlayer.addListener('onMetadata', this.onMetadata)
|
|
AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
|
|
AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
|
|
AbsAudioPlayer.addListener('onPlaybackSpeedChanged', this.onPlaybackSpeedChanged)
|
|
},
|
|
async screenOrientationChange() {
|
|
if (this.isRefreshingUI) return
|
|
this.isRefreshingUI = true
|
|
const windowWidth = window.innerWidth
|
|
this.refreshUI()
|
|
|
|
// Window width does not always change right away. Wait up to 250ms for a change.
|
|
// iPhone 10 on iOS 16 took between 100 - 200ms to update when going from portrait to landscape
|
|
// but landscape to portrait was immediate
|
|
for (let i = 0; i < 5; i++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
if (window.innerWidth !== windowWidth) {
|
|
this.refreshUI()
|
|
break
|
|
}
|
|
}
|
|
|
|
this.isRefreshingUI = false
|
|
},
|
|
refreshUI() {
|
|
this.updateScreenSize()
|
|
if (this.$refs.track) {
|
|
this.trackWidth = this.$refs.track.clientWidth
|
|
this.updateTrack()
|
|
this.updateReadyTrack()
|
|
}
|
|
},
|
|
updateScreenSize() {
|
|
setTimeout(() => {
|
|
if (this.titleMarquee) this.titleMarquee.init(this.title)
|
|
}, 500)
|
|
|
|
this.windowHeight = window.innerHeight
|
|
this.windowWidth = window.innerWidth
|
|
const coverHeight = this.fullscreenBookCoverWidth * this.bookCoverAspectRatio
|
|
const coverImageWidthCollapsed = 46 / this.bookCoverAspectRatio
|
|
const titleAuthorLeftOffsetCollapsed = 30 + coverImageWidthCollapsed
|
|
const titleAuthorWidthCollapsed = this.windowWidth - 128 - titleAuthorLeftOffsetCollapsed - 10
|
|
|
|
document.documentElement.style.setProperty('--cover-image-width', this.fullscreenBookCoverWidth + 'px')
|
|
document.documentElement.style.setProperty('--cover-image-height', coverHeight + 'px')
|
|
document.documentElement.style.setProperty('--cover-image-width-collapsed', coverImageWidthCollapsed + 'px')
|
|
document.documentElement.style.setProperty('--cover-image-height-collapsed', 46 + 'px')
|
|
document.documentElement.style.setProperty('--title-author-left-offset-collapsed', titleAuthorLeftOffsetCollapsed + 'px')
|
|
document.documentElement.style.setProperty('--title-author-width-collapsed', titleAuthorWidthCollapsed + 'px')
|
|
},
|
|
minimizePlayerEvt() {
|
|
this.collapseFullscreen()
|
|
},
|
|
showProgressSyncIsFailing() {
|
|
this.syncStatus = this.$constants.SyncStatus.FAILED
|
|
},
|
|
showProgressSyncSuccess() {
|
|
this.syncStatus = this.$constants.SyncStatus.SUCCESS
|
|
}
|
|
},
|
|
mounted() {
|
|
this.updateScreenSize()
|
|
if (screen.orientation) {
|
|
// Not available on ios
|
|
screen.orientation.addEventListener('change', this.screenOrientationChange)
|
|
} else {
|
|
document.addEventListener('orientationchange', this.screenOrientationChange)
|
|
}
|
|
window.addEventListener('resize', this.screenOrientationChange)
|
|
|
|
this.$eventBus.$on('minimize-player', this.minimizePlayerEvt)
|
|
document.body.addEventListener('touchstart', this.touchstart, { passive: false })
|
|
document.body.addEventListener('touchend', this.touchend)
|
|
document.body.addEventListener('touchmove', this.touchmove)
|
|
this.$nextTick(this.init)
|
|
},
|
|
beforeDestroy() {
|
|
if (screen.orientation) {
|
|
// Not available on ios
|
|
screen.orientation.removeEventListener('change', this.screenOrientationChange)
|
|
} else {
|
|
document.removeEventListener('orientationchange', this.screenOrientationChange)
|
|
}
|
|
window.removeEventListener('resize', this.screenOrientationChange)
|
|
|
|
if (this.playbackSession) {
|
|
console.log('[AudioPlayer] Before destroy closing playback')
|
|
this.closePlayback()
|
|
}
|
|
|
|
this.forceCloseDropdownMenu()
|
|
this.$eventBus.$off('minimize-player', this.minimizePlayerEvt)
|
|
document.body.removeEventListener('touchstart', this.touchstart)
|
|
document.body.removeEventListener('touchend', this.touchend)
|
|
document.body.removeEventListener('touchmove', this.touchmove)
|
|
|
|
if (AbsAudioPlayer.removeAllListeners) {
|
|
AbsAudioPlayer.removeAllListeners()
|
|
}
|
|
clearInterval(this.playInterval)
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
:root {
|
|
--cover-image-width: 0px;
|
|
--cover-image-height: 0px;
|
|
--cover-image-width-collapsed: 46px;
|
|
--cover-image-height-collapsed: 46px;
|
|
--title-author-left-offset-collapsed: 80px;
|
|
--title-author-width-collapsed: 40%;
|
|
}
|
|
|
|
.playerContainer {
|
|
height: 120px;
|
|
}
|
|
.fullscreen .playerContainer {
|
|
height: 200px;
|
|
}
|
|
@media (orientation: landscape) {
|
|
.fullscreen .playerContainer {
|
|
width: 50%;
|
|
left: 50%;
|
|
}
|
|
}
|
|
#playerContent {
|
|
box-shadow: 0px -8px 8px #11111155;
|
|
}
|
|
.fullscreen #playerContent {
|
|
box-shadow: none;
|
|
}
|
|
|
|
#playerTrack {
|
|
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
|
transition-property: margin;
|
|
bottom: 35px;
|
|
}
|
|
.fullscreen #playerTrack {
|
|
bottom: unset;
|
|
}
|
|
|
|
.cover-wrapper {
|
|
bottom: 68px;
|
|
left: 24px;
|
|
height: var(--cover-image-height-collapsed);
|
|
width: var(--cover-image-width-collapsed);
|
|
transition: all 0.25s cubic-bezier(0.39, 0.575, 0.565, 1);
|
|
transition-property: left, bottom, width, height;
|
|
transform-origin: left bottom;
|
|
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;
|
|
transform-origin: left bottom;
|
|
|
|
width: var(--title-author-width-collapsed);
|
|
bottom: 76px;
|
|
left: var(--title-author-left-offset-collapsed);
|
|
text-align: left;
|
|
}
|
|
.title-author-texts .title-text {
|
|
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
|
transition-property: font-size;
|
|
font-size: 0.85rem;
|
|
line-height: 1.5;
|
|
}
|
|
.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;
|
|
}
|
|
|
|
.fullscreen .title-author-texts {
|
|
bottom: calc(50% - var(--cover-image-height) / 2 + 50px);
|
|
width: 80%;
|
|
left: 10%;
|
|
text-align: center;
|
|
padding-bottom: calc(((260px - var(--cover-image-height)) / 260) * 40);
|
|
pointer-events: auto;
|
|
}
|
|
.fullscreen .title-author-texts .title-text {
|
|
font-size: clamp(0.8rem, calc(var(--cover-image-height) / 260 * 20), 1.3rem);
|
|
}
|
|
.fullscreen .title-author-texts .author-text {
|
|
font-size: clamp(0.6rem, calc(var(--cover-image-height) / 260 * 16), 1rem);
|
|
}
|
|
|
|
#playerControls {
|
|
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
|
transition-property: width, bottom;
|
|
width: 128px;
|
|
padding-right: 24px;
|
|
bottom: 70px;
|
|
}
|
|
#playerControls .jump-icon {
|
|
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
|
transition-property: font-size;
|
|
|
|
margin: 0px 0px;
|
|
font-size: 1.6rem;
|
|
}
|
|
#playerControls .jump-label {
|
|
margin-top: 2px;
|
|
}
|
|
#playerControls .play-btn {
|
|
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
|
transition-property: padding, margin, height, width, min-width, min-height;
|
|
|
|
height: 40px;
|
|
width: 40px;
|
|
min-width: 40px;
|
|
min-height: 40px;
|
|
margin: 0px 7px;
|
|
}
|
|
#playerControls .play-btn .material-symbols {
|
|
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
|
transition-property: font-size;
|
|
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.fullscreen .cover-wrapper {
|
|
margin: 0 auto;
|
|
height: var(--cover-image-height);
|
|
width: var(--cover-image-width);
|
|
left: calc(50% - (calc(var(--cover-image-width)) / 2));
|
|
bottom: calc(50% + 120px - (calc(var(--cover-image-height)) / 2));
|
|
border-radius: 16px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.fullscreen #playerControls {
|
|
width: 100%;
|
|
padding-left: 24px;
|
|
padding-right: 24px;
|
|
bottom: 78px;
|
|
left: 0;
|
|
}
|
|
.fullscreen #playerControls .jump-icon {
|
|
font-size: 2.4rem;
|
|
}
|
|
.fullscreen #playerControls .next-icon {
|
|
font-size: 2rem;
|
|
}
|
|
.fullscreen #playerControls .play-btn {
|
|
height: 65px;
|
|
width: 65px;
|
|
min-width: 65px;
|
|
min-height: 65px;
|
|
}
|
|
.fullscreen #playerControls .play-btn .material-symbols {
|
|
font-size: 2.1rem;
|
|
}
|
|
</style>
|