Files
QZMusic_PC/amll-local/packages/vue/src/LyricPlayer.tsx

388 lines
11 KiB
TypeScript
Raw Normal View History

import {
type BaseRenderer,
LyricPlayer as CoreLyricPlayer,
type LyricLine,
type LyricLineMouseEvent,
type LyricPlayerBase,
MaskObsceneWordsMode,
type OptimizeLyricOptions,
type spring,
} from "@applemusic-like-lyrics/core";
import {
computed,
defineComponent,
type ExtractPublicPropTypes,
onMounted,
onUnmounted,
type PropType,
type Ref,
ref,
type ShallowRef,
type SlotsType,
Teleport,
useTemplateRef,
watch,
watchEffect,
} from "vue";
const lyricPlayerProps = {
/**
* `false`
*
* `update`
*/
disabled: {
type: Boolean,
default: false,
},
/**
* `true`
*/
playing: {
type: Boolean,
default: true,
},
/**
* `undefined` `center`
*
* - `top`
* - `bottom`
* - `center`
*/
alignAnchor: {
type: String as PropType<"top" | "bottom" | "center">,
default: "center",
},
/**
* `undefined`
* `0.5`
*
* `[0.0-1.0]`
*/
alignPosition: {
type: Number,
default: 0.5,
},
/**
* 使
*
*
*
* 退 `transition`
*/
enableSpring: {
type: Boolean,
default: true,
},
/**
* `true`
*/
enableBlur: {
type: Boolean,
default: true,
},
/**
* 使
*
*
*
* 退 `transition`
*/
enableScale: {
type: Boolean,
default: true,
},
/**
*
*/
hidePassedLines: {
type: Boolean,
default: false,
},
/**
* `MaskObsceneWordsMode.Disabled`
*/
maskObsceneWordsMode: {
type: String as PropType<MaskObsceneWordsMode>,
default: MaskObsceneWordsMode.Disabled,
},
/**
*
*/
optimizeOptions: {
type: Object as PropType<OptimizeLyricOptions>,
required: false,
},
/**
*
*/
lyricLines: {
type: Object as PropType<LyricLine[]>,
required: false,
},
/**
* ****
*
*/
currentTime: {
type: Number,
default: 0,
},
/**
* 0.5
*
* Apple Music for Android 1
*
* Apple Music for iPad 0.5
*
* 0 `0.0001` ** 0**
*/
wordFadeWidth: {
type: Number,
default: 0.5,
},
/**
*
*
* @param params
*/
linePosXSpringParams: {
type: Object as PropType<Partial<spring.SpringParams>>,
required: false,
},
/**
*
*
* @param params
*/
linePosYSpringParams: {
type: Object as PropType<Partial<spring.SpringParams>>,
required: false,
},
/**
*
*
* @param params
*/
lineScaleSpringParams: {
type: Object as PropType<Partial<spring.SpringParams>>,
required: false,
},
/**
* `undefined` `MeshGradientRenderer`
*
*/
lyricPlayer: {
type: Object as PropType<{
new (...args: ConstructorParameters<typeof BaseRenderer>): BaseRenderer;
}>,
required: false,
},
} as const;
/**
*
*/
export type LyricPlayerProps = ExtractPublicPropTypes<typeof lyricPlayerProps>;
const lyricPlayerEmits = {
lineClick: (_: LyricLineMouseEvent) => true,
lineContextmenu: (_: LyricLineMouseEvent) => true,
} as const;
/**
*
*/
export type LyricPlayerEmits = typeof lyricPlayerEmits;
/**
*
*/
export interface LyricPlayerRef {
/**
*
*/
lyricPlayer: Ref<LyricPlayerBase | undefined>;
/**
* DIV
*/
wrapperEl: Readonly<ShallowRef<HTMLDivElement | null>>;
}
export const LyricPlayer = defineComponent({
name: "LyricPlayer",
props: lyricPlayerProps,
emits: lyricPlayerEmits,
slots: Object as SlotsType<{
"bottom-line": () => void;
}>,
setup(props, { expose, emit, attrs, slots }) {
const wrapperRef = useTemplateRef<HTMLDivElement>("wrapper-ref");
const playerRef = ref<CoreLyricPlayer>();
const lineClickHandler = (e: Event) =>
emit("lineClick", e as LyricLineMouseEvent);
const lineContextMenuHandler = (e: Event) =>
emit("lineContextmenu", e as LyricLineMouseEvent);
onMounted(() => {
const wrapper = wrapperRef.value;
if (wrapper) {
playerRef.value = new CoreLyricPlayer();
wrapper.appendChild(playerRef.value.getElement());
playerRef.value.addEventListener("line-click", lineClickHandler);
playerRef.value.addEventListener(
"line-contextmenu",
lineContextMenuHandler,
);
}
});
onUnmounted(() => {
if (playerRef.value) {
playerRef.value.removeEventListener("line-click", lineClickHandler);
playerRef.value.removeEventListener(
"line-contextmenu",
lineContextMenuHandler,
);
playerRef.value.dispose();
}
});
watchEffect((onCleanup) => {
if (!props.disabled) {
let canceled = false;
let lastTime = -1;
const onFrame = (time: number) => {
if (canceled) return;
if (lastTime === -1) {
lastTime = time;
}
playerRef.value?.update(time - lastTime);
lastTime = time;
requestAnimationFrame(onFrame);
};
requestAnimationFrame(onFrame);
onCleanup(() => {
canceled = true;
});
}
});
watchEffect(() => {
if (props.playing !== undefined) {
if (props.playing) {
playerRef.value?.resume();
} else {
playerRef.value?.pause();
}
} else playerRef.value?.resume();
});
watchEffect(() => {
if (props.alignAnchor !== undefined)
playerRef.value?.setAlignAnchor(props.alignAnchor);
});
watchEffect(() => {
if (props.hidePassedLines !== undefined)
playerRef.value?.setHidePassedLines(props.hidePassedLines);
});
watchEffect(() => {
if (props.maskObsceneWordsMode !== undefined)
playerRef.value?.setMaskObsceneWords(props.maskObsceneWordsMode);
else playerRef.value?.setMaskObsceneWords(MaskObsceneWordsMode.Disabled);
});
watchEffect(() => {
if (props.alignPosition !== undefined)
playerRef.value?.setAlignPosition(props.alignPosition);
});
watchEffect(() => {
if (props.enableSpring !== undefined)
playerRef.value?.setEnableSpring(props.enableSpring);
else playerRef.value?.setEnableSpring(true);
});
watchEffect(() => {
if (props.enableBlur !== undefined)
playerRef.value?.setEnableBlur(props.enableBlur);
else playerRef.value?.setEnableBlur(true);
});
watchEffect(() => {
if (props.enableScale !== undefined)
playerRef.value?.setEnableScale(props.enableScale);
else playerRef.value?.setEnableScale(true);
});
watch(
[playerRef, () => props.lyricLines, () => props.optimizeOptions],
([player, lyricLines, optimizeOptions]) => {
if (!player) return;
if (optimizeOptions !== undefined) {
player.setOptimizeOptions(optimizeOptions);
}
if (lyricLines !== undefined) {
player.setLyricLines(lyricLines);
} else {
player.setLyricLines([]);
}
if (props.currentTime !== undefined) {
player.setCurrentTime(props.currentTime, true);
}
},
{ immediate: true },
);
watchEffect(() => {
if (props.currentTime !== undefined)
playerRef.value?.setCurrentTime(props.currentTime);
});
watchEffect(() => {
if (props.wordFadeWidth !== undefined)
playerRef.value?.setWordFadeWidth(props.wordFadeWidth);
});
watchEffect(() => {
if (props.linePosXSpringParams !== undefined)
playerRef.value?.setLinePosXSpringParams(props.linePosXSpringParams);
});
watchEffect(() => {
if (props.linePosYSpringParams !== undefined)
playerRef.value?.setLinePosYSpringParams(props.linePosYSpringParams);
});
watchEffect(() => {
if (props.lineScaleSpringParams !== undefined)
playerRef.value?.setLineScaleSpringParams(props.lineScaleSpringParams);
});
const bottomLineEl = computed(() =>
playerRef.value?.getBottomLineElement(),
);
expose<LyricPlayerRef>({
lyricPlayer: playerRef,
wrapperEl: wrapperRef,
});
return () => (
<div ref="wrapper-ref" {...attrs}>
{bottomLineEl.value && (
<Teleport to={bottomLineEl.value}>
{slots["bottom-line"]?.()}
</Teleport>
)}
</div>
);
},
});