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.
|
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
|
## 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.
|
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[]) => {
|
const onKicadProjectFolder = useCallback((incoming: File[]) => {
|
||||||
|
if (incoming.length === 0) return;
|
||||||
setBomFromBundle(true);
|
setBomFromBundle(true);
|
||||||
setPcbFromBundle(true);
|
setPcbFromBundle(true);
|
||||||
setPcbFile(null);
|
setPcbFile(null);
|
||||||
@@ -2122,9 +2123,7 @@ export function CreateProjectDialog({
|
|||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<FileUploadZone
|
<FileUploadZone
|
||||||
label="Apri progetto KiCad"
|
label="Apri progetto KiCad"
|
||||||
accept=""
|
|
||||||
directory
|
directory
|
||||||
multiple
|
|
||||||
files={netlistFiles}
|
files={netlistFiles}
|
||||||
onFilesChange={onKicadProjectFolder}
|
onFilesChange={onKicadProjectFolder}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"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 { FileCheck, Upload } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export type FileUploadZoneProps = {
|
export type FileUploadZoneProps = {
|
||||||
label: string;
|
label: string;
|
||||||
accept: string;
|
accept?: string;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
directory?: boolean;
|
directory?: boolean;
|
||||||
files: File[];
|
files: File[];
|
||||||
@@ -94,6 +94,64 @@ function mergeSelection(current: File[], incoming: File[], multiple: boolean, di
|
|||||||
return [...current, ...incoming];
|
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({
|
export function FileUploadZone({
|
||||||
label,
|
label,
|
||||||
accept,
|
accept,
|
||||||
@@ -111,6 +169,7 @@ export function FileUploadZone({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setHover(false);
|
setHover(false);
|
||||||
void filesFromDropEvent(event).then((dropped) => {
|
void filesFromDropEvent(event).then((dropped) => {
|
||||||
|
if (dropped.length === 0) return;
|
||||||
onFilesChange(mergeSelection(files, dropped, multiple, directory));
|
onFilesChange(mergeSelection(files, dropped, multiple, directory));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -120,25 +179,32 @@ export function FileUploadZone({
|
|||||||
const onPick = useCallback(
|
const onPick = useCallback(
|
||||||
(event: ChangeEvent<HTMLInputElement>) => {
|
(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
const picked = Array.from(event.target.files ?? []);
|
const picked = Array.from(event.target.files ?? []);
|
||||||
|
if (picked.length === 0) return;
|
||||||
onFilesChange(mergeSelection(files, picked, multiple, directory));
|
onFilesChange(mergeSelection(files, picked, multiple, directory));
|
||||||
},
|
},
|
||||||
[files, multiple, directory, onFilesChange],
|
[files, multiple, directory, onFilesChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const onOpenFolder = useCallback(
|
||||||
<label
|
(event: MouseEvent) => {
|
||||||
className={cn(
|
event.preventDefault();
|
||||||
"flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-6 cursor-pointer transition-colors",
|
event.stopPropagation();
|
||||||
hover ? "border-blue-500 bg-blue-500/5" : "border-border hover:border-foreground/20",
|
void pickProjectFolder().then((picked) => {
|
||||||
filled && "border-emerald-500/40 bg-emerald-500/5",
|
if (picked.length === 0) return;
|
||||||
)}
|
onFilesChange(picked);
|
||||||
onDragOver={(event) => {
|
});
|
||||||
event.preventDefault();
|
},
|
||||||
setHover(true);
|
[onFilesChange],
|
||||||
}}
|
);
|
||||||
onDragLeave={() => setHover(false)}
|
|
||||||
onDrop={onDrop}
|
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",
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = (
|
||||||
|
<>
|
||||||
{filled ? (
|
{filled ? (
|
||||||
<FileCheck className="h-6 w-6 text-emerald-600 dark:text-emerald-400" />
|
<FileCheck className="h-6 w-6 text-emerald-600 dark:text-emerald-400" />
|
||||||
) : (
|
) : (
|
||||||
@@ -167,7 +233,7 @@ export function FileUploadZone({
|
|||||||
})
|
})
|
||||||
) : directory ? (
|
) : directory ? (
|
||||||
<span>
|
<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`}
|
?? `${files.length} file`}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -175,23 +241,44 @@ export function FileUploadZone({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept={directory ? undefined : accept}
|
accept={accept || undefined}
|
||||||
multiple={multiple || directory}
|
multiple={multiple}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={onPick}
|
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>
|
</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
|
Safari/macOS often puts the real name in webkitRelativePath while File.name
|
||||||
``File.name`` is the stem without ``.kicad_pro``. Matching only ``file.name``
|
is the stem without .kicad_pro. Matching only file.name misses HubAudio.kicad_pro.
|
||||||
misses ``HubAudio.kicad_pro`` after Apri progetto.
|
*/
|
||||||
"""
|
|
||||||
|
|
||||||
export type NamedUpload = {
|
export type NamedUpload = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -62,8 +61,8 @@ function isKicadKeep(file: NamedUpload): boolean {
|
|||||||
base.endsWith(".kicad_sch") ||
|
base.endsWith(".kicad_sch") ||
|
||||||
base.endsWith(".kicad_pcb") ||
|
base.endsWith(".kicad_pcb") ||
|
||||||
looksLikeKicadSch(file) ||
|
looksLikeKicadSch(file) ||
|
||||||
(file.name.toLowerCase().includes(".kicad_pcb") ||
|
file.name.toLowerCase().includes(".kicad_pcb") ||
|
||||||
uploadRel(file).toLowerCase().includes(".kicad_pcb"))
|
uploadRel(file).toLowerCase().includes(".kicad_pcb")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user