mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-06-27 23:40:25 +02:00
Capacitor Network plugin only shows ios device connected if internet access is available. This fix allows iOS users to use local server without internet access. Socket is used to detect if connection to server is availabe. networkConnected is only used for add server form and cellular permission check. Cellular permissions for download and streaming wont work for iOS if device is connected to cellular, but without internet access to server that is used for connectivity check.
1140 lines
43 KiB
Vue
1140 lines
43 KiB
Vue
<template>
|
|
<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' }">
|
|
<div v-if="showFullscreen" class="w-full h-full z-10 absolute top-0 left-0 pointer-events-auto" :style="{ backgroundColor: coverRgb }">
|
|
<div class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: var(--gradient-audio-player)" />
|
|
|
|
<div class="top-4 left-4 absolute cursor-pointer">
|
|
<span class="material-icons text-5xl" :class="{ 'text-black text-opacity-75': coverBgIsLight }" @click="collapseFullscreen">expand_more</span>
|
|
</div>
|
|
<div v-show="showCastBtn" class="top-6 right-16 absolute cursor-pointer">
|
|
<span class="material-icons text-3xl" :class="isCasting ? (coverBgIsLight ? 'text-successDark' : 'text-success') : coverBgIsLight ? 'text-black' : ''" @click="castClick">cast</span>
|
|
</div>
|
|
<div class="top-6 right-4 absolute cursor-pointer">
|
|
<span class="material-icons text-3xl" :class="{ 'text-black text-opacity-75': coverBgIsLight }" @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 }" style="font-size: 10px">{{ isDirectPlayMethod ? $strings.LabelPlaybackDirect : isLocalPlayMethod ? $strings.LabelPlaybackLocal : $strings.LabelPlaybackTranscode }}</p>
|
|
</div>
|
|
|
|
<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>
|
|
|
|
<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>
|
|
|
|
<div v-if="syncStatus === $constants.SyncStatus.FAILED" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-30">
|
|
<span class="material-icons text-error text-3xl">error</span>
|
|
</div>
|
|
</div>
|
|
|
|
<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">
|
|
<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-icons text-3xl text-fg-muted cursor-pointer" @click="$emit('showBookmarks')">{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }}</span>
|
|
<!-- hidden for podcasts but still using this as a placeholder -->
|
|
<span v-else class="material-icons 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-icons 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)" />
|
|
|
|
<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-icons next-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
|
|
<span v-show="!playerSettings.lockUi" class="material-icons jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">{{ jumpBackwardsIcon }}</span>
|
|
<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-icons" :class="{ 'text-white': coverRgb && !coverBgIsLight }">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span>
|
|
<widgets-spinner-icon v-else class="h-8 w-8" />
|
|
</div>
|
|
<span v-show="!playerSettings.lockUi" class="material-icons jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">{{ jumpForwardIcon }}</span>
|
|
<span v-show="showFullscreen && !playerSettings.lockUi" class="material-icons next-icon text-fg cursor-pointer" :class="nextChapter && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
|
|
</div>
|
|
</div>
|
|
|
|
<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>
|
|
|
|
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" :playback-rate="currentPlaybackRate" @select="selectChapter" />
|
|
<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 { FastAverageColor } from 'fast-average-color'
|
|
import WrappingMarquee from '@/assets/WrappingMarquee.js'
|
|
|
|
export default {
|
|
props: {
|
|
bookmarks: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
sleepTimerRunning: Boolean,
|
|
sleepTimeRemaining: Number,
|
|
serverLibraryItemId: String
|
|
},
|
|
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,
|
|
onPlaybackSessionListener: null,
|
|
onPlaybackClosedListener: null,
|
|
onPlayingUpdateListener: null,
|
|
onMetadataListener: null,
|
|
onProgressSyncFailing: null,
|
|
onProgressSyncSuccess: null,
|
|
onPlaybackSpeedChangedListener: null,
|
|
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: {
|
|
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
|
|
},
|
|
jumpForwardIcon() {
|
|
return this.$store.getters['globals/getJumpForwardIcon'](this.jumpForwardTime)
|
|
},
|
|
jumpBackwardsIcon() {
|
|
return this.$store.getters['globals/getJumpBackwardsIcon'](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: {
|
|
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)
|
|
this.$refs.playedTrack.style.width = ptWidth + 'px'
|
|
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()
|
|
|
|
this.onPlaybackSessionListener = AbsAudioPlayer.addListener('onPlaybackSession', this.onPlaybackSession)
|
|
this.onPlaybackClosedListener = AbsAudioPlayer.addListener('onPlaybackClosed', this.onPlaybackClosed)
|
|
this.onPlaybackFailedListener = AbsAudioPlayer.addListener('onPlaybackFailed', this.onPlaybackFailed)
|
|
this.onPlayingUpdateListener = AbsAudioPlayer.addListener('onPlayingUpdate', this.onPlayingUpdate)
|
|
this.onMetadataListener = AbsAudioPlayer.addListener('onMetadata', this.onMetadata)
|
|
this.onProgressSyncFailing = AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
|
|
this.onProgressSyncSuccess = AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
|
|
this.onPlaybackSpeedChangedListener = 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 (this.onPlayingUpdateListener) this.onPlayingUpdateListener.remove()
|
|
if (this.onMetadataListener) this.onMetadataListener.remove()
|
|
if (this.onPlaybackSessionListener) this.onPlaybackSessionListener.remove()
|
|
if (this.onPlaybackClosedListener) this.onPlaybackClosedListener.remove()
|
|
if (this.onPlaybackFailedListener) this.onPlaybackFailedListener.remove()
|
|
if (this.onProgressSyncFailing) this.onProgressSyncFailing.remove()
|
|
if (this.onProgressSyncSuccess) this.onProgressSyncSuccess.remove()
|
|
if (this.onPlaybackSpeedChangedListener) this.onPlaybackSpeedChangedListener.remove()
|
|
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;
|
|
}
|
|
#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;
|
|
}
|
|
|
|
.total-track {
|
|
bottom: 215px;
|
|
left: 0;
|
|
right: 0;
|
|
}
|
|
|
|
.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 .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-icons {
|
|
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-icons {
|
|
font-size: 2.1rem;
|
|
}
|
|
</style>
|