import { Action, ActionType, IState } from "../hooks/useLrc.js";
import { State as PrefState } from "../hooks/usePref.js";
import { convertTimeToTag, formatText, ILyric } from "../lrc-parser.js";
import { audioRef, currentTimePubSub } from "../utils/audiomodule.js";
import { isKeyboardElement } from "../utils/is-keyboard-element.js";
import { appContext } from "./app.context.js";
import { AsidePanel } from "./asidepanel.js";
import { Curser } from "./curser.js";
const { useCallback, useContext, useEffect, useRef, useState } = React;
const SpaceButton: React.FC<{ sync: () => void }> = ({ sync }) => {
return (
);
};
export const enum SyncMode {
select,
highlight,
}
interface ISynchronizerProps {
state: IState;
dispatch: React.Dispatch;
}
export const Synchronizer: React.FC = ({ state, dispatch }) => {
const self = useRef(Symbol(Synchronizer.name));
const { selectIndex, currentIndex: highlightIndex, lyric } = state;
const { prefState, lang } = useContext(appContext);
useEffect(() => {
dispatch({
type: ActionType.info,
payload: {
name: "tool",
value: `${lang.app.name} https://lrc-maker.github.io`,
},
});
}, [dispatch, lang]);
const [syncMode, setSyncMode] = useState(() =>
sessionStorage.getItem(SSK.syncMode) === SyncMode.highlight.toString() ? SyncMode.highlight : SyncMode.select,
);
useEffect(() => {
sessionStorage.setItem(SSK.syncMode, syncMode.toString());
}, [syncMode]);
const ul = useRef(null);
const needScrollLine = {
[SyncMode.select]: selectIndex,
[SyncMode.highlight]: highlightIndex,
}[syncMode];
useEffect(() => {
const line = ul.current?.children[needScrollLine];
if (line !== undefined) {
line.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "center",
});
}
}, [needScrollLine]);
useEffect(() => {
return currentTimePubSub.sub(self.current, (time) => {
dispatch({ type: ActionType.refresh, payload: time });
});
}, [dispatch]);
const sync = useCallback(() => {
if (!audioRef.duration) {
return;
}
dispatch({
type: ActionType.next,
payload: audioRef.currentTime,
});
}, [dispatch]);
const adjust = useCallback(
(ev: KeyboardEvent | React.MouseEvent, offset: number, index: number) => {
if (!audioRef.duration) {
return;
}
const selectTime = lyric[index]?.time;
if (selectTime === undefined) {
return;
}
dispatch({
type: ActionType.time,
payload: audioRef.step(ev, offset, selectTime),
});
},
[dispatch, lyric],
);
useEffect(() => {
const listener = (ev: KeyboardEvent): void => {
const { code, key, target } = ev;
const codeOrKey = code || key;
if (isKeyboardElement(target)) {
return;
}
if (codeOrKey === "Backspace" || codeOrKey === "Delete" || codeOrKey === "Del") {
ev.preventDefault();
dispatch({
type: ActionType.deleteTime,
payload: undefined,
});
return;
}
if (code === "Digit0" || key === "0") {
ev.preventDefault();
adjust(ev, 0, selectIndex);
return;
}
if (code === "Minus" || key === "-" || key === "_") {
ev.preventDefault();
adjust(ev, -0.5, selectIndex);
return;
}
if (code === "Equal" || key === "+" || key === "=") {
ev.preventDefault();
adjust(ev, 0.5, selectIndex);
return;
}
if (ev.metaKey === true || ev.ctrlKey === true) {
return;
}
if (code === "Space" || key === " " || key === "Spacebar") {
ev.preventDefault();
sync();
} else if (["ArrowUp", "KeyW", "KeyJ", "Up", "W", "w", "J", "j"].includes(codeOrKey)) {
ev.preventDefault();
dispatch({ type: ActionType.select, payload: (index) => index - 1 });
} else if (["ArrowDown", "KeyS", "KeyK", "Down", "S", "s", "K", "k"].includes(codeOrKey)) {
ev.preventDefault();
dispatch({ type: ActionType.select, payload: (index) => index + 1 });
} else if (codeOrKey === "Home") {
ev.preventDefault();
dispatch({ type: ActionType.select, payload: () => 0 });
} else if (codeOrKey === "End") {
ev.preventDefault();
dispatch({ type: ActionType.select, payload: () => Infinity });
} else if (codeOrKey === "PageUp") {
ev.preventDefault();
dispatch({ type: ActionType.select, payload: (index) => index - 10 });
} else if (codeOrKey === "PageDown") {
ev.preventDefault();
dispatch({ type: ActionType.select, payload: (index) => index + 10 });
}
};
document.addEventListener("keydown", listener);
return (): void => {
document.removeEventListener("keydown", listener);
};
}, [adjust, dispatch, selectIndex, sync]);
const onLineClick = useCallback(
(ev: React.MouseEvent) => {
ev.stopPropagation();
const target = ev.target as HTMLElement;
if (target.classList.contains("line")) {
const lineKey = Number.parseInt(target.dataset.key!, 10) || 0;
dispatch({ type: ActionType.select, payload: () => lineKey });
}
},
[dispatch],
);
const onLineDoubleClick = useCallback(
(ev: React.MouseEvent) => {
ev.stopPropagation();
if (!audioRef.duration) {
return;
}
const target = ev.target as HTMLElement;
if (target.classList.contains("line")) {
const key = Number.parseInt(target.dataset.key!, 10);
adjust(ev, 0, key);
}
},
[adjust],
);
const LyricLineIter = useCallback(
(line: Readonly, index: number, lines: readonly ILyric[]) => {
const select = index === selectIndex;
const highlight = index === highlightIndex;
const error = index > 0 && lines[index].time! <= lines[index - 1].time!;
const className = Object.entries({
line: true,
select,
highlight,
error,
})
.reduce((p, [name, value]) => {
if (value) {
p.push(name);
}
return p;
}, [] as string[])
.join(Const.space);
return (
);
},
[selectIndex, highlightIndex, prefState],
);
const ulClassName = prefState.screenButton ? "lyric-list on-screen-button" : "lyric-list";
return (
<>
{state.lyric.map(LyricLineIter)}
{prefState.screenButton && }
>
);
};
interface ILyricLineProps {
line: ILyric;
index: number;
select: boolean;
className: string;
prefState: PrefState;
}
const LyricLine: React.FC = ({ line, index, select, className, prefState }) => {
const lineTime = convertTimeToTag(line.time, prefState.fixed);
const lineText = formatText(line.text, prefState.spaceStart, prefState.spaceEnd);
return (
{select && }
{lineText}
);
};