mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-14 06:16:08 +01:00
Add .oxfmtignore to skip generated bindings and wasm-pack output. Add npm format script, update DEVELOPMENT.md for Vite+ toolchain, and format all non-generated files with oxfmt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
149 lines
4.6 KiB
TypeScript
149 lines
4.6 KiB
TypeScript
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
|
import { open } from "@tauri-apps/plugin-dialog";
|
|
import classNames from "classnames";
|
|
import mime from "mime";
|
|
import type { ReactNode } from "react";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import type { ButtonProps } from "./core/Button";
|
|
import { Button } from "./core/Button";
|
|
import { IconButton } from "./core/IconButton";
|
|
import { IconTooltip } from "./core/IconTooltip";
|
|
import { Label } from "./core/Label";
|
|
import { HStack } from "./core/Stacks";
|
|
|
|
type Props = Omit<ButtonProps, "type"> & {
|
|
onChange: (value: { filePath: string | null; contentType: string | null }) => void;
|
|
filePath: string | null;
|
|
nameOverride?: string | null;
|
|
directory?: boolean;
|
|
inline?: boolean;
|
|
noun?: string;
|
|
help?: ReactNode;
|
|
label?: ReactNode;
|
|
};
|
|
|
|
// Special character to insert ltr text in rtl element
|
|
const rtlEscapeChar = <>‎</>;
|
|
|
|
export function SelectFile({
|
|
onChange,
|
|
filePath,
|
|
inline,
|
|
className,
|
|
directory,
|
|
noun,
|
|
nameOverride,
|
|
size = "sm",
|
|
label,
|
|
help,
|
|
...props
|
|
}: Props) {
|
|
const handleClick = async () => {
|
|
const filePath = await open({
|
|
title: directory ? "Select Folder" : "Select File",
|
|
multiple: false,
|
|
directory,
|
|
});
|
|
if (filePath == null) return;
|
|
const contentType = filePath ? mime.getType(filePath) : null;
|
|
onChange({ filePath, contentType });
|
|
};
|
|
|
|
const handleClear = async () => {
|
|
onChange({ filePath: null, contentType: null });
|
|
};
|
|
|
|
const itemLabel = noun ?? (directory ? "Folder" : "File");
|
|
const selectOrChange = (filePath ? "Change " : "Select ") + itemLabel;
|
|
const [isHovering, setIsHovering] = useState(false);
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
// Listen for dropped files on the element
|
|
// NOTE: This doesn't work for Windows since native drag-n-drop can't work at the same tmie
|
|
// as browser drag-n-drop.
|
|
useEffect(() => {
|
|
let unlisten: (() => void) | undefined;
|
|
const setup = async () => {
|
|
const webview = getCurrentWebviewWindow();
|
|
unlisten = await webview.onDragDropEvent((event) => {
|
|
if (event.payload.type === "over") {
|
|
const p = event.payload.position;
|
|
const r = ref.current?.getBoundingClientRect();
|
|
if (r == null) return;
|
|
const isOver = p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom;
|
|
console.log("IS OVER", isOver);
|
|
setIsHovering(isOver);
|
|
} else if (event.payload.type === "drop" && isHovering) {
|
|
console.log("User dropped", event.payload.paths);
|
|
const p = event.payload.paths[0];
|
|
if (p) onChange({ filePath: p, contentType: null });
|
|
setIsHovering(false);
|
|
} else {
|
|
console.log("File drop cancelled");
|
|
setIsHovering(false);
|
|
}
|
|
});
|
|
};
|
|
setup().catch(console.error);
|
|
return () => {
|
|
if (unlisten) unlisten();
|
|
};
|
|
}, [isHovering, onChange]);
|
|
|
|
const filePathWithNameOverride = nameOverride ? `${filePath} (${nameOverride})` : filePath;
|
|
|
|
return (
|
|
<div ref={ref} className="w-full">
|
|
{label && (
|
|
<Label htmlFor={null} help={help}>
|
|
{label}
|
|
</Label>
|
|
)}
|
|
<HStack className="relative justify-stretch overflow-hidden">
|
|
<Button
|
|
className={classNames(
|
|
className,
|
|
"rtl mr-1.5",
|
|
inline && "w-full",
|
|
filePath && inline && "font-mono text-xs",
|
|
isHovering && "!border-notice",
|
|
)}
|
|
color={isHovering ? "primary" : "secondary"}
|
|
onClick={handleClick}
|
|
size={size}
|
|
{...props}
|
|
>
|
|
{rtlEscapeChar}
|
|
{inline ? filePathWithNameOverride || selectOrChange : selectOrChange}
|
|
</Button>
|
|
|
|
{!inline && (
|
|
<>
|
|
{filePath && (
|
|
<IconButton
|
|
size={size === "auto" ? "md" : size}
|
|
variant="border"
|
|
icon="x"
|
|
title={`Unset ${itemLabel}`}
|
|
onClick={handleClear}
|
|
/>
|
|
)}
|
|
<div
|
|
className={classNames(
|
|
"truncate rtl pl-1.5 pr-3 text-text",
|
|
filePath && "font-mono",
|
|
size === "xs" && filePath && "text-xs",
|
|
size === "sm" && filePath && "text-sm",
|
|
)}
|
|
>
|
|
{rtlEscapeChar}
|
|
{filePath ?? `No ${itemLabel.toLowerCase()} selected`}
|
|
</div>
|
|
{filePath == null && help && !label && <IconTooltip content={help} />}
|
|
</>
|
|
)}
|
|
</HStack>
|
|
</div>
|
|
);
|
|
}
|