import { parser, State as LrcState, TrimOptios } from "../lrc-parser.js"; type InitArgs = Readonly<{ text: string; options: TrimOptios; select: number; }>; export const enum ActionType { parse, refresh, next, time, info, select, deleteTime, getState, } export interface IState extends LrcState { readonly currentTime: number; readonly currentIndex: number; readonly nextTime: number; readonly nextIndex: number; readonly selectIndex: number; } type Map$Type$Payload = { [key in keyof T]: U extends key ? { type: key; payload: T[key] } : never }[keyof T]; export type Action = Map$Type$Payload< { [ActionType.parse]: { text: string; options: TrimOptios }; [ActionType.refresh]: number; [ActionType.next]: number; [ActionType.time]: number; [ActionType.info]: { name: string; value: string }; [ActionType.select]: (index: number) => number; [ActionType.deleteTime]: undefined; [ActionType.getState]: (state: IState) => void; }, ActionType >; export const guard = (value: number, min: number, max: number): number => { if (value < min) { return min; } if (value > max) { return max; } return value; }; const mergeObject = (target: T, obj: O): T => { for (const i in obj) { if (target[i] !== obj[i]) { return { ...target, ...obj }; } } return target; }; const reducer = (state: IState, action: Action): IState => { switch (action.type) { case ActionType.parse: { const { text, options } = action.payload; const lrc = parser(text, options); const selectIndex = guard(state.selectIndex, 0, lrc.lyric.length - 1); return { ...state, ...lrc, selectIndex }; } case ActionType.refresh: { const audioTime = action.payload; if (audioTime >= state.currentTime && audioTime < state.nextTime) { return state; } const record = state.lyric.reduce( (p, c, i) => { if (c.time) { if (c.time < p.nextTime && c.time > audioTime) { p.nextTime = c.time; p.nextIndex = i; } if (c.time > p.currentTime && c.time <= audioTime) { p.currentTime = c.time; p.currentIndex = i; } } return p; }, { currentTime: -Infinity, currentIndex: -Infinity, nextTime: Infinity, nextIndex: Infinity, }, ); return mergeObject(state, record); } case ActionType.next: { const index = state.selectIndex; const lyric = state.lyric; const selectIndex = guard(index + 1, 0, lyric.length - 1); return { ...reducer(state, { type: ActionType.time, payload: action.payload, }), selectIndex, }; } case ActionType.time: { const time = action.payload; const index = state.selectIndex; let lyric = state.lyric; if (lyric[index].time !== time) { const newLyric = lyric.slice(); newLyric[index] = { text: lyric[index].text, time }; lyric = newLyric; } return { ...state, lyric, currentTime: time, nextTime: -Infinity }; } case ActionType.info: { const { name, value } = action.payload; const info = new Map(state.info); if (value.trim() === "") { info.delete(name); } else { info.set(name, value.trim()); } return { ...state, info, }; } case ActionType.select: { const selectIndex = guard(action.payload(state.selectIndex), 0, state.lyric.length - 1); return state.selectIndex === selectIndex ? state : { ...state, selectIndex }; } case ActionType.deleteTime: { const { selectIndex, currentIndex } = state; let lyric = state.lyric; if (lyric[selectIndex].time !== undefined) { const newLyric = lyric.slice(); newLyric[selectIndex] = { text: lyric[selectIndex].text }; lyric = newLyric; let { currentTime, nextTime } = state; if (selectIndex === currentIndex) { currentTime = Infinity; nextTime = -Infinity; } return { ...state, lyric, currentTime, nextTime, }; } return state; } case ActionType.getState: { action.payload(state); return state; } } return state; }; const init = (lazyInit: () => InitArgs): IState => { const { text, options, select } = lazyInit(); return { ...parser(text, options), currentTime: Infinity, currentIndex: Infinity, nextTime: -Infinity, nextIndex: -Infinity, selectIndex: select, }; }; export const useLrc = (lazyInit: () => InitArgs): [IState, React.Dispatch] => React.useReducer(reducer, lazyInit, init);