mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-19 09:08:26 +02:00
Update:Audio player title scrolls when overflow #740
- increase width of the title on minimized player to scale with screen width - mini player shows media title and chapter title
This commit is contained in:
parent
ff01786063
commit
d03949f5d1
2 changed files with 159 additions and 19 deletions
117
assets/WrappingMarquee.js
Normal file
117
assets/WrappingMarquee.js
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
class WrappingMarquee {
|
||||||
|
#scrollDelay = 2000
|
||||||
|
#scrollSpeed = 30
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} el
|
||||||
|
*/
|
||||||
|
constructor(el) {
|
||||||
|
this.el = el
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.pEl = el?.firstElementChild
|
||||||
|
|
||||||
|
this.innerText = ''
|
||||||
|
this.isScrolling = false
|
||||||
|
|
||||||
|
/** @type {NodeJS.Timeout} */
|
||||||
|
this.timer = null
|
||||||
|
/** @type {number} */
|
||||||
|
this.animationId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transparent gradient mask shown when text is scrolling left and overflowing right
|
||||||
|
*
|
||||||
|
* @param {boolean} showLeft
|
||||||
|
*/
|
||||||
|
setMask(showLeft) {
|
||||||
|
if (!this.el) return
|
||||||
|
this.el.style.maskImage = showLeft ? 'linear-gradient(90deg, transparent 0%, #fff 10%, #000 90%, transparent)' : 'linear-gradient(90deg, #000 90%, transparent)'
|
||||||
|
}
|
||||||
|
|
||||||
|
startScroll() {
|
||||||
|
if (this.isScrolling) {
|
||||||
|
console.warn('Already scrolling')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isScrolling = true
|
||||||
|
this.setMask(true)
|
||||||
|
|
||||||
|
let textScrollAmount = this.el.scrollWidth
|
||||||
|
this.pEl.innerHTML = this.innerText + ' '.repeat(15)
|
||||||
|
let totalScrollAmount = this.el.scrollWidth
|
||||||
|
let scrollDuration = totalScrollAmount * this.#scrollSpeed
|
||||||
|
|
||||||
|
this.pEl.innerHTML = this.pEl.innerHTML + this.innerText
|
||||||
|
|
||||||
|
let done = false
|
||||||
|
let start, previousTimeStamp
|
||||||
|
|
||||||
|
const step = (timeStamp) => {
|
||||||
|
if (start === undefined) {
|
||||||
|
start = timeStamp
|
||||||
|
}
|
||||||
|
const elapsed = timeStamp - start
|
||||||
|
|
||||||
|
if (this.isScrolling && previousTimeStamp !== timeStamp) {
|
||||||
|
const amountToMove = Math.min(elapsed / scrollDuration * totalScrollAmount, totalScrollAmount)
|
||||||
|
this.pEl.style.transform = `translateX(-${amountToMove}px)`
|
||||||
|
if (amountToMove === totalScrollAmount) done = true
|
||||||
|
if (amountToMove > textScrollAmount) this.setMask(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isScrolling || done) { // canceled or done
|
||||||
|
this.isScrolling = false
|
||||||
|
this.pEl.style.transform = 'translateX(0px)'
|
||||||
|
this.pEl.innerText = this.innerText
|
||||||
|
this.setMask(false)
|
||||||
|
if (done) {
|
||||||
|
this.startTimer()
|
||||||
|
}
|
||||||
|
} else if (elapsed < scrollDuration) { // step
|
||||||
|
previousTimeStamp = timeStamp
|
||||||
|
this.animationId = window.requestAnimationFrame(step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.animationId = window.requestAnimationFrame(step)
|
||||||
|
}
|
||||||
|
|
||||||
|
startTimer() {
|
||||||
|
clearTimeout(this.timer)
|
||||||
|
this.timer = setTimeout(() => {
|
||||||
|
this.startScroll()
|
||||||
|
}, this.#scrollDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
clearTimeout(this.timer)
|
||||||
|
this.timer = null
|
||||||
|
this.isScrolling = false
|
||||||
|
window.cancelAnimationFrame(this.animationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize and start marquee if text overflows container
|
||||||
|
* resets the marquee if already active
|
||||||
|
*
|
||||||
|
* @param {string} innerText
|
||||||
|
*/
|
||||||
|
init(innerText) {
|
||||||
|
if (!this.el || !this.pEl) return
|
||||||
|
|
||||||
|
this.reset()
|
||||||
|
|
||||||
|
this.innerText = innerText
|
||||||
|
this.pEl.innerText = innerText
|
||||||
|
this.pEl.style.transform = 'translateX(0px)'
|
||||||
|
|
||||||
|
if (this.el.scrollWidth > this.el.clientWidth) {
|
||||||
|
this.setMask(false)
|
||||||
|
this.startTimer()
|
||||||
|
} else {
|
||||||
|
this.el.style.maskImage = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default WrappingMarquee
|
|
@ -41,7 +41,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor">
|
<div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor">
|
||||||
<p class="title-text truncate">{{ title }}</p>
|
<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>
|
<p class="author-text text-fg text-opacity-75 truncate">{{ authorName }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -106,6 +108,7 @@
|
||||||
import { Capacitor } from '@capacitor/core'
|
import { Capacitor } from '@capacitor/core'
|
||||||
import { AbsAudioPlayer } from '@/plugins/capacitor'
|
import { AbsAudioPlayer } from '@/plugins/capacitor'
|
||||||
import { FastAverageColor } from 'fast-average-color'
|
import { FastAverageColor } from 'fast-average-color'
|
||||||
|
import WrappingMarquee from '@/assets/WrappingMarquee.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
@ -159,7 +162,8 @@ export default {
|
||||||
syncStatus: 0,
|
syncStatus: 0,
|
||||||
showMoreMenuDialog: false,
|
showMoreMenuDialog: false,
|
||||||
coverRgb: 'rgb(55, 56, 56)',
|
coverRgb: 'rgb(55, 56, 56)',
|
||||||
coverBgIsLight: false
|
coverBgIsLight: false,
|
||||||
|
titleMarquee: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -170,6 +174,9 @@ export default {
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
this.updateScreenSize()
|
this.updateScreenSize()
|
||||||
|
},
|
||||||
|
title(val) {
|
||||||
|
if (this.titleMarquee) this.titleMarquee.init(val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -264,22 +271,22 @@ export default {
|
||||||
return this.mediaPlayer === 'cast-player'
|
return this.mediaPlayer === 'cast-player'
|
||||||
},
|
},
|
||||||
mediaPlayer() {
|
mediaPlayer() {
|
||||||
return this.playbackSession ? this.playbackSession.mediaPlayer : null
|
return this.playbackSession?.mediaPlayer || null
|
||||||
},
|
},
|
||||||
mediaType() {
|
mediaType() {
|
||||||
return this.playbackSession ? this.playbackSession.mediaType : null
|
return this.playbackSession?.mediaType || null
|
||||||
},
|
},
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.mediaType === 'podcast'
|
return this.mediaType === 'podcast'
|
||||||
},
|
},
|
||||||
mediaMetadata() {
|
mediaMetadata() {
|
||||||
return this.playbackSession ? this.playbackSession.mediaMetadata : null
|
return this.playbackSession?.mediaMetadata || null
|
||||||
},
|
},
|
||||||
libraryItem() {
|
libraryItem() {
|
||||||
return this.playbackSession ? this.playbackSession.libraryItem || null : null
|
return this.playbackSession?.libraryItem || null
|
||||||
},
|
},
|
||||||
localLibraryItem() {
|
localLibraryItem() {
|
||||||
return this.playbackSession ? this.playbackSession.localLibraryItem || null : null
|
return this.playbackSession?.localLibraryItem || null
|
||||||
},
|
},
|
||||||
localLibraryItemCoverSrc() {
|
localLibraryItemCoverSrc() {
|
||||||
var localItemCover = this.localLibraryItem?.coverContentUrl || null
|
var localItemCover = this.localLibraryItem?.coverContentUrl || null
|
||||||
|
@ -287,7 +294,7 @@ export default {
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
playMethod() {
|
playMethod() {
|
||||||
return this.playbackSession ? this.playbackSession.playMethod : null
|
return this.playbackSession?.playMethod || null
|
||||||
},
|
},
|
||||||
isLocalPlayMethod() {
|
isLocalPlayMethod() {
|
||||||
return this.playMethod == this.$constants.PlayMethod.LOCAL
|
return this.playMethod == this.$constants.PlayMethod.LOCAL
|
||||||
|
@ -296,19 +303,19 @@ export default {
|
||||||
return this.playMethod == this.$constants.PlayMethod.DIRECTPLAY
|
return this.playMethod == this.$constants.PlayMethod.DIRECTPLAY
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
if (this.currentChapterTitle && this.showFullscreen) return this.currentChapterTitle
|
const mediaItemTitle = this.playbackSession?.displayTitle || this.mediaMetadata?.title || 'Title'
|
||||||
if (this.playbackSession) return this.playbackSession.displayTitle
|
if (this.currentChapterTitle) {
|
||||||
return this.mediaMetadata ? this.mediaMetadata.title : 'Title'
|
if (this.showFullscreen) return this.currentChapterTitle
|
||||||
|
return `${mediaItemTitle} | ${this.currentChapterTitle}`
|
||||||
|
}
|
||||||
|
return mediaItemTitle
|
||||||
},
|
},
|
||||||
authorName() {
|
authorName() {
|
||||||
if (this.playbackSession) return this.playbackSession.displayAuthor
|
if (this.playbackSession) return this.playbackSession.displayAuthor
|
||||||
return this.mediaMetadata ? this.mediaMetadata.authorName : 'Author'
|
return this.mediaMetadata?.authorName || 'Author'
|
||||||
},
|
},
|
||||||
chapters() {
|
chapters() {
|
||||||
if (this.playbackSession && this.playbackSession.chapters) {
|
return this.playbackSession?.chapters || []
|
||||||
return this.playbackSession.chapters
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
},
|
},
|
||||||
currentChapter() {
|
currentChapter() {
|
||||||
if (!this.chapters.length) return null
|
if (!this.chapters.length) return null
|
||||||
|
@ -319,7 +326,7 @@ export default {
|
||||||
return this.chapters.find((c) => Number(Number(c.start).toFixed(2)) > this.currentTime)
|
return this.chapters.find((c) => Number(Number(c.start).toFixed(2)) > this.currentTime)
|
||||||
},
|
},
|
||||||
currentChapterTitle() {
|
currentChapterTitle() {
|
||||||
return this.currentChapter ? this.currentChapter.title : ''
|
return this.currentChapter?.title || ''
|
||||||
},
|
},
|
||||||
currentChapterDuration() {
|
currentChapterDuration() {
|
||||||
return this.currentChapter ? this.currentChapter.end - this.currentChapter.start : this.totalDuration
|
return this.currentChapter ? this.currentChapter.end - this.currentChapter.start : this.totalDuration
|
||||||
|
@ -426,6 +433,7 @@ export default {
|
||||||
},
|
},
|
||||||
expandToFullscreen() {
|
expandToFullscreen() {
|
||||||
this.showFullscreen = true
|
this.showFullscreen = true
|
||||||
|
if (this.titleMarquee) this.titleMarquee.reset()
|
||||||
|
|
||||||
// Update track for total time bar if useChapterTrack is set
|
// Update track for total time bar if useChapterTrack is set
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
@ -434,6 +442,8 @@ export default {
|
||||||
},
|
},
|
||||||
collapseFullscreen() {
|
collapseFullscreen() {
|
||||||
this.showFullscreen = false
|
this.showFullscreen = false
|
||||||
|
if (this.titleMarquee) this.titleMarquee.reset()
|
||||||
|
|
||||||
this.forceCloseDropdownMenu()
|
this.forceCloseDropdownMenu()
|
||||||
},
|
},
|
||||||
async jumpNextChapter() {
|
async jumpNextChapter() {
|
||||||
|
@ -833,6 +843,10 @@ export default {
|
||||||
|
|
||||||
// Set track width
|
// Set track width
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
if (this.titleMarquee) this.titleMarquee.reset()
|
||||||
|
this.titleMarquee = new WrappingMarquee(this.$refs.titlewrapper)
|
||||||
|
this.titleMarquee.init(this.title)
|
||||||
|
|
||||||
if (this.$refs.track) {
|
if (this.$refs.track) {
|
||||||
this.trackWidth = this.$refs.track.clientWidth
|
this.trackWidth = this.$refs.track.clientWidth
|
||||||
} else {
|
} else {
|
||||||
|
@ -877,15 +891,23 @@ export default {
|
||||||
}, 50)
|
}, 50)
|
||||||
},
|
},
|
||||||
updateScreenSize() {
|
updateScreenSize() {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.titleMarquee) this.titleMarquee.init(this.title)
|
||||||
|
}, 500)
|
||||||
|
|
||||||
this.windowHeight = window.innerHeight
|
this.windowHeight = window.innerHeight
|
||||||
this.windowWidth = window.innerWidth
|
this.windowWidth = window.innerWidth
|
||||||
const coverHeight = this.fullscreenBookCoverWidth * this.bookCoverAspectRatio
|
const coverHeight = this.fullscreenBookCoverWidth * this.bookCoverAspectRatio
|
||||||
const coverImageWidthCollapsed = 46 / 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-width', this.fullscreenBookCoverWidth + 'px')
|
||||||
document.documentElement.style.setProperty('--cover-image-height', coverHeight + '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-width-collapsed', coverImageWidthCollapsed + 'px')
|
||||||
document.documentElement.style.setProperty('--cover-image-height-collapsed', 46 + 'px')
|
document.documentElement.style.setProperty('--cover-image-height-collapsed', 46 + 'px')
|
||||||
document.documentElement.style.setProperty('--title-author-left-offset-collapsed', 30 + coverImageWidthCollapsed + 'px')
|
document.documentElement.style.setProperty('--title-author-left-offset-collapsed', titleAuthorLeftOffsetCollapsed + 'px')
|
||||||
|
document.documentElement.style.setProperty('--title-author-width-collapsed', titleAuthorWidthCollapsed + 'px')
|
||||||
},
|
},
|
||||||
minimizePlayerEvt() {
|
minimizePlayerEvt() {
|
||||||
this.collapseFullscreen()
|
this.collapseFullscreen()
|
||||||
|
@ -953,6 +975,7 @@ export default {
|
||||||
--cover-image-width-collapsed: 46px;
|
--cover-image-width-collapsed: 46px;
|
||||||
--cover-image-height-collapsed: 46px;
|
--cover-image-height-collapsed: 46px;
|
||||||
--title-author-left-offset-collapsed: 80px;
|
--title-author-left-offset-collapsed: 80px;
|
||||||
|
--title-author-width-collapsed: 40%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playerContainer {
|
.playerContainer {
|
||||||
|
@ -1000,7 +1023,7 @@ export default {
|
||||||
transition-property: left, bottom, width, height;
|
transition-property: left, bottom, width, height;
|
||||||
transform-origin: left bottom;
|
transform-origin: left bottom;
|
||||||
|
|
||||||
width: 40%;
|
width: var(--title-author-width-collapsed);
|
||||||
bottom: 76px;
|
bottom: 76px;
|
||||||
left: var(--title-author-left-offset-collapsed);
|
left: var(--title-author-left-offset-collapsed);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue