Open a real folder picker for Apri progetto KiCad.
The Mac Carica dialog was a file input (accept empty, no working webkitdirectory). Use showDirectoryPicker or a hidden input with webkitdirectory=true and no accept. Ignore empty/cancel picks.
This commit is contained in:
@@ -2,6 +2,12 @@
|
||||
|
||||
What's new in Periscope.
|
||||
|
||||
## 2.60.8 — 2026-09-21 — Folder picker, not Carica file dialog
|
||||
|
||||
Apri progetto opened the Mac file list (**Carica**, no row selected) because the control was a React `<input type="file" accept="">` without a working `webkitdirectory`. Empty Carica then looked like “no .kicad_pro”. It now uses `showDirectoryPicker` (Chrome) or a hidden input created with `webkitdirectory=true` and **no accept**. Cancel/empty pick is ignored.
|
||||
|
||||
- [Fixed] One folder gesture; `.kicad_pro` from name or webkitRelativePath.
|
||||
|
||||
## 2.60.7 — 2026-09-21 — Detect HubAudio.kicad_pro after folder pick
|
||||
|
||||
Apri progetto was rejecting the folder that actually contains `HubAudio.kicad_pro`. Safari/macOS folder pick often puts the real name in `webkitRelativePath` while `File.name` is the stem without `.kicad_pro`, and React was not setting `webkitdirectory` on the input. We match name **or** relative path, sniff JSON if the suffix is missing, and set the folder attribute on the DOM.
|
||||
|
||||
@@ -884,6 +884,7 @@ export function CreateProjectDialog({
|
||||
}, []);
|
||||
|
||||
const onKicadProjectFolder = useCallback((incoming: File[]) => {
|
||||
if (incoming.length === 0) return;
|
||||
setBomFromBundle(true);
|
||||
setPcbFromBundle(true);
|
||||
setPcbFile(null);
|
||||
@@ -2122,9 +2123,7 @@ export function CreateProjectDialog({
|
||||
<div className="space-y-1.5">
|
||||
<FileUploadZone
|
||||
label="Apri progetto KiCad"
|
||||
accept=""
|
||||
directory
|
||||
multiple
|
||||
files={netlistFiles}
|
||||
onFilesChange={onKicadProjectFolder}
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState, type ChangeEvent, type DragEvent } from "react";
|
||||
import { useCallback, useState, type ChangeEvent, type DragEvent, type MouseEvent } from "react";
|
||||
import { FileCheck, Upload } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type FileUploadZoneProps = {
|
||||
label: string;
|
||||
accept: string;
|
||||
accept?: string;
|
||||
multiple?: boolean;
|
||||
directory?: boolean;
|
||||
files: File[];
|
||||
@@ -94,6 +94,64 @@ function mergeSelection(current: File[], incoming: File[], multiple: boolean, di
|
||||
return [...current, ...incoming];
|
||||
}
|
||||
|
||||
async function filesFromDirectoryHandle(
|
||||
handle: FileSystemDirectoryHandle,
|
||||
prefix = "",
|
||||
): Promise<File[]> {
|
||||
const out: File[] = [];
|
||||
for await (const [name, child] of handle.entries()) {
|
||||
if (SKIP_DIR.test(name)) continue;
|
||||
if (child.kind === "file") {
|
||||
const file = await child.getFile();
|
||||
out.push(stampRelativePath(file, prefix + name));
|
||||
} else if (child.kind === "directory") {
|
||||
out.push(
|
||||
...(await filesFromDirectoryHandle(
|
||||
child as FileSystemDirectoryHandle,
|
||||
prefix + name + "/",
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function pickFolderViaHiddenInput(): Promise<File[]> {
|
||||
return new Promise((resolve) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = true;
|
||||
input.setAttribute("webkitdirectory", "true");
|
||||
input.setAttribute("directory", "true");
|
||||
input.style.display = "none";
|
||||
const finish = (files: File[]) => {
|
||||
input.remove();
|
||||
resolve(files);
|
||||
};
|
||||
input.addEventListener("change", () => finish(Array.from(input.files ?? [])));
|
||||
input.addEventListener("cancel", () => finish([]));
|
||||
document.body.appendChild(input);
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
|
||||
async function pickProjectFolder(): Promise<File[]> {
|
||||
const picker = (
|
||||
window as Window & {
|
||||
showDirectoryPicker?: () => Promise<FileSystemDirectoryHandle>;
|
||||
}
|
||||
).showDirectoryPicker;
|
||||
if (typeof picker === "function") {
|
||||
try {
|
||||
const handle = await picker.call(window);
|
||||
return await filesFromDirectoryHandle(handle, `${handle.name}/`);
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === "AbortError") return [];
|
||||
}
|
||||
}
|
||||
return pickFolderViaHiddenInput();
|
||||
}
|
||||
|
||||
export function FileUploadZone({
|
||||
label,
|
||||
accept,
|
||||
@@ -111,6 +169,7 @@ export function FileUploadZone({
|
||||
event.preventDefault();
|
||||
setHover(false);
|
||||
void filesFromDropEvent(event).then((dropped) => {
|
||||
if (dropped.length === 0) return;
|
||||
onFilesChange(mergeSelection(files, dropped, multiple, directory));
|
||||
});
|
||||
},
|
||||
@@ -120,25 +179,32 @@ export function FileUploadZone({
|
||||
const onPick = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const picked = Array.from(event.target.files ?? []);
|
||||
if (picked.length === 0) return;
|
||||
onFilesChange(mergeSelection(files, picked, multiple, directory));
|
||||
},
|
||||
[files, multiple, directory, onFilesChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<label
|
||||
className={cn(
|
||||
const onOpenFolder = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void pickProjectFolder().then((picked) => {
|
||||
if (picked.length === 0) return;
|
||||
onFilesChange(picked);
|
||||
});
|
||||
},
|
||||
[onFilesChange],
|
||||
);
|
||||
|
||||
const boxClass = cn(
|
||||
"flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-6 cursor-pointer transition-colors",
|
||||
hover ? "border-blue-500 bg-blue-500/5" : "border-border hover:border-foreground/20",
|
||||
filled && "border-emerald-500/40 bg-emerald-500/5",
|
||||
)}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
setHover(true);
|
||||
}}
|
||||
onDragLeave={() => setHover(false)}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
);
|
||||
|
||||
const body = (
|
||||
<>
|
||||
{filled ? (
|
||||
<FileCheck className="h-6 w-6 text-emerald-600 dark:text-emerald-400" />
|
||||
) : (
|
||||
@@ -167,7 +233,7 @@ export function FileUploadZone({
|
||||
})
|
||||
) : directory ? (
|
||||
<span>
|
||||
{files.find((f) => f.name.toLowerCase().includes("kicad_pro") || (f as File & { webkitRelativePath?: string }).webkitRelativePath?.toLowerCase().includes("kicad_pro"))?.name
|
||||
{files.find((f) => relativePath(f).toLowerCase().includes("kicad_pro"))?.name
|
||||
?? `${files.length} file`}
|
||||
</span>
|
||||
) : (
|
||||
@@ -175,23 +241,44 @@ export function FileUploadZone({
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
if (directory) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(boxClass, "w-full")}
|
||||
onClick={onOpenFolder}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
setHover(true);
|
||||
}}
|
||||
onDragLeave={() => setHover(false)}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
{body}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<label
|
||||
className={boxClass}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
setHover(true);
|
||||
}}
|
||||
onDragLeave={() => setHover(false)}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
{body}
|
||||
<input
|
||||
type="file"
|
||||
accept={directory ? undefined : accept}
|
||||
multiple={multiple || directory}
|
||||
accept={accept || undefined}
|
||||
multiple={multiple}
|
||||
className="hidden"
|
||||
onChange={onPick}
|
||||
ref={(el) => {
|
||||
if (!el) return;
|
||||
if (directory) {
|
||||
el.setAttribute("webkitdirectory", "");
|
||||
el.setAttribute("directory", "");
|
||||
el.multiple = true;
|
||||
} else {
|
||||
el.removeAttribute("webkitdirectory");
|
||||
el.removeAttribute("directory");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""Pick KiCad project files from a folder picker FileList (Chrome + Safari).
|
||||
/** Pick KiCad project files from a folder picker FileList (Chrome + Safari).
|
||||
|
||||
Safari/macOS often puts the real name in ``webkitRelativePath`` while
|
||||
``File.name`` is the stem without ``.kicad_pro``. Matching only ``file.name``
|
||||
misses ``HubAudio.kicad_pro`` after Apri progetto.
|
||||
"""
|
||||
Safari/macOS often puts the real name in webkitRelativePath while File.name
|
||||
is the stem without .kicad_pro. Matching only file.name misses HubAudio.kicad_pro.
|
||||
*/
|
||||
|
||||
export type NamedUpload = {
|
||||
name: string;
|
||||
@@ -62,8 +61,8 @@ function isKicadKeep(file: NamedUpload): boolean {
|
||||
base.endsWith(".kicad_sch") ||
|
||||
base.endsWith(".kicad_pcb") ||
|
||||
looksLikeKicadSch(file) ||
|
||||
(file.name.toLowerCase().includes(".kicad_pcb") ||
|
||||
uploadRel(file).toLowerCase().includes(".kicad_pcb"))
|
||||
file.name.toLowerCase().includes(".kicad_pcb") ||
|
||||
uploadRel(file).toLowerCase().includes(".kicad_pcb")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user