diff --git a/web-src/src/components/LyricsPane.vue b/web-src/src/components/LyricsPane.vue index 225f6b0d..5c69baa9 100644 --- a/web-src/src/components/LyricsPane.vue +++ b/web-src/src/components/LyricsPane.vue @@ -1,33 +1,31 @@ @@ -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 } + }) } } } diff --git a/web-src/src/mystyles.scss b/web-src/src/mystyles.scss index 3dcc8137..0009a8b1 100644 --- a/web-src/src/mystyles.scss +++ b/web-src/src/mystyles.scss @@ -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); + } } } diff --git a/web-src/src/pages/PageNowPlaying.vue b/web-src/src/pages/PageNowPlaying.vue index a751b530..c66468bd 100644 --- a/web-src/src/pages/PageNowPlaying.vue +++ b/web-src/src/pages/PageNowPlaying.vue @@ -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)" />