mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-14 17:41:20 +00:00
added audioplayer controls and fixed usePublish types
This commit is contained in:
parent
910f07b439
commit
dbd1e6adba
274
src/components/AudioPlayer/AudioPlayerControls.tsx
Normal file
274
src/components/AudioPlayer/AudioPlayerControls.tsx
Normal file
@ -0,0 +1,274 @@
|
||||
import React, {
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { QortalGetMetadata } from '../../types/interfaces/resources';
|
||||
import { useResourceStatus } from '../../hooks/useResourceStatus';
|
||||
|
||||
export interface OnTrackChangeMeta {
|
||||
hasNext: boolean;
|
||||
hasPrevious: boolean;
|
||||
}
|
||||
|
||||
export interface AudioPlayerProps {
|
||||
srcs: QortalGetMetadata[];
|
||||
currentTrack?: QortalGetMetadata;
|
||||
controls?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
sx?: object;
|
||||
loopCurrentTrack?: boolean;
|
||||
shuffle?: boolean;
|
||||
onTrackChange?: (track: QortalGetMetadata, meta: OnTrackChangeMeta) => void;
|
||||
onEndedAll?: () => void;
|
||||
onPlay?: () => void;
|
||||
onPause?: () => void;
|
||||
onEnded?: () => void;
|
||||
onError?: React.ReactEventHandler<HTMLAudioElement>;
|
||||
onProgress?: (currentTime: number, duration: number) => void;
|
||||
onResourceStatus?: (
|
||||
resourceStatus: ReturnType<typeof useResourceStatus>
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface AudioPlayerHandle {
|
||||
play: () => void;
|
||||
pause: () => void;
|
||||
stop: () => void;
|
||||
next: () => void;
|
||||
prev: () => void;
|
||||
setTrack: (track: QortalGetMetadata) => void;
|
||||
seekTo: (seconds: number) => void;
|
||||
setVolume: (level: number) => void;
|
||||
setMuted: (muted: boolean) => void;
|
||||
toggleMute: () => void;
|
||||
isPlaying: boolean;
|
||||
currentTrackIndex: number;
|
||||
audioEl: HTMLAudioElement | null;
|
||||
}
|
||||
|
||||
const AudioPlayerComponent = forwardRef<AudioPlayerHandle, AudioPlayerProps>(
|
||||
(
|
||||
{
|
||||
srcs,
|
||||
currentTrack,
|
||||
style,
|
||||
className,
|
||||
sx,
|
||||
loopCurrentTrack = false,
|
||||
shuffle = false,
|
||||
onTrackChange,
|
||||
onEndedAll,
|
||||
onPlay,
|
||||
onPause,
|
||||
onEnded,
|
||||
onError,
|
||||
onProgress,
|
||||
onResourceStatus,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const [shuffledOrder, setShuffledOrder] = useState<number[]>([]);
|
||||
const [shuffledIndex, setShuffledIndex] = useState<number>(0);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
|
||||
const isControlled = currentTrack !== undefined;
|
||||
const [activeTrack, setActiveTrack] = useState<QortalGetMetadata>(
|
||||
currentTrack || srcs[0]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isControlled && currentTrack) {
|
||||
setActiveTrack(currentTrack);
|
||||
}
|
||||
}, [currentTrack, isControlled]);
|
||||
|
||||
const resetShuffle = useCallback(() => {
|
||||
setShuffledOrder([]);
|
||||
setShuffledIndex(0);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
resetShuffle();
|
||||
}, [shuffle, resetShuffle, srcs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shuffle) {
|
||||
const indices = srcs.map((_, i) => i);
|
||||
for (let i = indices.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[indices[i], indices[j]] = [indices[j], indices[i]];
|
||||
}
|
||||
setShuffledOrder(indices);
|
||||
setShuffledIndex(0);
|
||||
setActiveTrack(srcs[indices[0]]);
|
||||
}
|
||||
}, [shuffle, srcs]);
|
||||
|
||||
const trackIndex = srcs.findIndex(
|
||||
(t) =>
|
||||
t.identifier === activeTrack?.identifier &&
|
||||
t.service === activeTrack?.service &&
|
||||
t.name === activeTrack?.name
|
||||
);
|
||||
const resourceStatus = useResourceStatus({
|
||||
resource: activeTrack || null,
|
||||
});
|
||||
const { isReady, resourceUrl } = resourceStatus;
|
||||
const hasNext = trackIndex < srcs.length - 1;
|
||||
const hasPrevious = trackIndex > 0;
|
||||
|
||||
const setTrack = (track: QortalGetMetadata) => {
|
||||
setActiveTrack(track);
|
||||
};
|
||||
|
||||
const play = () => audioRef.current?.play();
|
||||
const pause = () => audioRef.current?.pause();
|
||||
const stop = () => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
audioRef.current.currentTime = 0;
|
||||
setIsPlaying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const next = () => {
|
||||
if (shuffle) {
|
||||
const nextIndex = shuffledIndex + 1;
|
||||
if (nextIndex < shuffledOrder.length) {
|
||||
setShuffledIndex(nextIndex);
|
||||
setTrack(srcs[shuffledOrder[nextIndex]]);
|
||||
} else {
|
||||
onEndedAll?.();
|
||||
}
|
||||
} else if (hasNext) {
|
||||
setTrack(srcs[trackIndex + 1]);
|
||||
} else {
|
||||
onEndedAll?.();
|
||||
}
|
||||
};
|
||||
|
||||
const prev = () => {
|
||||
if (shuffle) {
|
||||
const prevIndex = shuffledIndex - 1;
|
||||
if (prevIndex >= 0) {
|
||||
setShuffledIndex(prevIndex);
|
||||
setTrack(srcs[shuffledOrder[prevIndex]]);
|
||||
}
|
||||
} else if (hasPrevious) {
|
||||
setTrack(srcs[trackIndex - 1]);
|
||||
}
|
||||
};
|
||||
|
||||
const seekTo = (seconds: number) => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.currentTime = seconds;
|
||||
}
|
||||
};
|
||||
|
||||
const setVolume = (level: number) => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.volume = Math.min(1, Math.max(0, level));
|
||||
}
|
||||
};
|
||||
|
||||
const setMuted = (muted: boolean) => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.muted = muted;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMute = () => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.muted = !audioRef.current.muted;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (audioRef.current && isReady && resourceUrl) {
|
||||
audioRef.current.pause();
|
||||
audioRef.current.src = resourceUrl;
|
||||
|
||||
audioRef.current.currentTime = 0;
|
||||
audioRef.current.play();
|
||||
}
|
||||
}, [resourceUrl, isReady]);
|
||||
|
||||
useEffect(() => {
|
||||
const index = srcs.findIndex(
|
||||
(t) =>
|
||||
t.identifier === activeTrack?.identifier &&
|
||||
t.service === activeTrack?.service &&
|
||||
t.name === activeTrack?.name
|
||||
);
|
||||
console.log('srcs2', srcs, activeTrack, index);
|
||||
if (index !== -1) {
|
||||
onTrackChange?.(activeTrack, {
|
||||
hasNext: index < srcs.length - 1,
|
||||
hasPrevious: index > 0,
|
||||
});
|
||||
}
|
||||
}, [activeTrack, srcs, onTrackChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onResourceStatus) {
|
||||
onResourceStatus(resourceStatus);
|
||||
}
|
||||
}, [onResourceStatus, resourceStatus]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
play,
|
||||
pause,
|
||||
stop,
|
||||
next,
|
||||
prev,
|
||||
setTrack,
|
||||
seekTo,
|
||||
setVolume,
|
||||
setMuted,
|
||||
toggleMute,
|
||||
isPlaying,
|
||||
currentTrackIndex: trackIndex,
|
||||
audioEl: audioRef.current,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Box className={className} sx={sx} style={style}>
|
||||
<audio
|
||||
ref={audioRef}
|
||||
loop={loopCurrentTrack}
|
||||
src={resourceUrl || undefined}
|
||||
onPlay={() => {
|
||||
setIsPlaying(true);
|
||||
onPlay?.();
|
||||
}}
|
||||
onPause={() => {
|
||||
setIsPlaying(false);
|
||||
onPause?.();
|
||||
}}
|
||||
onEnded={() => {
|
||||
setIsPlaying(false);
|
||||
onEnded?.();
|
||||
if (!loopCurrentTrack) next();
|
||||
}}
|
||||
onError={onError}
|
||||
onTimeUpdate={() => {
|
||||
const audio = audioRef.current;
|
||||
if (audio && onProgress) {
|
||||
onProgress(audio.currentTime, audio.duration || 0);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const AudioPlayerControls = React.memo(AudioPlayerComponent);
|
||||
|
54
src/components/AudioPlayer/useAudioPlayerHotkeys.tsx
Normal file
54
src/components/AudioPlayer/useAudioPlayerHotkeys.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { useEffect } from 'react';
|
||||
import { AudioPlayerHandle } from './AudioPlayerControls';
|
||||
|
||||
export const useAudioPlayerHotkeys = (
|
||||
ref: React.RefObject<AudioPlayerHandle | null>,
|
||||
isAudioPlayerAvalable: boolean
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (!ref?.current || !isAudioPlayerAvalable) return;
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const tag = (e.target as HTMLElement)?.tagName;
|
||||
const isTyping =
|
||||
tag === 'INPUT' ||
|
||||
tag === 'TEXTAREA' ||
|
||||
(e.target as HTMLElement)?.isContentEditable;
|
||||
if (isTyping) return;
|
||||
|
||||
const audio = ref.current;
|
||||
|
||||
switch (e.key) {
|
||||
case ' ':
|
||||
e.preventDefault();
|
||||
if (audio?.isPlaying) {
|
||||
audio.pause();
|
||||
} else {
|
||||
audio?.play();
|
||||
}
|
||||
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
audio?.seekTo((audio.audioEl?.currentTime || 0) - 5);
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
audio?.seekTo((audio.audioEl?.currentTime || 0) + 5);
|
||||
break;
|
||||
case 'm':
|
||||
case 'M':
|
||||
audio?.toggleMute();
|
||||
break;
|
||||
case 'n':
|
||||
case 'N':
|
||||
audio?.next();
|
||||
break;
|
||||
case 'p':
|
||||
case 'P':
|
||||
audio?.prev();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [ref, isAudioPlayerAvalable]);
|
||||
};
|
@ -11,11 +11,54 @@ interface StoredPublish {
|
||||
data: any;
|
||||
timestamp: number;
|
||||
}
|
||||
export const usePublish = (
|
||||
|
||||
type UsePublishWithMetadata = {
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
resource: { qortalMetadata: QortalMetadata; data: any } | null;
|
||||
hasResource: boolean | null;
|
||||
refetch: () => Promise<{
|
||||
hasResource: boolean | null;
|
||||
resource: { qortalMetadata: QortalMetadata; data: any } | null;
|
||||
error: string | null;
|
||||
}>
|
||||
fetchPublish: (metadataProp: QortalGetMetadata) => Promise<{
|
||||
hasResource: boolean | null;
|
||||
resource: { qortalMetadata: QortalMetadata; data: any } | null;
|
||||
error: string | null;
|
||||
}>;
|
||||
updatePublish: (publish: QortalGetMetadata, data: any) => Promise<void>;
|
||||
deletePublish: (publish: QortalGetMetadata) => Promise<boolean | undefined>;
|
||||
};
|
||||
|
||||
type UsePublishWithoutMetadata = {
|
||||
fetchPublish: (metadataProp: QortalGetMetadata) => Promise<{
|
||||
hasResource: boolean | null;
|
||||
resource: { qortalMetadata: QortalMetadata; data: any } | null;
|
||||
error: string | null;
|
||||
}>;
|
||||
updatePublish: (publish: QortalGetMetadata, data: any) => Promise<void>;
|
||||
deletePublish: (publish: QortalGetMetadata) => Promise<boolean | undefined>;
|
||||
};
|
||||
|
||||
export function usePublish(
|
||||
maxFetchTries: number,
|
||||
returnType: ReturnType,
|
||||
metadata: QortalGetMetadata
|
||||
): UsePublishWithMetadata;
|
||||
|
||||
export function usePublish(
|
||||
maxFetchTries?: number,
|
||||
returnType?: ReturnType,
|
||||
metadata?: null
|
||||
): UsePublishWithoutMetadata;
|
||||
|
||||
// ✅ Actual implementation (must be a `function`, not `const`)
|
||||
export function usePublish(
|
||||
maxFetchTries: number = 3,
|
||||
returnType: ReturnType = "JSON",
|
||||
metadata?: QortalGetMetadata
|
||||
) => {
|
||||
metadata?: QortalGetMetadata | null
|
||||
): UsePublishWithMetadata | UsePublishWithoutMetadata {
|
||||
const {auth, appInfo} = useGlobal()
|
||||
const username = auth?.name
|
||||
const appNameHashed = appInfo?.appNameHashed
|
||||
|
@ -3,6 +3,8 @@
|
||||
export { useResourceStatus } from './hooks/useResourceStatus';
|
||||
export { Spacer } from './common/Spacer';
|
||||
export { useModal } from './hooks/useModal';
|
||||
export { AudioPlayerControls , OnTrackChangeMeta, AudioPlayerProps} from './components/AudioPlayer/AudioPlayerControls';
|
||||
export { useAudioPlayerHotkeys } from './components/AudioPlayer/useAudioPlayerHotkeys';
|
||||
import './index.css'
|
||||
export { formatBytes, formatDuration } from './utils/numbers';
|
||||
export { createQortalLink } from './utils/qortal';
|
||||
|
Loading…
x
Reference in New Issue
Block a user