From 3414880705344bd085343a9c3f7e807ea707b6c8 Mon Sep 17 00:00:00 2001 From: Ryan Walters Date: Wed, 20 Aug 2025 02:19:12 -0500 Subject: [PATCH] refactor: reorganize frontend code --- src/App.tsx | 36 ++-------- src/bindings.ts | 9 +-- .../drop}/drop-overlay.tsx | 69 ++++++++++--------- src/{components => features/graph}/graph.tsx | 8 +-- src/hooks/useDragDropPaths.ts | 24 +++++++ src/types/graph.ts | 4 ++ 6 files changed, 78 insertions(+), 72 deletions(-) rename src/{components => features/drop}/drop-overlay.tsx (89%) rename src/{components => features/graph}/graph.tsx (92%) create mode 100644 src/hooks/useDragDropPaths.ts create mode 100644 src/types/graph.ts diff --git a/src/App.tsx b/src/App.tsx index a647886..3600b36 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,38 +1,12 @@ -type Frame = { - id: string; - data: { x: string | number; y: number }[]; -}; - -import { getCurrentWebview } from "@tauri-apps/api/webview"; -import { useEffect, useState } from "react"; -import Graph from "./components/graph.js"; -import DropOverlay from "./components/drop-overlay.js"; +import { useDragDropPaths } from "./hooks/useDragDropPaths.js"; +import Graph from "./features/graph/graph.js"; +import DropOverlay from "./features/drop/drop-overlay.js"; +import type { Frame } from "./types/graph.js"; function App() { const data: Frame[] = []; - const [paths, setPaths] = useState([]); - useEffect(() => { - const unlistenPromise = getCurrentWebview().onDragDropEvent( - async ({ payload }) => { - if (payload.type === "enter") { - setPaths(payload.paths); - console.log("User hovering", payload); - } else if (payload.type === "leave" || payload.type === "drop") { - setPaths([]); - console.log("User left", payload); - } - }, - ); - - // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted - return () => { - unlistenPromise.then((unlisten) => { - unlisten(); - console.log("Unlistened"); - }); - }; - }, []); + const paths = useDragDropPaths(); const graph = ; diff --git a/src/bindings.ts b/src/bindings.ts index ebf4942..596bb75 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -1,8 +1,9 @@ // Import generated TypeScript types from ts-rs -export type { StreamResult } from "./bindings/StreamResult"; -export type { StreamDetail } from "./bindings/StreamDetail"; -export type { StreamResultError } from "./bindings/StreamResultError"; -export type { MediaType } from "./bindings/MediaType"; +import type { StreamResult } from "./bindings/StreamResult"; +import type { StreamDetail } from "./bindings/StreamDetail"; +import type { StreamResultError } from "./bindings/StreamResultError"; +import type { MediaType } from "./bindings/MediaType"; +export type { StreamResult, StreamDetail, StreamResultError, MediaType }; // Tauri invoke wrapper import { invoke } from "@tauri-apps/api/core"; diff --git a/src/components/drop-overlay.tsx b/src/features/drop/drop-overlay.tsx similarity index 89% rename from src/components/drop-overlay.tsx rename to src/features/drop/drop-overlay.tsx index 807bbac..648a6eb 100644 --- a/src/components/drop-overlay.tsx +++ b/src/features/drop/drop-overlay.tsx @@ -1,5 +1,17 @@ import { ReactNode, useEffect, useRef, useState } from "react"; import { match, P } from "ts-pattern"; +import { + CheckCircle, + File as FileIcon, + FileText, + Film, + Image, + Loader2, + Music, + XCircle, +} from "lucide-react"; +import { commands } from "../../bindings"; +import type { MediaType, StreamDetail } from "../../bindings"; type DropOverlayProps = { paths: string[]; @@ -14,31 +26,19 @@ type State = name: string; key: string; media_type: MediaType; - duration?: number; + duration?: number | null; size: number; - streams: any[]; + streams: StreamDetail[]; }[]; } | { status: "error"; reason: string; filename?: string; error_type?: string }; -import { - CheckCircle, - File as FileIcon, - FileText, - Film, - Image, - Loader2, - Music, - XCircle, -} from "lucide-react"; -import { commands, MediaType } from "../bindings"; - type FileItemProps = { filename: string; media_type: MediaType; - duration?: number; + duration?: number | null; size: number; - streams: any[]; + streams: StreamDetail[]; error?: string; error_type?: string; }; @@ -57,7 +57,9 @@ const formatDuration = (seconds: number): string => { const secs = Math.floor(seconds % 60); if (hours > 0) { - return `${hours}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; + return `${hours}:${minutes.toString().padStart(2, "0")}:${secs + .toString() + .padStart(2, "0")}`; } return `${minutes}:${secs.toString().padStart(2, "0")}`; }; @@ -107,7 +109,10 @@ const getFileIcon = ( } }; -const getStreamInfo = (streams: any[], mediaType: MediaType): string => { +const getStreamInfo = ( + streams: StreamDetail[], + mediaType: MediaType, +): string => { // For non-media files, return file type description if (!["Audio", "Video", "Image"].includes(mediaType)) { switch (mediaType) { @@ -125,17 +130,17 @@ const getStreamInfo = (streams: any[], mediaType: MediaType): string => { } // For media files, analyze streams - const videoStreams = streams.filter((s) => "Video" in s); - const audioStreams = streams.filter((s) => "Audio" in s); - const subtitleStreams = streams.filter((s) => "Subtitle" in s); + const videoStreams = streams.filter((s: any) => "Video" in s); + const audioStreams = streams.filter((s: any) => "Audio" in s); + const subtitleStreams = streams.filter((s: any) => "Subtitle" in s); - const parts = []; + const parts = [] as string[]; if (videoStreams.length > 0) { - const video = videoStreams[0]; + const video: any = videoStreams[0] as any; if ("Video" in video) { - const width = video.Video.width; - const height = video.Video.height; - const codec = video.Video.codec; + const width = (video as any).Video.width; + const height = (video as any).Video.height; + const codec = (video as any).Video.codec; if (width && height) { parts.push(`${width}x${height} ${codec}`); } else { @@ -144,9 +149,9 @@ const getStreamInfo = (streams: any[], mediaType: MediaType): string => { } } if (audioStreams.length > 0) { - const audio = audioStreams[0]; + const audio: any = audioStreams[0] as any; if ("Audio" in audio) { - parts.push(`${audio.Audio.codec} audio`); + parts.push(`${(audio as any).Audio.codec} audio`); } } if (subtitleStreams.length > 0) { @@ -219,7 +224,9 @@ const FileItem = ({ } else { const streamInfo = getStreamInfo(streams, media_type); const durationStr = duration ? formatDuration(duration) : null; - const details = [streamInfo, durationStr, fileSize].filter(Boolean); + const details = [streamInfo, durationStr, fileSize].filter( + Boolean, + ) as string[]; subtitle = details.join(" • "); status = "success"; } @@ -253,7 +260,7 @@ const DropOverlay = ({ paths }: DropOverlayProps) => { key: item.path, media_type: item.media_type, duration: item.duration, - size: item.size, + size: Number(item.size), streams: item.streams, })), })) @@ -343,7 +350,7 @@ const DropOverlay = ({ paths }: DropOverlayProps) => { ); }) - .with({ status: "error" }, ({ reason, error_type }) => { + .with({ status: "error" }, ({ reason }) => { return (
diff --git a/src/components/graph.tsx b/src/features/graph/graph.tsx similarity index 92% rename from src/components/graph.tsx rename to src/features/graph/graph.tsx index 1180342..1fe4edd 100644 --- a/src/components/graph.tsx +++ b/src/features/graph/graph.tsx @@ -1,10 +1,6 @@ import { ResponsiveLine } from "@nivo/line"; -import { formatBytes } from "../lib/format.js"; - -type Frame = { - id: string; - data: { x: string | number; y: number }[]; -}; +import { formatBytes } from "../../lib/format.js"; +import type { Frame } from "../../types/graph.js"; type GraphProps = { data: Frame[]; diff --git a/src/hooks/useDragDropPaths.ts b/src/hooks/useDragDropPaths.ts new file mode 100644 index 0000000..272d4a6 --- /dev/null +++ b/src/hooks/useDragDropPaths.ts @@ -0,0 +1,24 @@ +import { useEffect, useState } from "react"; +import { getCurrentWebview } from "@tauri-apps/api/webview"; + +export function useDragDropPaths(): string[] { + const [paths, setPaths] = useState([]); + + useEffect(() => { + const unlistenPromise = getCurrentWebview().onDragDropEvent( + async ({ payload }) => { + if (payload.type === "enter") { + setPaths(payload.paths); + } else if (payload.type === "leave" || payload.type === "drop") { + setPaths([]); + } + }, + ); + + return () => { + unlistenPromise.then((unlisten) => unlisten()); + }; + }, []); + + return paths; +} diff --git a/src/types/graph.ts b/src/types/graph.ts new file mode 100644 index 0000000..bc1a40f --- /dev/null +++ b/src/types/graph.ts @@ -0,0 +1,4 @@ +export type Frame = { + id: string; + data: { x: string | number; y: number }[]; +};