diff --git a/js/package.json b/js/package.json index 81599cd..931e744 100644 --- a/js/package.json +++ b/js/package.json @@ -11,7 +11,7 @@ "make": "electron-forge make", "publish": "electron-forge publish", "lint": "eslint --ext .ts,.tsx .", - "compile": "npx tsc --noEmit 2>&1 | grep -E '^src/'" + "compile": "npx tsc --noEmit 2>&1 | grep '^src/'" }, "keywords": [], "author": { diff --git a/js/src/components/ui/avatar.tsx b/js/src/components/ui/avatar.tsx new file mode 100644 index 0000000..75c377d --- /dev/null +++ b/js/src/components/ui/avatar.tsx @@ -0,0 +1,107 @@ +import * as React from "react" +import { Avatar as AvatarPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" | "lg" +}) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { + return ( + svg]:hidden", + "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", + "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", + className + )} + {...props} + /> + ) +} + +function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AvatarGroupCount({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2", className)} + {...props} + /> + ) +} + +export { + Avatar, + AvatarImage, + AvatarFallback, + AvatarGroup, + AvatarGroupCount, + AvatarBadge, +} diff --git a/js/src/features/playback/media-particle-view.tsx b/js/src/features/playback/media-particle-view.tsx index 9cb4eaa..773ea94 100644 --- a/js/src/features/playback/media-particle-view.tsx +++ b/js/src/features/playback/media-particle-view.tsx @@ -18,6 +18,22 @@ function formatTime(ms: number): string { return `${minutes}:${seconds.toString().padStart(2, "0")}`; } +function DurationPill({ + currentTimeMs, + totalDurationMs, +}: { + currentTimeMs: number; + totalDurationMs: number; +}) { + return ( +
+ + {formatTime(currentTimeMs)} / {formatTime(totalDurationMs)} + +
+ ); +} + export function MediaParticleView({ particle, onEnded, @@ -35,7 +51,10 @@ export function MediaParticleView({ const [audioEl, setAudioEl] = useState(null); const [currentTimeMs, setCurrentTimeMs] = useState(0); - const audioSource = useAudioSource(audioEl); + const data = particle.data as MediaParticleData; + const isAudio = data.mime_type?.startsWith("audio/"); + + const audioSource = useAudioSource(isAudio ? audioEl : null); useEffect(() => { if (cachedUrl) { @@ -60,8 +79,19 @@ export function MediaParticleView({ }; }, [particle.id, cachedUrl, cacheDownloadUrl]); + // Start audio playback once the AudioContext source is ready useEffect(() => { - const el = videoRef.current ?? audioRef.current; + const el = audioRef.current; + if (!el || !audioSource) return; + + if (!paused) { + el.play().catch(() => {}); + } + }, [audioSource, paused]); + + // Handle video pause/resume + useEffect(() => { + const el = videoRef.current; if (!el) return; if (paused) { @@ -71,6 +101,16 @@ export function MediaParticleView({ } }, [paused]); + // Handle audio pause/resume (after initial play) + useEffect(() => { + const el = audioRef.current; + if (!el || !audioSource) return; + + if (paused) { + el.pause(); + } + }, [paused, audioSource]); + if (error) { return (
@@ -83,9 +123,6 @@ export function MediaParticleView({ return ; } - const data = particle.data as MediaParticleData; - const isAudio = data.mime_type?.startsWith("audio/"); - if (isAudio) { return (
@@ -94,8 +131,8 @@ export function MediaParticleView({ audioRef.current = el; setAudioEl(el); }} + crossOrigin="anonymous" src={url} - autoPlay onEnded={onEnded} onTimeUpdate={(e) => { setCurrentTimeMs(e.currentTarget.currentTime * 1000); @@ -106,23 +143,31 @@ export function MediaParticleView({ )} -
- - {formatTime(currentTimeMs)} / {formatTime(data.duration_ms)} - -
+
); } return ( -