mirror of
https://github.com/owntone/owntone-server.git
synced 2025-01-13 16:03:23 -05:00
[web] Fix a the lyrics pane overlapping the slider
When the track played has a composer or a long title, the slider is not covered by the lyrics pane anymore.
This commit is contained in:
parent
d146a9e940
commit
f419869dfc
@ -1,33 +1,31 @@
|
||||
<template>
|
||||
<div class="lyrics-overlay"></div>
|
||||
<div
|
||||
ref="lyricsWrapper"
|
||||
class="lyrics-wrapper"
|
||||
@touchstart="autoScroll = false"
|
||||
@touchend="autoScroll = true"
|
||||
@scroll.passive="startedScroll"
|
||||
@wheel.passive="startedScroll"
|
||||
ref="lyrics"
|
||||
class="lyrics"
|
||||
@touchstart="autoScrolling = false"
|
||||
@touchend="autoScrolling = true"
|
||||
@scroll.passive="start_scrolling"
|
||||
@wheel.passive="start_scrolling"
|
||||
>
|
||||
<div class="lyrics">
|
||||
<div class="space"></div>
|
||||
<template v-for="(item, index) in lyrics" :key="item">
|
||||
<div
|
||||
v-for="(item, key) in lyricsArr"
|
||||
:key="item"
|
||||
:class="key == lyricIndex && is_sync && 'gradient'"
|
||||
v-if="is_sync && index == verse_index"
|
||||
:class="{ 'is-highlighted': is_playing }"
|
||||
>
|
||||
<ul v-if="key == lyricIndex && is_sync && is_playing">
|
||||
<template v-for="timedWord in splitLyric" :key="timedWord.delay">
|
||||
<li :style="{ animationDuration: timedWord.delay + 's' }">
|
||||
{{ timedWord.text }}
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<template v-if="key != lyricIndex || !is_sync || !is_playing">
|
||||
{{ item[0] }}
|
||||
</template>
|
||||
<span
|
||||
v-for="word in split_verse(index)"
|
||||
class="has-text-weight-bold is-size-5"
|
||||
>
|
||||
<span
|
||||
:style="{ animationDuration: word.duration + 's' }"
|
||||
v-text="word.content"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="space"></div>
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ item[0] }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -43,11 +41,8 @@ export default {
|
||||
// Fired upon scrolling, thus disabling the auto scrolling for 5 seconds
|
||||
this.scrollTimer = null
|
||||
this.lastItemId = -1
|
||||
// Reactive
|
||||
return {
|
||||
scroll: {},
|
||||
// Stops scrolling when a touch event is detected
|
||||
autoScroll: true
|
||||
autoScrolling: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -55,201 +50,156 @@ export default {
|
||||
return this.player.state === 'play'
|
||||
},
|
||||
is_sync() {
|
||||
return this.lyricsArr.length && this.lyricsArr[0].length > 1
|
||||
return this.lyrics.length && this.lyrics[0].length > 1
|
||||
},
|
||||
lyricIndex() {
|
||||
/*
|
||||
* A dichotomous search is performed in
|
||||
* the time array to find the matching index.
|
||||
*/
|
||||
const curTime = this.player.item_progress_ms / 1000
|
||||
const la = this.lyricsArr
|
||||
if (la.length && la[0].length === 1) {
|
||||
this.resetPosCache()
|
||||
// Bail out for non synchronized lyrics
|
||||
verse_index() {
|
||||
if (!this.is_sync) {
|
||||
this.reset_scrolling()
|
||||
return -1
|
||||
}
|
||||
if (
|
||||
this.player.item_id != this.lastItemId ||
|
||||
(this.lastIndexValid() && la[this.lastIndex][1] > curTime)
|
||||
) {
|
||||
const curTime = this.player.item_progress_ms / 1000,
|
||||
la = this.lyrics,
|
||||
trackChanged = this.player.item_id !== this.lastItemId,
|
||||
trackSeeked =
|
||||
this.lastIndex >= 0 &&
|
||||
this.lastIndex < la.length &&
|
||||
la[this.lastIndex][1] > curTime
|
||||
if (trackChanged || trackSeeked) {
|
||||
// Reset the cache when the track has changed or has been rewind
|
||||
this.resetPosCache()
|
||||
this.reset_scrolling()
|
||||
}
|
||||
// Check the cached value to avoid searching the times
|
||||
if (this.lastIndex < la.length - 1 && la[this.lastIndex + 1][1] > curTime)
|
||||
return this.lastIndex
|
||||
if (this.lastIndex < la.length - 2 && la[this.lastIndex + 2][1] > curTime)
|
||||
return this.lastIndex + 1
|
||||
|
||||
// Not found in the next 2 items, so start dichotomous search for the best time
|
||||
let i
|
||||
let start = 0
|
||||
let end = la.length - 1
|
||||
let start = 0,
|
||||
end = la.length - 1,
|
||||
index
|
||||
while (start <= end) {
|
||||
i = ((end + start) / 2) | 0
|
||||
if (la[i][1] <= curTime && la.length > i + 1 && la[i + 1][1] > curTime)
|
||||
break
|
||||
if (la[i][1] < curTime) start = i + 1
|
||||
else end = i - 1
|
||||
index = (start + end) >> 1
|
||||
if (
|
||||
la[index][1] <= curTime &&
|
||||
(la[index + 1]?.[1] > curTime || !la[index + 1])
|
||||
) {
|
||||
return index
|
||||
}
|
||||
la[index][1] < curTime ? (start = index + 1) : (end = index - 1)
|
||||
}
|
||||
return i
|
||||
return -1
|
||||
},
|
||||
lyricDuration() {
|
||||
// Ignore unsynchronized lyrics
|
||||
if (!this.lyricsArr.length || this.lyricsArr[0].length < 2) return 3600
|
||||
// The index is 0 before the first lyrics and until their end
|
||||
if (
|
||||
this.lyricIndex == -1 &&
|
||||
this.player.item_progress_ms / 1000 < this.lyricsArr[0][1]
|
||||
)
|
||||
return this.lyricsArr[0][1]
|
||||
return this.lyricIndex < this.lyricsArr.length - 1
|
||||
? this.lyricsArr[this.lyricIndex + 1][1] -
|
||||
this.lyricsArr[this.lyricIndex][1]
|
||||
: 3600
|
||||
},
|
||||
lyricsArr() {
|
||||
lyrics() {
|
||||
return this.$store.getters.lyrics
|
||||
},
|
||||
player() {
|
||||
return this.$store.state.player
|
||||
},
|
||||
splitLyric() {
|
||||
if (!this.lyricsArr.length || this.lyricIndex == -1) return {}
|
||||
|
||||
// Split evenly the transition in the lyrics word (based on the word size / lyrics size)
|
||||
const lyric = this.lyricsArr[this.lyricIndex][0]
|
||||
const lyricDur = this.lyricDuration / lyric.length
|
||||
|
||||
// Split lyrics into words
|
||||
const parsedWords = lyric.match(/\S+\s*/g)
|
||||
let duration = 0
|
||||
return parsedWords.map((w) => {
|
||||
const d = duration
|
||||
duration += (w.length + 1) * lyricDur
|
||||
return { delay: d, text: w }
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
lyricIndex() {
|
||||
/*
|
||||
* Scroll current lyrics in the center of the view
|
||||
* unless manipulated by user
|
||||
*/
|
||||
this.autoScroll && this._scrollToElement()
|
||||
this.lastIndex = this.lyricIndex
|
||||
verse_index() {
|
||||
this.autoScrolling && this.scroll_to_verse()
|
||||
this.lastIndex = this.verse_index
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
lastIndexValid() {
|
||||
return this.lastIndex >= 0 && this.lastIndex < this.lyricsArr.length
|
||||
},
|
||||
|
||||
resetPosCache() {
|
||||
reset_scrolling() {
|
||||
// Scroll to the start of the track lyrics in all cases
|
||||
if (this.player.item_id != this.lastItemId && this.$refs.lyricsWrapper)
|
||||
this.$refs.lyricsWrapper.scrollTo(0, 0)
|
||||
|
||||
if (this.player.item_id != this.lastItemId && this.$refs.lyrics) {
|
||||
this.$refs.lyrics.scrollTo(0, 0)
|
||||
}
|
||||
this.lastItemId = this.player.item_id
|
||||
this.lastIndex = -1
|
||||
},
|
||||
startedScroll(e) {
|
||||
start_scrolling(e) {
|
||||
/*
|
||||
* Distinguish scroll event triggered by a user or programmatically
|
||||
* Programmatically triggered event are ignored
|
||||
*/
|
||||
if (!e.screenX || e.screenX == 0 || !e.screenY || e.screenY == 0) return
|
||||
this.autoScroll = false
|
||||
if (this.scrollTimer) clearTimeout(this.scrollTimer)
|
||||
this.autoScrolling = false
|
||||
if (this.scrollTimer) {
|
||||
clearTimeout(this.scrollTimer)
|
||||
}
|
||||
const t = this
|
||||
// Reenable automatic scrolling after 5 seconds
|
||||
this.scrollTimer = setTimeout(() => {
|
||||
t.autoScroll = true
|
||||
}, 5000)
|
||||
t.autoScrolling = true
|
||||
}, 2000)
|
||||
},
|
||||
|
||||
_scrollToElement() {
|
||||
const scrollTouch = this.$refs.lyricsWrapper
|
||||
if (this.lyricIndex == -1) {
|
||||
scrollTouch.scrollTo(0, 0)
|
||||
scroll_to_verse() {
|
||||
const pane = this.$refs.lyrics
|
||||
if (this.verse_index === -1) {
|
||||
pane.scrollTo(0, 0)
|
||||
return
|
||||
}
|
||||
|
||||
const currentLyric =
|
||||
scrollTouch.children[0].children[this.lyricIndex + 1], // Because of space item
|
||||
offsetToCenter = scrollTouch.offsetHeight >> 1
|
||||
if (!this.lyricsArr || !currentLyric) return
|
||||
|
||||
const currOff = scrollTouch.scrollTop,
|
||||
destOff =
|
||||
currentLyric.offsetTop -
|
||||
offsetToCenter +
|
||||
(currentLyric.offsetHeight >> 1)
|
||||
const currentVerse = pane.children[this.verse_index]
|
||||
const offsetToCenter = pane.offsetHeight >> 1
|
||||
if (!this.lyrics || !currentVerse) return
|
||||
const top =
|
||||
currentVerse.offsetTop -
|
||||
offsetToCenter +
|
||||
(currentVerse.offsetHeight >> 1) -
|
||||
pane.scrollTop
|
||||
/*
|
||||
* Using scrollBy ensures the scrolling will happen
|
||||
* even if the element is visible before scrolling
|
||||
*/
|
||||
scrollTouch.scrollBy({
|
||||
top: destOff - currOff,
|
||||
pane.scrollBy({
|
||||
top,
|
||||
left: 0,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
},
|
||||
split_verse(index) {
|
||||
const verse = this.lyrics[index]
|
||||
let verseDuration = 3 // Default duration for a verse
|
||||
if (index < this.lyrics.length - 1) {
|
||||
verseDuration = this.lyrics[index + 1][1] - verse[1]
|
||||
}
|
||||
const unitDuration = verseDuration / verse[0].length
|
||||
console.log(`${unitDuration}`)
|
||||
// Split verse into words
|
||||
let duration = 0
|
||||
return verse[0].match(/\S+\s*/g).map((word) => {
|
||||
const d = duration
|
||||
duration += word.length * unitDuration
|
||||
return { duration: d, content: word }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.lyrics-overlay {
|
||||
position: absolute;
|
||||
top: -2rem;
|
||||
.lyrics {
|
||||
top: 0;
|
||||
left: calc(50% - 50vw);
|
||||
width: 100vw;
|
||||
height: calc(100% - 8rem);
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.lyrics-wrapper {
|
||||
height: calc(100vh - 26rem);
|
||||
position: absolute;
|
||||
top: -1rem;
|
||||
left: calc(50% - 50vw);
|
||||
width: 100vw;
|
||||
height: calc(100% - 9rem);
|
||||
z-index: 1;
|
||||
overflow: auto;
|
||||
|
||||
/* Glass effect */
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
--mask: linear-gradient(
|
||||
180deg,
|
||||
transparent 0%,
|
||||
rgba(0, 0, 0, 1) 15%,
|
||||
rgba(0, 0, 0, 1) 85%,
|
||||
transparent 100%
|
||||
);
|
||||
-webkit-mask: var(--mask);
|
||||
mask: var(--mask);
|
||||
}
|
||||
.lyrics-wrapper .lyrics {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.lyrics-wrapper .lyrics .gradient {
|
||||
font-weight: bold;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
.lyrics-wrapper .lyrics .gradient ul li {
|
||||
display: inline;
|
||||
.lyrics div.is-highlighted span {
|
||||
animation: pop-color 0s linear forwards;
|
||||
}
|
||||
|
||||
.lyrics-wrapper .lyrics div {
|
||||
.lyrics div {
|
||||
line-height: 3rem;
|
||||
text-align: center;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.lyrics div:first-child {
|
||||
padding-top: calc(25vh - 2rem);
|
||||
}
|
||||
|
||||
.lyrics-wrapper .lyrics .space {
|
||||
height: 20vh;
|
||||
.lyrics div:last-child {
|
||||
padding-bottom: calc(25vh - 3rem);
|
||||
}
|
||||
</style>
|
||||
|
@ -13,19 +13,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.lyrics-wrapper .lyrics .gradient {
|
||||
color: $success;
|
||||
}
|
||||
|
||||
.lyrics-wrapper .lyrics div {
|
||||
color: $black;
|
||||
}
|
||||
.lyrics-overlay {
|
||||
box-shadow:
|
||||
0px 40px 40px 0px $white inset,
|
||||
0px -40px 40px 0px $white inset;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background-color: $info;
|
||||
border-radius: 2px;
|
||||
@ -139,6 +126,9 @@ a.navbar-item {
|
||||
border-radius: $radius-large;
|
||||
max-height: calc(100vh - 26rem);
|
||||
}
|
||||
&.is-masked {
|
||||
filter: blur(0.5rem) opacity(0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
||||
:artist="track.artist"
|
||||
:album="track.album"
|
||||
class="is-clickable fd-has-shadow fd-cover-big-image"
|
||||
:class="{ 'is-masked': lyrics_visible }"
|
||||
@click="open_dialog(track)"
|
||||
/>
|
||||
<lyrics-pane v-if="lyrics_visible" />
|
||||
|
Loading…
Reference in New Issue
Block a user