refactor audio to hook
This commit is contained in:
parent
a41df38a28
commit
2f24f53311
33
package-lock.json
generated
33
package-lock.json
generated
@ -46,6 +46,7 @@
|
|||||||
"kysely-codegen": "^0.13.1",
|
"kysely-codegen": "^0.13.1",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.3.0",
|
"tailwindcss": "^3.3.0",
|
||||||
|
"type-fest": "^4.12.0",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -4267,6 +4268,18 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/boxen/node_modules/type-fest": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/boxen/node_modules/wrap-ansi": {
|
"node_modules/boxen/node_modules/wrap-ansi": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
@ -6811,6 +6824,18 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/globals/node_modules/type-fest": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/globalthis": {
|
"node_modules/globalthis": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
|
||||||
@ -10967,12 +10992,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/type-fest": {
|
"node_modules/type-fest": {
|
||||||
"version": "0.20.2",
|
"version": "4.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.12.0.tgz",
|
||||||
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
"integrity": "sha512-5Y2/pp2wtJk8o08G0CMkuFPCO354FGwk/vbidxrdhRGZfd0tFnb4Qb8anp9XxXriwBgVPjdWbKpGl4J9lJY2jQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=16"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
@ -52,6 +52,7 @@
|
|||||||
"kysely-codegen": "^0.13.1",
|
"kysely-codegen": "^0.13.1",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.3.0",
|
"tailwindcss": "^3.3.0",
|
||||||
|
"type-fest": "^4.12.0",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,8 @@ import { ChuniAvatar } from '@/components/chuni/avatar';
|
|||||||
import { CHUNI_VOICE_LINES } from '@/helpers/chuni/voice';
|
import { CHUNI_VOICE_LINES } from '@/helpers/chuni/voice';
|
||||||
import { PlayIcon, StopIcon } from '@heroicons/react/24/solid';
|
import { PlayIcon, StopIcon } from '@heroicons/react/24/solid';
|
||||||
import { SaveIcon } from '@/components/save-icon';
|
import { SaveIcon } from '@/components/save-icon';
|
||||||
|
import { useAudio } from '@/helpers/use-audio';
|
||||||
|
import { useIsMounted } from 'usehooks-ts';
|
||||||
|
|
||||||
export type ChuniUserboxProps = {
|
export type ChuniUserboxProps = {
|
||||||
profile: ChuniUserData,
|
profile: ChuniUserData,
|
||||||
@ -44,12 +46,20 @@ export const ChuniUserbox = ({ profile, userboxItems }: ChuniUserboxProps) => {
|
|||||||
.find(i => ('id' in i ? i.id : i.avatarAccessoryId) === profile?.[profileKey])])) as EquippedItem);
|
.find(i => ('id' in i ? i.id : i.avatarAccessoryId) === profile?.[profileKey])])) as EquippedItem);
|
||||||
const [equipped, setEquipped] = useState<EquippedItem>(initialEquipped.current);
|
const [equipped, setEquipped] = useState<EquippedItem>(initialEquipped.current);
|
||||||
const [saved, setSaved] = useState<SavedItem>(Object.fromEntries(Object.keys(ITEM_KEYS).map(k => [k, true])) as any);
|
const [saved, setSaved] = useState<SavedItem>(Object.fromEntries(Object.keys(ITEM_KEYS).map(k => [k, true])) as any);
|
||||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
|
||||||
const [playingVoice, setPlayingVoice] = useState(false);
|
const [playingVoice, setPlayingVoice] = useState(false);
|
||||||
const [selectedLine, setSelectedLine] = useState(new Set(['0035']));
|
const [selectedLine, setSelectedLine] = useState(new Set(['0035']));
|
||||||
const [playPreviews, setPlayPreviews] = useState(true);
|
const [playPreviews, _setPlayPreviews] = useState(true);
|
||||||
const [selectingVoice, setSelectingVoice] = useState<EquippedItem['systemVoice'] | null>(null);
|
const [selectingVoice, setSelectingVoice] = useState<EquippedItem['systemVoice'] | null>(null);
|
||||||
|
|
||||||
|
const setPlayPreviews = (play: boolean) => {
|
||||||
|
_setPlayPreviews(play);
|
||||||
|
localStorage.setItem('chuni-userbox-play-previews', play ? '1' : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
_setPlayPreviews(!!(localStorage.getItem('chuni-userbox-play-previews') ?? 1));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const equipItem = <K extends keyof RequiredUserbox>(k: K, item: RequiredUserbox[K][number] | undefined | null) => {
|
const equipItem = <K extends keyof RequiredUserbox>(k: K, item: RequiredUserbox[K][number] | undefined | null) => {
|
||||||
if (!item || equipped[k] === item) return;
|
if (!item || equipped[k] === item) return;
|
||||||
|
|
||||||
@ -63,28 +73,11 @@ export const ChuniUserbox = ({ profile, userboxItems }: ChuniUserboxProps) => {
|
|||||||
.entries(initialEquipped.current).filter(([k]) => items.includes(k as any))) }))
|
.entries(initialEquipped.current).filter(([k]) => items.includes(k as any))) }))
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const audioRef = useAudio({
|
||||||
const audio = audioRef.current;
|
play: () => setPlayingVoice(true),
|
||||||
if (!audio) return;
|
ended: () => setPlayingVoice(false),
|
||||||
audio.volume = 0.25;
|
pause: () => setPlayingVoice(false)
|
||||||
|
}, audio => audio.volume = 0.25);
|
||||||
const setPlay = () => {
|
|
||||||
setPlayingVoice(true);
|
|
||||||
};
|
|
||||||
const setStop = () => {
|
|
||||||
setPlayingVoice(false);
|
|
||||||
};
|
|
||||||
audio.addEventListener('play', setPlay);
|
|
||||||
audio.addEventListener('ended', setStop);
|
|
||||||
audio.addEventListener('pause', setStop)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
audio.removeEventListener('play', setPlay);
|
|
||||||
audio.removeEventListener('ended', setStop);
|
|
||||||
audio.removeEventListener('pause', setStop);
|
|
||||||
audio.pause();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const play = (src: string) => {
|
const play = (src: string) => {
|
||||||
if (!audioRef.current || !playPreviews) return;
|
if (!audioRef.current || !playPreviews) return;
|
||||||
@ -202,8 +195,6 @@ export const ChuniUserbox = ({ profile, userboxItems }: ChuniUserboxProps) => {
|
|||||||
|
|
||||||
{/* begin system voice */}
|
{/* begin system voice */}
|
||||||
<div className="flex flex-col p-4 w-full col-span-full xl:col-span-6 sm:bg-content2 rounded-lg sm:shadow-inner items-center">
|
<div className="flex flex-col p-4 w-full col-span-full xl:col-span-6 sm:bg-content2 rounded-lg sm:shadow-inner items-center">
|
||||||
<audio ref={audioRef} />
|
|
||||||
|
|
||||||
<div className="text-2xl font-semibold mb-4 px-2 flex w-full h-10">
|
<div className="text-2xl font-semibold mb-4 px-2 flex w-full h-10">
|
||||||
Voice
|
Voice
|
||||||
|
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import { Button, Card, CardBody, Slider } from '@nextui-org/react';
|
import { Button, Card, CardBody, Slider } from '@nextui-org/react';
|
||||||
import { PauseCircleIcon, PlayCircleIcon } from '@heroicons/react/24/solid';
|
import { PauseCircleIcon, PlayCircleIcon } from '@heroicons/react/24/solid';
|
||||||
import { ReactNode, useEffect, useRef, useState } from 'react';
|
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useAudio } from '@/helpers/use-audio';
|
||||||
|
|
||||||
export type MusicPlayerProps = {
|
export type MusicPlayerProps = {
|
||||||
audio: string,
|
audio: string,
|
||||||
@ -22,42 +23,24 @@ export const MusicPlayer = ({ audio, image, children, className }: MusicPlayerPr
|
|||||||
const [duration, setDuration] = useState(NaN);
|
const [duration, setDuration] = useState(NaN);
|
||||||
const [progress, setProgress] = useState(NaN);
|
const [progress, setProgress] = useState(NaN);
|
||||||
const [playing, setPlaying] = useState(false);
|
const [playing, setPlaying] = useState(false);
|
||||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const audio = audioRef.current;
|
|
||||||
if (!audio) return;
|
|
||||||
|
|
||||||
|
const audioRef = useAudio(audio, {
|
||||||
|
loadedmetadata() {
|
||||||
|
if (this.duration !== undefined)
|
||||||
|
setDuration(this.duration);
|
||||||
|
setProgress(0);
|
||||||
|
},
|
||||||
|
timeupdate() {
|
||||||
|
if (this.currentTime !== undefined)
|
||||||
|
setProgress(this.currentTime);
|
||||||
|
},
|
||||||
|
ended: () => setPlaying(false)
|
||||||
|
}, audio => {
|
||||||
if (!Number.isNaN(audio.duration)) {
|
if (!Number.isNaN(audio.duration)) {
|
||||||
setDuration(audio.duration)
|
setDuration(audio.duration)
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
const metadata = () => {
|
|
||||||
if (audio.duration !== undefined)
|
|
||||||
setDuration(audio.duration);
|
|
||||||
setProgress(0);
|
|
||||||
};
|
|
||||||
audio.addEventListener('loadedmetadata', metadata);
|
|
||||||
|
|
||||||
const timeupdate = () => {
|
|
||||||
if (audio.currentTime !== undefined)
|
|
||||||
setProgress(audio.currentTime);
|
|
||||||
};
|
|
||||||
audio.addEventListener('timeupdate', timeupdate);
|
|
||||||
|
|
||||||
const ended = () => {
|
|
||||||
setPlaying(false);
|
|
||||||
}
|
|
||||||
audio.addEventListener('ended', ended);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
audio.removeEventListener('loadedmetadata', metadata);
|
|
||||||
audio.removeEventListener('timeupdate', timeupdate);
|
|
||||||
audio.removeEventListener('ended', ended);
|
|
||||||
audio.pause();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (playing)
|
if (playing)
|
||||||
@ -70,8 +53,6 @@ export const MusicPlayer = ({ audio, image, children, className }: MusicPlayerPr
|
|||||||
|
|
||||||
return (<Card isBlurred radius="none" className={`border-none shadow-lg sm:rounded-2xl w-full max-w-full sm:max-w-[48rem] ${className ?? ''}`}>
|
return (<Card isBlurred radius="none" className={`border-none shadow-lg sm:rounded-2xl w-full max-w-full sm:max-w-[48rem] ${className ?? ''}`}>
|
||||||
<CardBody className="sm:rounded-2xl sm:p-4 bg-content1 sm:bg-content2">
|
<CardBody className="sm:rounded-2xl sm:p-4 bg-content1 sm:bg-content2">
|
||||||
<audio src={audio} ref={audioRef} />
|
|
||||||
|
|
||||||
<div className="grid grid-cols-12">
|
<div className="grid grid-cols-12">
|
||||||
<div className="col-span-full sm:col-span-4 h-full flex items-center justify-center sm:justify-start">
|
<div className="col-span-full sm:col-span-4 h-full flex items-center justify-center sm:justify-start">
|
||||||
<img src={image} alt="" className="aspect-square rounded-md shadow-2xl max-w-56 w-full border border-gray-500 sm:border-0" />
|
<img src={image} alt="" className="aspect-square rounded-md shadow-2xl max-w-56 w-full border border-gray-500 sm:border-0" />
|
||||||
|
55
src/helpers/use-audio.ts
Normal file
55
src/helpers/use-audio.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { RefObject, useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
type UseAudioEvents = Partial<{
|
||||||
|
readonly [K in keyof HTMLMediaElementEventMap]: ((this: HTMLAudioElement, ev: HTMLMediaElementEventMap[K]) => any) |
|
||||||
|
[(this: HTMLAudioElement, ev: HTMLMediaElementEventMap[K]) => any,
|
||||||
|
options?: boolean | AddEventListenerOptions]
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type UseAudioCallback = (audio: HTMLAudioElement) => any;
|
||||||
|
|
||||||
|
type UseAudio = {
|
||||||
|
(src: string, events: UseAudioEvents, cb?: UseAudioCallback): RefObject<HTMLAudioElement | null>,
|
||||||
|
(events: UseAudioEvents, cb?: UseAudioCallback): RefObject<HTMLAudioElement | null>
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAudio: UseAudio = (srcOrEvents, eventsOrCb, cb?) => {
|
||||||
|
const src = typeof srcOrEvents === 'string' ? srcOrEvents : undefined;
|
||||||
|
const events = typeof eventsOrCb === 'object' ? eventsOrCb : (srcOrEvents as UseAudioEvents);
|
||||||
|
const callback = useCallback(typeof cb === 'function' ? cb : eventsOrCb as UseAudioCallback, []);
|
||||||
|
|
||||||
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!audioRef.current)
|
||||||
|
audioRef.current = new Audio(src);
|
||||||
|
const audio = audioRef.current;
|
||||||
|
|
||||||
|
if (audio.src !== src && src) audio.src = src;
|
||||||
|
|
||||||
|
Object.entries(events).forEach(([name, listener]) => {
|
||||||
|
if (typeof listener === 'function')
|
||||||
|
audio.addEventListener(name, listener as any);
|
||||||
|
else
|
||||||
|
audio.addEventListener(name, ...(listener as [any, any]));
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
Object.entries(events).forEach(([name, listener]) => {
|
||||||
|
if (typeof listener === 'function')
|
||||||
|
audio.removeEventListener(name, listener as any);
|
||||||
|
else
|
||||||
|
audio.removeEventListener(name, ...(listener as [any, any]));
|
||||||
|
});
|
||||||
|
audio.pause();
|
||||||
|
}
|
||||||
|
}, [src]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (callback && audioRef.current) callback(audioRef.current);
|
||||||
|
}, [callback]);
|
||||||
|
|
||||||
|
return audioRef;
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user