Skip to content

Commit

Permalink
Add lyrics player to the webinterface
Browse files Browse the repository at this point in the history
  • Loading branch information
X-Ryl669 committed Sep 22, 2023
1 parent 5eb870a commit d95eb64
Show file tree
Hide file tree
Showing 15 changed files with 332 additions and 27 deletions.
2 changes: 1 addition & 1 deletion htdocs/assets/index.css

Large diffs are not rendered by default.

33 changes: 17 additions & 16 deletions htdocs/assets/index.js

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions web-src/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -297,15 +297,26 @@ export default {
update_player_status() {
webapi.player_status().then(({ data }) => {
this.$store.commit(types.UPDATE_PLAYER_STATUS, data)
this.update_lyrics()
})
},
update_queue() {
webapi.queue().then(({ data }) => {
this.$store.commit(types.UPDATE_QUEUE, data)
this.update_lyrics()
})
},
update_lyrics() {
let track = this.$store.state.queue.items.filter(e => e.id == this.$store.state.player.item_id)
if (track.length == 1)
webapi.library_lyrics(track[0].track_id).then(({ data }) => {
this.$store.commit(types.UPDATE_LYRICS, data)
})
},
update_settings() {
webapi.settings().then(({ data }) => {
this.$store.commit(types.UPDATE_SETTINGS, data)
Expand Down
181 changes: 181 additions & 0 deletions web-src/src/components/Lyrics.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<template>
<div
class="lyrics-wrapper"
ref="lyricsWrapper"
@touchstart="scrollAbled = false"
@touchend="scrollAbled = true"
v-on:scroll.passive="startedScroll"
v-on:wheel.passive="startedScroll"
>
<div class="lyrics">
<p
v-for="(item, key) in lyricsArr"
:class="key == lyricIndex && is_sync && 'gradient'"
>
{{ item[0] }}
</p>
</div>
</div>
</template>

<script>
export default {
name: "lyrics",
data() {
// Non reactive
// Used as a cache to speed up finding the lyric index in the array for the current time
this.lastIndex = 0;
// Fired upon scrolling, that's disabling the auto scrolling for 5s
this.scrollTimer = null;
this.lastItemId = -1;
// Reactive
return {
scroll: {},
// lineHeight: 42,
scrollAbled: true, // stop scroll to element when touch
};
},
computed: {
player() {
return this.$store.state.player
},
is_sync() {
return this.lyricsArr.length && this.lyricsArr[0].length > 1;
},
lyricIndex() {
// We have to perform a dichotomic search in the time array to find the index that's matching
const curTime = this.player.item_progress_ms / 1000;
const la = this.lyricsArr;
if (this.player.item_id != this.lastItemId) {
// Song changed, let's reset the cache
this.lastItemId = this.player.item_id;
this.lastIndex = 0;
}
if (la.length && la[0].length === 1) return 0; // Bail out for non synchronized lyrics
// 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 dichotomic search for the best time
let i;
let start = 0,
end = la.length - 1;
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;
}
return i;
},
lyricDuration() {
return this.lyricIndex < this.lyricsArr.length - 1
? this.lyricsArr[this.lyricIndex + 1][1] -
this.lyricsArr[this.lyricIndex][1]
: 3600;
},
lyricsArr() {
return this.$store.getters.lyrics;
},
},
watch: {
lyricIndex() {
// Scroll current lyric in the center of the view unless user manipulated
this.scrollAbled && this._scrollToElement();
this.lastIndex = this.lyricIndex;
},
},
methods: {
startedScroll(e) {
// Ugly trick to check if a scroll event comes from the user or from JS
if (!e.screenX || e.screenX == 0 || !e.screenY || e.screenY == 0) return; // Programmatically triggered event are ignored here
this.scrollAbled = false;
if (this.scrollTimer) clearTimeout(this.scrollTimer);
let t = this;
// Re-enable automatic scrolling after 5s
this.scrollTimer = setTimeout(function () {
t.scrollAbled = true;
}, 5000);
},
_scrollToElement() {
let scrollTouch = this.$refs.lyricsWrapper,
currentLyric = scrollTouch.children[0].children[this.lyricIndex],
offsetToCenter = scrollTouch.offsetHeight >> 1;
let currOff = scrollTouch.scrollTop,
destOff = currentLyric.offsetTop - offsetToCenter;
// Using scrollBy ensure that scrolling will happen
// even if the element is visible before scrolling
scrollTouch.scrollBy({
top: destOff - currOff,
left: 0,
behavior: "smooth",
});
// Then prepare the animated gradient too
currentLyric.style.animationDuration = this.lyricDuration + "s";
},
},
};
</script>

<style scoped>
.lyrics-wrapper {
position: absolute;
top: -1rem;
left: calc(50% - 40vw);
right: calc(50% - 40vw);
bottom: 0;
max-height: calc(100vh - 24rem); /* See cover thumbnail style for why */
overflow: auto;
/* From https://css.glass */
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(3px);
-webkit-backdrop-filter: blur(3px);
border: 1px solid rgba(0, 0, 0, 0.3);
}
.lyrics-wrapper .lyrics {
display: flex;
align-items: center;
flex-direction: column;
}
.lyrics-wrapper .lyrics .gradient {
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: bold;
font-size: 120%;
animation: slide-right 1 linear;
background-size: 200% 100%;
background-image: -webkit-linear-gradient(
left,
#080 50%,
#000 50%
);
}
@keyframes slide-right {
0% {
background-position: 100% 0%;
}
100% {
background-position: 0% 0%;
}
}
.lyrics-wrapper .lyrics p {
line-height: 3rem;
text-align: center;
font-size: 1rem;
color: #000;
}
</style>
19 changes: 16 additions & 3 deletions web-src/src/components/NavbarBottom.vue
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,9 @@
</div>
</div>
</div>
<div class="navbar-brand is-flex-grow-1">
<navbar-item-link :to="{ name: 'queue' }" exact class="mr-auto">
<div class="tile is-ancestor mt-0 mb-0">
<group class="tile is-parent is-justify-content-left">
<navbar-item-link :to="{ name: 'queue' }" exact class="">
<mdicon class="icon" name="playlist-play" size="24" />
</navbar-item-link>
<navbar-item-link
Expand All @@ -126,6 +127,8 @@
/>
</div>
</navbar-item-link>
</group>
<group class="tile is-parent is-justify-content-center">
<player-button-previous
v-if="is_now_playing_page"
class="navbar-item px-2"
Expand Down Expand Up @@ -153,15 +156,23 @@
class="navbar-item px-2"
:icon_size="24"
/>
</group>
<group class="tile is-parent is-justify-content-right">
<player-button-lyrics
v-if="is_now_playing_page"
class="navbar-item"
:icon_size="24"
/>
<a
class="navbar-item ml-auto"
class="navbar-item"
@click="show_player_menu = !show_player_menu"
>
<mdicon
class="icon"
:name="show_player_menu ? 'chevron-down' : 'chevron-up'"
/>
</a>
</group>
</div>
<!-- Player menu for mobile and tablet -->
<div
Expand Down Expand Up @@ -271,6 +282,7 @@ import PlayerButtonConsume from '@/components/PlayerButtonConsume.vue'
import PlayerButtonNext from '@/components/PlayerButtonNext.vue'
import PlayerButtonPlayPause from '@/components/PlayerButtonPlayPause.vue'
import PlayerButtonPrevious from '@/components/PlayerButtonPrevious.vue'
import PlayerButtonLyrics from '@/components/PlayerButtonLyrics.vue'
import PlayerButtonRepeat from '@/components/PlayerButtonRepeat.vue'
import PlayerButtonSeekBack from '@/components/PlayerButtonSeekBack.vue'
import PlayerButtonSeekForward from '@/components/PlayerButtonSeekForward.vue'
Expand All @@ -287,6 +299,7 @@ export default {
PlayerButtonNext,
PlayerButtonPlayPause,
PlayerButtonPrevious,
PlayerButtonLyrics,
PlayerButtonRepeat,
PlayerButtonSeekBack,
PlayerButtonSeekForward,
Expand Down
45 changes: 45 additions & 0 deletions web-src/src/components/PlayerButtonLyrics.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<template>
<a :class="{ 'is-active': is_active }" @click="toggle_lyrics">
<mdicon
v-if="!is_active"
name="script-text-outline"
:size="icon_size"
:title="$t('player.button.toggle-lyrics')"
/>
<mdicon
v-if="is_active"
name="script-text-play"
:size="icon_size"
:title="$t('player.button.toggle-lyrics')"
/>
</a>
</template>

<script>
import webapi from '@/webapi'
export default {
name: 'PlayerButtonLyrics',
props: {
icon_size: {
type: Number,
default: 16
}
},
computed: {
is_active() {
return this.$store.getters.lyrics_pane;
}
},
methods: {
toggle_lyrics() {
this.$store.state.lyrics.lyrics_pane = !this.$store.state.lyrics.lyrics_pane;
}
}
}
</script>

<style></style>
8 changes: 6 additions & 2 deletions web-src/src/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ import {
mdiSubdirectoryArrowLeft,
mdiVolumeHigh,
mdiVolumeOff,
mdiWeb
mdiWeb,
mdiScriptTextOutline,
mdiScriptTextPlay
} from '@mdi/js'

export const icons = {
Expand Down Expand Up @@ -117,5 +119,7 @@ export const icons = {
mdiSubdirectoryArrowLeft,
mdiVolumeHigh,
mdiVolumeOff,
mdiWeb
mdiWeb,
mdiScriptTextOutline,
mdiScriptTextPlay
}
3 changes: 2 additions & 1 deletion web-src/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,8 @@
"shuffle-disabled": "Tracks in Reihenfolge wiedergeben",
"skip-backward": "Zum vorherigen Track springen",
"skip-forward": "Zum nächsten Track springen",
"stop": "Wiedergabe stoppen"
"stop": "Wiedergabe stoppen",
"toggle-lyrics": "Liedtexte anzeigen/verbergen"
}
},
"setting": {
Expand Down
3 changes: 2 additions & 1 deletion web-src/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,8 @@
"shuffle-disabled": "Play tracks in order",
"skip-backward": "Skip to previous track",
"skip-forward": "Skip to next track",
"stop": "Stop"
"stop": "Stop",
"toggle-lyrics": "Toggle lyrics"
}
},
"setting": {
Expand Down
3 changes: 2 additions & 1 deletion web-src/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,8 @@
"shuffle-disabled": "Lire les pistes dans l’ordre",
"skip-backward": "Reculer à la piste précédente",
"skip-forward": "Avancer à la piste suivante",
"stop": "Arrêter la lecture"
"stop": "Arrêter la lecture",
"toggle-lyrics": "Voir/Cacher les paroles"
}
},
"setting": {
Expand Down
3 changes: 2 additions & 1 deletion web-src/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,8 @@
"shuffle-disabled": "按顺序播放曲目",
"skip-backward": "播放上一首",
"skip-forward": "播放下一首",
"stop": "停止"
"stop": "停止",
"toggle-lyrics": "显示/隐藏歌词"
}
},
"setting": {
Expand Down
Loading

0 comments on commit d95eb64

Please sign in to comment.