1- import { Button , Divider , Icon , Text , useContextMenu } from "@common" ;
1+ import { Button , Divider , Icon , Spinner , Text , useContextMenu , useTimedText } from "@common" ;
22import { SourceBadge , useLikeMediaSource } from "@media-source" ;
3- import { QueueActions , QueueSeekSlider , useQueue } from "@queue" ;
3+ import { LyricsUtil , QueueActions , QueueSeekSlider , useLyrics , useQueue } from "@queue" ;
44import { useSettings } from "@settings" ;
5- import { onMount , Show , type Component } from "solid-js" ;
5+ import { createSignal , onMount , Show , type Component } from "solid-js" ;
66
77type Props = {
88 bottomPadding ?: boolean ;
@@ -17,7 +17,7 @@ const EmptyNowPlaying: Component<Props> = (props) => {
1717 </ div >
1818 </ div >
1919 < Divider dark />
20- < div class = "flex items-center h-[3.75rem ] px-4" >
20+ < div class = "flex items-center h-[3.5rem ] px-4" >
2121 < Text . H3 class = "truncate text-neutral-500 hidden discord-pip:block" > It's lonely here...</ Text . H3 >
2222 </ div >
2323
@@ -28,10 +28,71 @@ const EmptyNowPlaying: Component<Props> = (props) => {
2828 ) ;
2929} ;
3030
31+ const Lyrics : Component = ( ) => {
32+ const queue = useQueue ( ) ! ;
33+ const lyrics = useLyrics ( ) ;
34+ const timedText = useTimedText ( ( ) => {
35+ const lyrics = syncedLyrics ( ) ;
36+ return {
37+ elapsed : queue . data . position / 1000 ,
38+ timedTexts : lyrics || [ ] ,
39+ } ;
40+ } ) ;
41+ const syncedLyrics = ( ) => {
42+ const nowPlaying = queue . data . nowPlaying ;
43+ if ( ! nowPlaying ) return null ;
44+
45+ const lyricsOptions = lyrics . data ( ) ;
46+ if ( ! lyricsOptions ?. length ) return null ;
47+
48+ const bestMatch = lyricsOptions
49+ . sort ( ( a , b ) => {
50+ const aDiff = Math . abs ( a . duration - nowPlaying . mediaSource . duration ) ;
51+ const bDiff = Math . abs ( b . duration - nowPlaying . mediaSource . duration ) ;
52+ return aDiff - bDiff ;
53+ } )
54+ . at ( 0 ) ?. synced ;
55+
56+ return bestMatch ? LyricsUtil . parse ( bestMatch ) . synced : null ;
57+ } ;
58+
59+ const lines = ( ) => {
60+ const index = timedText . index ( ) ;
61+ return {
62+ previous : syncedLyrics ( ) ?. [ index - 1 ] ?. text || "" ,
63+ current : syncedLyrics ( ) ?. [ index ] ?. text || "" ,
64+ next : syncedLyrics ( ) ?. [ index + 1 ] ?. text || "" ,
65+ } ;
66+ } ;
67+
68+ return (
69+ < div
70+ class = "relative h-full w-full flex flex-col justify-center overflow-hidden"
71+ classList = { { "items-center" : lyrics . data . loading } }
72+ >
73+ < Show when = { ! lyrics . data . loading } fallback = { < Spinner /> } >
74+ < Show
75+ when = { lyrics . data }
76+ fallback = { < Text . Body2 class = "text-neutral-400 text-center" > No Lyrics Found :(</ Text . Body2 > }
77+ >
78+ < div class = "relative max-h-8" >
79+ < Text . Caption1 class = "absolute bottom-0 left-0 px-4" > { lines ( ) . previous } </ Text . Caption1 >
80+ </ div >
81+ < Text . Body1 class = "text-white font-medium px-4 py-2" > { lines ( ) . current } </ Text . Body1 >
82+ < div class = "relative max-h-8" >
83+ < Text . Caption1 class = "absolute top-0 left-0 px-4" > { lines ( ) . next } </ Text . Caption1 >
84+ </ div >
85+ </ Show >
86+ </ Show >
87+ </ div >
88+ ) ;
89+ } ;
90+
3191export const DiscordActivityPip : Component = ( ) => {
3292 const queue = useQueue ( ) ! ;
3393 const contextMenu = useContextMenu ( ) ! ;
3494 const { settings } = useSettings ( ) ! ;
95+ const [ isShowLyrics , setIsShowLyrics ] = createSignal ( true ) ;
3596
3697 const like = useLikeMediaSource ( ( ) => queue . data . nowPlaying ?. mediaSource . id || "" ) ! ;
3798
@@ -45,15 +106,17 @@ export const DiscordActivityPip: Component = () => {
45106 < Show when = { queue . data . nowPlaying } keyed fallback = { < EmptyNowPlaying bottomPadding /> } >
46107 { ( { mediaSource } ) => (
47108 < div class = "w-full h-full flex flex-col" >
48- < div class = "group/item-list flex items-center justify-center relative w-full h-full" >
49- < img
50- src = { mediaSource . minThumbnailUrl }
51- class = "absolute blur-2xl top-0 left-0 h-full w-full opacity-50"
52- />
53- < img
54- src = { mediaSource . maxThumbnailUrl }
55- class = "relative h-20 w-20 rounded-lg object-cover m-2.5"
56- />
109+ < div class = "group/item-list flex items-center justify-center relative w-full h-full overflow-hidden" >
110+ < Show when = { ! isShowLyrics ( ) } fallback = { < Lyrics /> } >
111+ < img
112+ src = { mediaSource . minThumbnailUrl }
113+ class = "absolute blur-2xl top-0 left-0 h-full w-full opacity-50"
114+ />
115+ < img
116+ src = { mediaSource . maxThumbnailUrl }
117+ class = "relative h-20 w-20 rounded-lg object-cover m-2.5"
118+ />
119+ </ Show >
57120
58121 < div class = "group-hover/item-list:opacity-100 opacity-0 transition-all absolute flex-row-center justify-center w-full h-full bg-black/75" >
59122 < QueueActions extraClass = "w-full justify-between max-w-64" iconSize = "lg" />
@@ -62,7 +125,7 @@ export const DiscordActivityPip: Component = () => {
62125 < div class = "absolute bottom-0 left-0 w-full z-10" >
63126 < QueueSeekSlider
64127 dense
65- extraLabelClass = " px-2.5 text-shadow"
128+ extraLabelClass = { ` px-2.5 text-shadow ${ isShowLyrics ( ) ? "invisible" : "visible" } ` }
66129 value = { queue . data . position / 1000 }
67130 onChange = { ( v ) => queue . seek ( v * 1000 ) }
68131 max = { mediaSource . duration }
@@ -71,7 +134,7 @@ export const DiscordActivityPip: Component = () => {
71134 </ div >
72135 </ div >
73136
74- < div class = "relative flex flex-row w-full bg-neutral-950 space-x-2 px-4 pr-2 py-2.5 " >
137+ < div class = "relative flex flex-row w-full bg-neutral-950 space-x-2 px-4 pr-2 py-2" >
75138 < div class = "grow flex flex-col space-y-1 truncate justify-center" >
76139 < Text . Body1 class = "truncate font-medium text-sm" title = { mediaSource . title } >
77140 { mediaSource . title }
@@ -103,18 +166,32 @@ export const DiscordActivityPip: Component = () => {
103166 </ div >
104167
105168 < Show when = { settings [ "discord.interactivePip.enabled" ] } >
106- < Button
107- flat
108- icon = { like . isLiked ( ) ? "heart" : "heartLine" }
109- iconSize = { "md" }
110- class = "p-2 visible"
111- theme = { like . isLiked ( ) ? "brand" : "secondary" }
112- title = { like . isLiked ( ) ? "Unlike" : "Like" }
113- on :click = { ( e ) => {
114- e . stopImmediatePropagation ( ) ;
115- like . toggle ( ) ;
116- } }
117- />
169+ < div class = "flex space-x-0.5" >
170+ < Button
171+ flat
172+ icon = { like . isLiked ( ) ? "heart" : "heartLine" }
173+ iconSize = { "md" }
174+ class = "p-2 visible"
175+ theme = { like . isLiked ( ) ? "brand" : "secondary" }
176+ title = { like . isLiked ( ) ? "Unlike" : "Like" }
177+ on :click = { ( e ) => {
178+ e . stopImmediatePropagation ( ) ;
179+ like . toggle ( ) ;
180+ } }
181+ />
182+ < Button
183+ flat
184+ icon = { "microphone" }
185+ iconSize = { "md" }
186+ class = "p-2 visible"
187+ theme = { isShowLyrics ( ) ? "brand" : "secondary" }
188+ title = { isShowLyrics ( ) ? "Hide Lyrics" : "Show Lyrics" }
189+ on :click = { ( e ) => {
190+ e . stopImmediatePropagation ( ) ;
191+ setIsShowLyrics ( ! isShowLyrics ( ) ) ;
192+ } }
193+ />
194+ </ div >
118195 </ Show >
119196 </ div >
120197
0 commit comments