Skip to content

Commit b0aa32d

Browse files
add lyrics offset
1 parent e6f0e26 commit b0aa32d

File tree

8 files changed

+151
-54
lines changed

8 files changed

+151
-54
lines changed

src/apps/app/layout/discord-activity-pip/discord.activity-pip.layout.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Button, Divider, Icon, Spinner, Text, useContextMenu, useTimedText } from "@common";
22
import { SourceBadge, useLikeMediaSource } from "@media-source";
3-
import { LyricsUtil, QueueActions, QueueSeekSlider, useLyrics, useQueue } from "@queue";
3+
import { LyricsUtil, QueueActions, QueueSeekSlider, useLyrics, usePlayerSpeed, useQueue } from "@queue";
44
import { useSettings } from "@settings";
55
import { onMount, Show, type Component } from "solid-js";
66

@@ -30,12 +30,16 @@ const EmptyNowPlaying: Component<Props> = (props) => {
3030

3131
const Lyrics: Component = () => {
3232
const queue = useQueue()!;
33+
const speed = usePlayerSpeed();
34+
const { settings } = useSettings();
3335
const lyrics = useLyrics();
3436
const timedText = useTimedText(() => {
3537
const lyrics = syncedLyrics();
3638
return {
39+
offset: settings["app.lyrics.offset"],
3740
elapsed: queue.data.position / 1000,
3841
timedTexts: lyrics || [],
42+
speed: speed(),
3943
};
4044
});
4145
const syncedLyrics = () => {

src/apps/app/views/lyrics/lyrics.view.tsx

Lines changed: 118 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useApp } from "@app/providers";
2-
import { Container, Icon, Spinner, useTimedText } from "@common";
3-
import { LyricsUtil, useLyrics, useQueue } from "@queue";
2+
import { Button, Container, Divider, Icon, Input, Spinner, Text, useTimedText } from "@common";
3+
import { LyricsUtil, useLyrics, usePlayerSpeed, useQueue } from "@queue";
4+
import { useSettings } from "@settings";
45
import { For, Match, Switch, createEffect, onMount, type Component } from "solid-js";
56
import "./lyrics.style.css";
67

@@ -25,12 +26,16 @@ export const Lyrics: Component = () => {
2526
let container!: HTMLDivElement;
2627
const app = useApp()!;
2728
const queue = useQueue()!;
29+
const speed = usePlayerSpeed();
30+
const { settings, setSettings } = useSettings();
2831
const lyrics = useLyrics();
2932
const timedText = useTimedText(() => {
3033
const lyrics = syncedLyrics();
3134
return {
35+
offset: settings["app.lyrics.offset"],
3236
elapsed: queue.data.position / 1000,
33-
timedTexts: lyrics || [],
37+
timedTexts: lyrics?.content || [],
38+
speed: speed(),
3439
};
3540
});
3641

@@ -52,9 +57,14 @@ export const Lyrics: Component = () => {
5257
const bDiff = Math.abs(b.duration - nowPlaying.mediaSource.duration);
5358
return aDiff - bDiff;
5459
})
55-
.at(0)?.synced;
56-
57-
return bestMatch ? LyricsUtil.parse(bestMatch).synced : null;
60+
.at(0);
61+
62+
return bestMatch?.synced
63+
? {
64+
source: bestMatch.source,
65+
content: LyricsUtil.parse(bestMatch.synced).synced,
66+
}
67+
: null;
5868
};
5969

6070
const normalLyrics = () => {
@@ -68,7 +78,7 @@ export const Lyrics: Component = () => {
6878
if (unsyncedLyrics?.unsynced) {
6979
return {
7080
content: unsyncedLyrics.unsynced,
71-
description: unsyncedLyrics.source,
81+
source: unsyncedLyrics.source,
7282
};
7383
}
7484

@@ -79,13 +89,17 @@ export const Lyrics: Component = () => {
7989
LyricsUtil.parse(synced.synced)
8090
.synced?.map((s) => s.text)
8191
.join("\n") || "",
82-
description: synced.source,
92+
source: synced.source,
8393
};
8494
}
8595

8696
return null;
8797
};
8898

99+
const source = () => {
100+
return (syncedLyrics()?.source || normalLyrics()?.source || "-").toUpperCase();
101+
};
102+
89103
createEffect(() => {
90104
if (timedText.index() === -1 && container) {
91105
container.scrollTop = 0;
@@ -112,51 +126,104 @@ export const Lyrics: Component = () => {
112126
};
113127

114128
return (
115-
<Container
116-
size="full"
117-
extraClass="h-full flex flex-col items-center space-y-2.5"
118-
centered
119-
ref={container}
120-
onScroll={onContainerScrollHandler}
121-
>
122-
<Switch fallback={<LyricsNotFound />}>
123-
<Match when={lyrics.data.loading}>
124-
<Loading />
125-
</Match>
126-
<Match when={syncedLyrics()} keyed>
127-
<For each={syncedLyrics()}>
128-
{(t, i) => (
129-
<div
130-
class="space-y-1 py-2 text"
131-
classList={{
132-
"text-neutral-300": i() < timedText.index(),
133-
"text-neutral-500": i() > timedText.index(),
134-
"!text-neutral-300": i() === timedText.index() + 1,
135-
"!text-neutral-400": i() === timedText.index() + 2,
136-
"text-xl md:text-2xl": i() !== timedText.index(),
137-
"font-semibold text-2xl md:text-3xl !text-neutral-100": i() === timedText.index(),
138-
}}
139-
>
140-
<div>{t.text}</div>
141-
</div>
129+
<div class="flex flex-col h-full overflow-y-auto space-y-0.5 md:space-y-2">
130+
<Container
131+
size="full"
132+
extraClass="h-full flex flex-col items-center space-y-2.5"
133+
centered
134+
ref={container}
135+
onScroll={onContainerScrollHandler}
136+
>
137+
<Switch fallback={<LyricsNotFound />}>
138+
<Match when={lyrics.data.loading}>
139+
<Loading />
140+
</Match>
141+
<Match when={syncedLyrics()} keyed>
142+
<For each={syncedLyrics()?.content}>
143+
{(t, i) => (
144+
<div
145+
class="space-y-1 py-2 text"
146+
classList={{
147+
"text-neutral-300": i() < timedText.index(),
148+
"text-neutral-500": i() > timedText.index(),
149+
"!text-neutral-300": i() === timedText.index() + 1,
150+
"!text-neutral-400": i() === timedText.index() + 2,
151+
"text-xl md:text-2xl": i() !== timedText.index(),
152+
"font-semibold text-2xl md:text-3xl !text-neutral-100":
153+
i() === timedText.index(),
154+
}}
155+
>
156+
<div>{t.text}</div>
157+
</div>
158+
)}
159+
</For>
160+
</Match>
161+
<Match when={normalLyrics()} keyed>
162+
{({ content }) => (
163+
<>
164+
<For each={content.split(/\r?\n/)}>
165+
{(t) => (
166+
<div
167+
class="!text-lg md:!text-xl text-neutral-300 text"
168+
classList={{ "py-1": !t }}
169+
>
170+
{t}
171+
</div>
172+
)}
173+
</For>
174+
</>
142175
)}
143-
</For>
144-
</Match>
145-
<Match when={normalLyrics()} keyed>
146-
{({ content, description }) => (
147-
<>
148-
<For each={content.split(/\r?\n/)}>
149-
{(t) => (
150-
<div class="!text-lg md:!text-xl text-neutral-300 text" classList={{ "py-1": !t }}>
151-
{t}
176+
</Match>
177+
</Switch>
178+
</Container>
179+
180+
<div class="shrink">
181+
<Container
182+
padless
183+
extraClass={"h-full py-3 px-4 md:px-8 flex flex-row items-center space-x-4 md:space-x-6"}
184+
>
185+
<div class="flex flex-row h-full items-center space-x-4">
186+
<Text.Caption1>Offset</Text.Caption1>
187+
<div class="flex flex-row h-full space-x-2">
188+
<Input
189+
type="number"
190+
outlined
191+
dense
192+
prefix={() => (
193+
<Button
194+
flat
195+
onClick={() => setSettings("app.lyrics.offset", (v) => v - 100)}
196+
class="px-2"
197+
>
198+
-
199+
</Button>
200+
)}
201+
class="h-full w-36 appearance-none"
202+
inputExtraClass="text-center"
203+
suffix={() => (
204+
<div class="flex flex-row items-center space-x-1.5">
205+
<Text.Caption1>s</Text.Caption1>
206+
<Button
207+
flat
208+
onClick={() => setSettings("app.lyrics.offset", (v) => v + 100)}
209+
class="px-2"
210+
>
211+
+
212+
</Button>
152213
</div>
153214
)}
154-
</For>
155-
<div class="pt-8 text-neutral-400 text">{description}</div>
156-
</>
157-
)}
158-
</Match>
159-
</Switch>
160-
</Container>
215+
value={settings["app.lyrics.offset"] / 1000}
216+
step={0.1}
217+
onChange={(e) => {
218+
setSettings("app.lyrics.offset", +e.currentTarget.value * 1000);
219+
}}
220+
/>
221+
</div>
222+
</div>
223+
<Divider vertical dark extraClass="h-full" />
224+
<Text.Caption1>Source: {source()}</Text.Caption1>
225+
</Container>
226+
</div>
227+
</div>
161228
);
162229
};

src/index.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,11 @@ input:focus {
9999
transform: translateY(100vh);
100100
}
101101
}
102+
103+
@layer base {
104+
input[type="number"]::-webkit-inner-spin-button,
105+
input[type="number"]::-webkit-outer-spin-button {
106+
-webkit-appearance: none;
107+
margin: 0;
108+
}
109+
}

src/libs/common/components/input/input.component.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export type InputProps = Omit<JSX.InputHTMLAttributes<HTMLInputElement>, "prefix
77
focusOnMount?: boolean;
88
prefix?: Accessor<JSX.Element>;
99
suffix?: Accessor<JSX.Element>;
10+
inputExtraClass?: string;
11+
inputExtraClassList?: Record<string, boolean>;
1012
};
1113

1214
export const Input: Component<InputProps> = (props) => {
@@ -56,6 +58,7 @@ export const Input: Component<InputProps> = (props) => {
5658
"!pr-1.5": !!props.suffix && props.dense,
5759
"py-2 px-4": !props.dense,
5860
"py-1 px-2": props.dense,
61+
[props.inputExtraClass || ""]: !!props.inputExtraClass,
5962
}}
6063
/>
6164
{props.suffix?.()}

src/libs/common/hooks/timed-text.hook.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export type TimedText = {
99
type Params = {
1010
timedTexts: TimedText[];
1111
elapsed: number;
12+
speed?: number;
13+
offset?: number;
1214
};
1315

1416
export const useTimedText = (params: Accessor<Params>) => {
@@ -20,8 +22,9 @@ export const useTimedText = (params: Accessor<Params>) => {
2022
createEffect(() => {
2123
clearUpdateTimeout();
2224

23-
const elapsed = params().elapsed;
25+
const elapsed = params().elapsed + (params().offset ?? 0) / 1000;
2426
const data = params().timedTexts;
27+
const speed = params().speed ?? 1;
2528

2629
const indexes = [];
2730
for (const [i, t] of data.entries()) {
@@ -44,7 +47,7 @@ export const useTimedText = (params: Accessor<Params>) => {
4447
else setIndex(index);
4548

4649
if (delay < 5000 && elapsed < (data[data.length - 1].endTime ?? Infinity) && !last) {
47-
optimisticUpdateTimeout = setTimeout(() => setIndex((v) => v + 1), delay);
50+
optimisticUpdateTimeout = setTimeout(() => setIndex((v) => v + 1), delay * speed);
4851
}
4952
});
5053

src/libs/queue/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from "./lyrics.hook";
2+
export * from "./player-speed.hook";
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { useQueue } from "../providers";
2+
3+
export const usePlayerSpeed = () => {
4+
const queue = useQueue()!;
5+
6+
const speed = () => (queue.data.filtersState.timescale.speed || 1) * (queue.data.filtersState.timescale.rate || 1);
7+
8+
return speed;
9+
};

src/libs/settings/providers/settings/settings.provider.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type Settings = {
2727
["app.snowfall.amount"]: number;
2828
["app.snowfall.speed"]: number;
2929
["app.mediaSession.enabled"]: boolean;
30+
["app.lyrics.offset"]: number;
3031
["overlay.enabled"]: boolean;
3132
["overlay.shortcut"]: string[];
3233
["overlay.nowPlaying.enabled"]: boolean;
@@ -61,6 +62,7 @@ const defaultSettings: Settings = {
6162
["app.snowfall.amount"]: 25,
6263
["app.snowfall.speed"]: 50,
6364
["app.mediaSession.enabled"]: true,
65+
["app.lyrics.offset"]: 0,
6466
["overlay.enabled"]: true,
6567
["overlay.shortcut"]: ["Control", "Shift", "B"],
6668
["overlay.nowPlaying.enabled"]: false,

0 commit comments

Comments
 (0)