0c29506402
- Extract utility functions from component files into dedicated modules to fix react-refresh/only-export-components warnings: - parseBadgeDefinition -> src/lib/parseBadgeDefinition.ts - parseProfileBadges -> src/lib/parseProfileBadges.ts - getColors, paletteToTheme -> src/lib/colorMomentUtils.ts - parseDimToAspectRatio, eventToMediaItem -> src/lib/mediaUtils.ts - isAudioUrl, isImageUrl, isVideoUrl -> src/lib/mediaTypeDetection.ts - buildKindOptions, parseSelectedKinds -> src/lib/feedFilterUtils.ts - useVideoThumbnail -> src/hooks/useVideoThumbnail.ts - useEnvelopeDimensions -> src/hooks/useEnvelopeDimensions.ts - usePortalContainer -> src/hooks/usePortalContainer.ts - useAudioPlayer -> src/contexts/audioPlayerContextDef.ts - SubHeaderBar context/hooks -> src/components/SubHeaderBarContext.ts - EmotionDev hooks -> src/blobbi/dev/useEmotionDev.ts - BlobbiActions context def -> BlobbiActionsContextDef.ts - Remove export from internal-only functions (useEventComments, parseEmojiPack, useScrollCarets, formatEffectSummary, getSortedEffectEntries) - Fix react-hooks/exhaustive-deps warnings by adding missing dependencies to useEffect/useCallback/useMemo hooks across 14 files - Fix logical expression dependency warnings by wrapping conditional values (tasks, pubkeys, authorPubkeys) in useMemo - Move module-level constants (CORE_TAB_IDS, CORE_TAB_LABELS, DEFAULT_TAB_LABELS) out of ProfilePage component body - Reorder usePushNotifications hooks so syncPreferences is defined before enable to fix block-scoped variable error
118 lines
3.9 KiB
TypeScript
118 lines
3.9 KiB
TypeScript
import { useRef, useState, useEffect, useCallback } from 'react';
|
|
import { Play, Pause } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
import { formatTime } from '@/lib/formatTime';
|
|
|
|
interface MiniAudioPlayerProps {
|
|
src: string;
|
|
label?: string;
|
|
className?: string;
|
|
}
|
|
|
|
/** Compact inline audio player for profile fields. */
|
|
export function MiniAudioPlayer({ src, label, className }: MiniAudioPlayerProps) {
|
|
const audioRef = useRef<HTMLAudioElement>(null);
|
|
const progressRef = useRef<HTMLDivElement>(null);
|
|
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
const [currentTime, setCurrentTime] = useState(0);
|
|
const [duration, setDuration] = useState(0);
|
|
|
|
const progress = duration > 0 ? currentTime / duration : 0;
|
|
|
|
useEffect(() => {
|
|
const audio = audioRef.current;
|
|
if (!audio) return;
|
|
const onPlay = () => setIsPlaying(true);
|
|
const onPause = () => setIsPlaying(false);
|
|
const onEnded = () => setIsPlaying(false);
|
|
const onTime = () => setCurrentTime(audio.currentTime);
|
|
const onDur = () => {
|
|
if (audio.duration && isFinite(audio.duration)) {
|
|
setDuration(audio.duration);
|
|
}
|
|
};
|
|
audio.addEventListener('play', onPlay);
|
|
audio.addEventListener('pause', onPause);
|
|
audio.addEventListener('ended', onEnded);
|
|
audio.addEventListener('timeupdate', onTime);
|
|
audio.addEventListener('durationchange', onDur);
|
|
audio.addEventListener('loadedmetadata', onDur);
|
|
return () => {
|
|
audio.removeEventListener('play', onPlay);
|
|
audio.removeEventListener('pause', onPause);
|
|
audio.removeEventListener('ended', onEnded);
|
|
audio.removeEventListener('timeupdate', onTime);
|
|
audio.removeEventListener('durationchange', onDur);
|
|
audio.removeEventListener('loadedmetadata', onDur);
|
|
};
|
|
}, []);
|
|
|
|
const togglePlay = useCallback((e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
const audio = audioRef.current;
|
|
if (!audio) return;
|
|
if (audio.paused) {
|
|
audio.play();
|
|
} else {
|
|
audio.pause();
|
|
}
|
|
}, []);
|
|
|
|
const handleSeek = useCallback((e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
const audio = audioRef.current;
|
|
const bar = progressRef.current;
|
|
if (!audio || !bar || !duration) return;
|
|
const rect = bar.getBoundingClientRect();
|
|
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
|
audio.currentTime = ratio * duration;
|
|
}, [duration]);
|
|
|
|
return (
|
|
<div className={cn('flex items-center gap-2.5 rounded-lg border border-border bg-muted/30 px-3 py-2', className)}>
|
|
<audio ref={audioRef} preload="metadata" className="hidden">
|
|
<source src={src} />
|
|
</audio>
|
|
|
|
{/* Play/Pause */}
|
|
<button
|
|
onClick={togglePlay}
|
|
className={cn(
|
|
'shrink-0 size-7 rounded-full flex items-center justify-center transition-colors',
|
|
isPlaying
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'bg-primary/15 text-primary hover:bg-primary/25',
|
|
)}
|
|
aria-label={isPlaying ? 'Pause' : 'Play'}
|
|
>
|
|
{isPlaying
|
|
? <Pause className="size-3" fill="currentColor" />
|
|
: <Play className="size-3 ml-px" fill="currentColor" />}
|
|
</button>
|
|
|
|
{/* Track info + progress */}
|
|
<div className="flex-1 min-w-0 space-y-1">
|
|
{label && <span className="block text-xs font-medium truncate">{label}</span>}
|
|
|
|
{/* Progress bar */}
|
|
<div
|
|
ref={progressRef}
|
|
className="h-1 w-full rounded-full bg-primary/15 cursor-pointer"
|
|
onClick={handleSeek}
|
|
>
|
|
<div
|
|
className="h-full rounded-full bg-primary transition-[width] duration-150"
|
|
style={{ width: `${progress * 100}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Time */}
|
|
<span className="text-[10px] tabular-nums text-muted-foreground shrink-0">
|
|
{formatTime(currentTime)}/{duration > 0 ? formatTime(duration) : '--:--'}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|