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;
|
data: any;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
export const usePublish = (
|
|
||||||
maxFetchTries: number = 3,
|
type UsePublishWithMetadata = {
|
||||||
returnType: ReturnType = "JSON",
|
isLoading: boolean;
|
||||||
metadata?: QortalGetMetadata
|
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 | null
|
||||||
|
): UsePublishWithMetadata | UsePublishWithoutMetadata {
|
||||||
const {auth, appInfo} = useGlobal()
|
const {auth, appInfo} = useGlobal()
|
||||||
const username = auth?.name
|
const username = auth?.name
|
||||||
const appNameHashed = appInfo?.appNameHashed
|
const appNameHashed = appInfo?.appNameHashed
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
export { useResourceStatus } from './hooks/useResourceStatus';
|
export { useResourceStatus } from './hooks/useResourceStatus';
|
||||||
export { Spacer } from './common/Spacer';
|
export { Spacer } from './common/Spacer';
|
||||||
export { useModal } from './hooks/useModal';
|
export { useModal } from './hooks/useModal';
|
||||||
|
export { AudioPlayerControls , OnTrackChangeMeta, AudioPlayerProps} from './components/AudioPlayer/AudioPlayerControls';
|
||||||
|
export { useAudioPlayerHotkeys } from './components/AudioPlayer/useAudioPlayerHotkeys';
|
||||||
import './index.css'
|
import './index.css'
|
||||||
export { formatBytes, formatDuration } from './utils/numbers';
|
export { formatBytes, formatDuration } from './utils/numbers';
|
||||||
export { createQortalLink } from './utils/qortal';
|
export { createQortalLink } from './utils/qortal';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user