Skip to content

Commit

Permalink
feat: add lyric mode (#215)
Browse files Browse the repository at this point in the history
Co-authored-by: JellyBrick <[email protected]>
Co-authored-by: Su-Yong <[email protected]>
  • Loading branch information
JellyBrick and Su-Yong authored Sep 1, 2023
1 parent 96029fd commit 36474f0
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 55 deletions.
5 changes: 5 additions & 0 deletions common/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const ConfigLyricMode = {
NONE: -1,
PLAYER: -2,
};
export type ConfigLyricMode = typeof ConfigLyricMode[keyof typeof ConfigLyricMode];
6 changes: 4 additions & 2 deletions common/intl/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
"lyrics.title": "Titel",
"lyrics.current-playing-track": "aktuell gespielte Musik",
"lyrics.current-applied-lyric": "aktuell verwendete Lyrics",
"lyrics.auto-recognizing": "wird automatisch erkannt",
"lyrics.change-to-auto-recognize-mode": "in den automatischen Erkennungsmodus wechseln",
"lyrics.mode.auto": "automatische erkennung",
"lyrics.mode.manual": "manuell auswählen",
"lyrics.mode.none": "keine Lyrics anzeigen",
"lyrics.mode.player": "Liedtext aus dem Musikplayer",
"lyrics.lyric-id": "ID",
"lyrics.lyric-author": "Autor",
"lyrics.auto-recognized": "automatisch suchen",
Expand Down
6 changes: 4 additions & 2 deletions common/intl/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
"lyrics.title": "Title",
"lyrics.current-playing-track": "Current music",
"lyrics.current-applied-lyric": "Current lyrics",
"lyrics.auto-recognizing": "Now Auto-recognizing",
"lyrics.change-to-auto-recognize-mode": "Change to Auto-recognizing mode",
"lyrics.mode.auto": "Auto recognizing",
"lyrics.mode.manual": "Manual selection",
"lyrics.mode.none": "Don't show lyrics",
"lyrics.mode.player": "Provide from music player",
"lyrics.lyric-id": "ID",
"lyrics.lyric-author": "Lyrics author",
"lyrics.auto-recognized": "Automatic search",
Expand Down
6 changes: 4 additions & 2 deletions common/intl/translations/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
"lyrics.title": "タイトル",
"lyrics.current-playing-track": "現在再生中の音楽",
"lyrics.current-applied-lyric": "現在適用中の歌詞",
"lyrics.auto-recognizing": "認識中",
"lyrics.change-to-auto-recognize-mode": "自動認識モードに変更",
"lyrics.mode.auto": "自動認識",
"lyrics.mode.manual": "手動設定",
"lyrics.mode.none": "歌詞を表示しない",
"lyrics.mode.player": "プレーヤーが提供する歌詞",
"lyrics.lyric-id": "ID",
"lyrics.lyric-author": "アップローダー",
"lyrics.auto-recognized": "認識完了",
Expand Down
6 changes: 4 additions & 2 deletions common/intl/translations/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
"lyrics.title": "제목",
"lyrics.current-playing-track": "현재 재생 중인 음악",
"lyrics.current-applied-lyric": "현재 적용된 가사",
"lyrics.auto-recognizing": "자동 인식 중",
"lyrics.change-to-auto-recognize-mode": "자동 인식 모드로 변경",
"lyrics.mode.auto": "자동 인식",
"lyrics.mode.manual": "수동 설정",
"lyrics.mode.none": "가사 표시 안 함",
"lyrics.mode.player": "플레이어 제공 가사",
"lyrics.lyric-id": "ID",
"lyrics.lyric-author": "작성자",
"lyrics.auto-recognized": "자동 검색",
Expand Down
69 changes: 51 additions & 18 deletions renderer/components/PlayingInfoProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,24 @@ import {
useContext,
} from 'solid-js';

import IconMusic from '../../assets/icon_music.png';
import useLyricMapper from '../hooks/useLyricMapper';
import { UpdateData } from '../types';
import { ConfigLyricMode } from '../../common/constants';

export type Lyric = Awaited<ReturnType<typeof alsong.getLyricById>>;
export type Status = 'idle' | 'playing' | 'stopped';
export type LyricMode = 'auto' | 'manual' | 'player' | 'none';
export type PlayingInfo = {
progress: Accessor<number>;
duration: Accessor<number>;
title: Accessor<string>;
artist: Accessor<string>;
status: Accessor<Status>;
coverUrl: Accessor<string>;
coverUrl: Accessor<string | undefined>;
lyrics: Accessor<FlatMap<number, string[]> | null>;
originalData: Accessor<UpdateData | null>;
originalLyric: Accessor<LyricInfo | null>;
lyricMode: Accessor<LyricMode>;
};

export type LyricInfo =
Expand All @@ -41,24 +43,39 @@ const PlayingInfoContext = createContext<PlayingInfo>({
title: () => 'Not Playing' as const,
artist: () => 'N/A' as const,
status: () => 'idle' as const,
coverUrl: () => IconMusic,
coverUrl: () => undefined,
lyrics: () => null,
originalData: () => null,
originalLyric: () => null,
lyricMode: () => 'auto' as const,
});
const PlayingInfoProvider = (props: { children: JSX.Element }) => {
const [progress, setProgress] = createSignal(0);
const [duration, setDuration] = createSignal(0);
const [title, setTitle] = createSignal('Not Playing');
const [artist, setArtist] = createSignal('N/A');
const [status, setStatus] = createSignal('idle');
const [status, setStatus] = createSignal<Status>('idle');
const [coverUrl, setCoverUrl] = createSignal<string>();
const [lyrics, setLyrics] = createSignal<FlatMap<number, string[]> | null>(null);
const [originalData, setOriginalData] = createSignal<UpdateData | null>(null);
const [originalLyric, setOriginalLyric] = createSignal<LyricInfo | null>(null);

const [lyricMapper] = useLyricMapper();

const lyricMode = () => {
const data = originalData();
const mapper = lyricMapper();

if (!data) return 'auto';

const mode: number | undefined = mapper[`${data.title}:${data.cover_url ?? 'unknown'}`];
if (mode === undefined) return 'auto';
if (mode === ConfigLyricMode.NONE) return 'none';
if (mode === ConfigLyricMode.PLAYER) return 'player';

return 'manual';
};

const onUpdate = (_event: unknown, message: { data: UpdateData }) => {
const data: UpdateData = message.data;

Expand All @@ -85,7 +102,7 @@ const PlayingInfoProvider = (props: { children: JSX.Element }) => {
if (typeof data.cover_url === 'string' && /^(?:file|https?):\/\//.exec(data.cover_url)) {
setCoverUrl(data.cover_url);
} else {
setCoverUrl(IconMusic);
setCoverUrl(undefined);
}
};

Expand Down Expand Up @@ -126,30 +143,45 @@ const PlayingInfoProvider = (props: { children: JSX.Element }) => {
const coverDataURL = data.cover_url ?? 'unknown';

const id: number | undefined = mapper[`${data.title}:${coverDataURL}`];
const lyricInfo = await (async (): Promise<LyricInfo | null> => {
const alsongLyric = (
id
? await alsong.getLyricById(id).catch(() => null)
: await getLyric(data)
);

let lyricInfo: LyricInfo | null = null;
if (id === ConfigLyricMode.NONE) {
setLyrics(null);
} else if (id === ConfigLyricMode.PLAYER) {
if (data.lyrics) {
lyricInfo = {
useMapper: !!id,
kind: 'default',
data: {
...data,
lyric: data.lyrics,
},
} satisfies LyricInfo;
}
} else {
const alsongLyric = id
? await alsong.getLyricById(id).catch(() => null)
: await getLyric(data);

if (alsongLyric) {
return { useMapper: !!id, kind: 'alsong', data: alsongLyric } as const;
lyricInfo = {
useMapper: !!id,
kind: 'alsong',
data: alsongLyric,
} satisfies LyricInfo;
}

if (data.lyrics) {
return {
lyricInfo = {
useMapper: !!id,
kind: 'default',
data: {
...data,
lyric: data.lyrics,
},
} as const;
} satisfies LyricInfo;
}

return null;
})();
}

setOriginalLyric(lyricInfo);
if (lyricInfo?.data?.lyric) {
Expand All @@ -175,7 +207,8 @@ const PlayingInfoProvider = (props: { children: JSX.Element }) => {
lyrics,
originalData,
originalLyric,
} as PlayingInfo;
lyricMode,
} satisfies PlayingInfo;

return (
<PlayingInfoContext.Provider value={playingInfo}>
Expand Down
52 changes: 35 additions & 17 deletions renderer/components/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,42 @@ import { For, Show, createEffect, createSignal, mergeProps, onCleanup, onMount,

import { Transition } from 'solid-transition-group';

import { Dynamic } from 'solid-js/web';

import { cx } from '../utils/classNames';

import type { JSX } from 'solid-js/jsx-runtime';

export interface SelectProps extends Omit<JSX.HTMLAttributes<HTMLInputElement>, 'value' | 'onChange'> {
export interface SelectProps<T extends string> extends Omit<JSX.HTMLAttributes<HTMLElement>, 'value' | 'onChange'> {
mode?: 'select' | 'autocomplete';

placeholder?: string;
placement?: Placement;

options: string[];
value?: string;
onChange?: (value: string, index: number) => void;
format?: (value: string) => string;
options: T[];
value?: T;
onChange?: (value: T, index: number) => void;
format?: (value: T) => string;

popupClass?: string;
popupStyle?: string;

renderItem?: (props: JSX.HTMLAttributes<HTMLLIElement>, option: string, isSelected: boolean) => JSX.Element;
}
const Selector = (props: SelectProps) => {
const Selector = <T extends string>(props: SelectProps<T>) => {
const [local, popup, leftProps] = splitProps(
// eslint-disable-next-line solid/reactivity
mergeProps({ mode: 'select' }, props),
['format', 'options', 'value', 'onChange', 'renderItem', 'placement', 'mode'],
['popupClass', 'popupStyle']
);

const [keyword, setKeyword] = createSignal<string | null>(null);
const [open, setOpen] = createSignal(false);
const [anchor, setAnchor] = createSignal<HTMLInputElement>();
const [anchor, setAnchor] = createSignal<HTMLElement>();
const [popper, setPopper] = createSignal<HTMLUListElement>();
const [input, setInput] = createSignal<HTMLInputElement>();
const [input, setInput] = createSignal<HTMLElement>();
// eslint-disable-next-line solid/reactivity
const [options, setOptions] = createSignal(local.options);

/* properties */
Expand Down Expand Up @@ -97,7 +101,7 @@ const Selector = (props: SelectProps) => {
});

/* callbacks */
const onSelect = (option: string, index: number) => {
const onSelect = (option: T, index: number) => {
input()?.blur();
setKeyword(null);
setOpen(false);
Expand All @@ -117,14 +121,28 @@ const Selector = (props: SelectProps) => {
onClick={() => input()?.focus()}
data-active={open()}
>
<input
{...leftProps}
ref={setInput}
class={cx('select', leftProps.class)}
value={keyword() ?? local.format?.(local.value ?? '') ?? local.value}
onInput={(event) => setKeyword(event.target.value)}
onFocusIn={onOpen}
/>
<Show
when={local.mode === 'autocomplete'}
fallback={
<div
{...leftProps}
ref={setInput}
class={cx('select', leftProps.class)}
onClick={onOpen}
>
{keyword() ?? local.format?.(local.value ?? '' as T) ?? local.value}
</div>
}
>
<input
{...leftProps}
ref={setInput}
class={cx('select', leftProps.class)}
value={keyword() ?? local.format?.(local.value ?? '' as T) ?? local.value}
onInput={(event) => setKeyword(event.target.value)}
onFocusIn={onOpen}
/>
</Show>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
Expand Down
28 changes: 17 additions & 11 deletions renderer/lyrics/SideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { Trans, useTransContext } from '@jellybrick/solid-i18next';
import { Marquee } from '@suyongs/solid-utility';

import Card from '../components/Card';
import { usePlayingInfo } from '../components/PlayingInfoProvider';
import { LyricMode, usePlayingInfo } from '../components/PlayingInfoProvider';
import useLyric from '../hooks/useLyric';
import useLyricMapper from '../hooks/useLyricMapper';
import LyricProgressBar from '../main/components/LyricProgressBar';
import Selector from '../components/Select';
import { ConfigLyricMode } from '../../common/constants';

const SideBar = () => {
const { coverUrl, title, lyrics, originalLyric } = usePlayingInfo();
const { coverUrl, title, lyrics, originalLyric, lyricMode } = usePlayingInfo();
const [, lyricTime] = useLyric();
const [, setLyricMapper] = useLyricMapper();
const [t] = useTransContext();
Expand All @@ -32,9 +34,13 @@ const SideBar = () => {
});
});

const onResetLyric = () => {
const onChangeLyricMode = (mode: LyricMode) => {
let newValue: number | undefined = undefined;
if (mode === 'player') newValue = ConfigLyricMode.PLAYER;
if (mode === 'none') newValue = ConfigLyricMode.NONE;

const newMapper = {
[`${title()}:${coverUrl()}`]: undefined,
[`${title()}:${coverUrl() ?? 'unknown'}`]: newValue,
};

setLyricMapper(newMapper);
Expand All @@ -61,13 +67,13 @@ const SideBar = () => {
class={'w-full flex flex-row justify-start items-center gap-1'}
subCards={[
<div class={'w-full h-full flex items-center'}>
<button class={isMappedLyric() ? 'btn-primary' : 'btn-primary-disabled disabled'} onClick={onResetLyric} disabled={!isMappedLyric()}>
<Switch fallback={t('lyrics.auto-recognizing')}>
<Match when={isMappedLyric()}>
<Trans key={'lyrics.change-to-auto-recognize-mode'} />
</Match>
</Switch>
</button>
<Selector
mode={'select'}
options={['auto', 'player', 'none'] as LyricMode[]}
value={lyricMode()}
format={(mode) => t(`lyrics.mode.${mode}`)}
onChange={onChangeLyricMode}
/>
</div>,
]}
>
Expand Down
2 changes: 1 addition & 1 deletion renderer/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
@apply border-b-primary-500 border-b-2;
}
.select {
@apply outline-none bg-transparent w-fit;
@apply outline-none bg-transparent w-fit min-w-[20ch];
}

.spinner-shape {
Expand Down

0 comments on commit 36474f0

Please sign in to comment.