Skip to content

Commit

Permalink
[web] Add lyrics player to the webinterface
Browse files Browse the repository at this point in the history
Update icons.js
Add icons in alphabetical order.
Change comment to remove reference to external website
Remove extra line feeds

Co-Authored-by: Alain Nussbaumer <alain.nussbaumer@alleluia.ch>
  • Loading branch information
X-Ryl669 and hacketiwack committed Oct 10, 2023
1 parent c9a4da9 commit f3ba450
Show file tree
Hide file tree
Showing 15 changed files with 352 additions and 25 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.

10 changes: 10 additions & 0 deletions web-src/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -297,15 +297,25 @@ 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_track(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
188 changes: 188 additions & 0 deletions web-src/src/components/Lyrics.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<template>
<div
class="lyrics-wrapper"
ref="lyricsWrapper"
@touchstart="autoScroll = false"
@touchend="autoScroll = 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,
autoScroll: 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 (la.length && la[0].length === 1) return 0; // Bail out for non synchronized lyrics
if (this.player.item_id != this.lastItemId
|| this.lastIndex < la.length && la[this.lastIndex][1] > curTime) {
// Song changed or time scrolled back, let's reset the cache
this.lastItemId = this.player.item_id;
this.lastIndex = 0;
}
// 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() {
// Ignore unsynchronized lyrics.
if (!this.lyricsArr.length || this.lyricsArr[0].length < 2) return 3600;
// The index is 0 before the first lyric until the end of the first lyric
if (!this.lyricIndex && 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() {
return this.$store.getters.lyrics;
},
},
watch: {
lyricIndex() {
// Scroll current lyric in the center of the view unless user manipulated
this.autoScroll && 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.autoScroll = false;
if (this.scrollTimer) clearTimeout(this.scrollTimer);
let t = this;
// Re-enable automatic scrolling after 5s
this.scrollTimer = setTimeout(function () {
t.autoScroll = true;
}, 5000);
},
_scrollToElement() {
let scrollTouch = this.$refs.lyricsWrapper,
currentLyric = scrollTouch.children[0].children[this.lyricIndex],
offsetToCenter = scrollTouch.offsetHeight >> 1;
if (!this.lyricsArr || !currentLyric) return;
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(100% - 9rem);
overflow: auto;
/* Glass effect */
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="navbar-brand is-flex-grow-1">
<div class="navbar-item is-expanded is-justify-content-left is-no-basis">
<navbar-item-link :to="{ name: 'queue' }" exact>
<mdicon class="icon" name="playlist-play" size="24" />
</navbar-item-link>
<navbar-item-link
Expand All @@ -126,6 +127,8 @@
/>
</div>
</navbar-item-link>
</div>
<div class="navbar-item is-expanded is-justify-content-center is-no-basis">
<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"
/>
</div>
<div class="navbar-item is-expanded is-justify-content-right is-no-basis">
<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>
</div>
</div>
<!-- Player menu for mobile and tablet -->
<div
Expand Down Expand Up @@ -268,6 +279,7 @@ import { mdiCancel } from '@mdi/js'
import NavbarItemLink from './NavbarItemLink.vue'
import NavbarItemOutput from './NavbarItemOutput.vue'
import PlayerButtonConsume from '@/components/PlayerButtonConsume.vue'
import PlayerButtonLyrics from '@/components/PlayerButtonLyrics.vue'
import PlayerButtonNext from '@/components/PlayerButtonNext.vue'
import PlayerButtonPlayPause from '@/components/PlayerButtonPlayPause.vue'
import PlayerButtonPrevious from '@/components/PlayerButtonPrevious.vue'
Expand All @@ -284,6 +296,7 @@ export default {
NavbarItemLink,
NavbarItemOutput,
PlayerButtonConsume,
PlayerButtonLyrics,
PlayerButtonNext,
PlayerButtonPlayPause,
PlayerButtonPrevious,
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>
4 changes: 4 additions & 0 deletions web-src/src/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ import {
mdiRepeatOnce,
mdiRewind10,
mdiRss,
mdiScriptTextOutline,
mdiScriptTextPlay,
mdiServer,
mdiShuffle,
mdiShuffleDisabled,
Expand Down Expand Up @@ -107,6 +109,8 @@ export const icons = {
mdiRewind10,
mdiRss,
mdiServer,
mdiScriptTextOutline,
mdiScriptTextPlay,
mdiShuffle,
mdiShuffleDisabled,
mdiSkipBackward,
Expand Down
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 f3ba450

Please sign in to comment.