Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/base/dom/file-picker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
Copyright (c) 2025 Xiang Yang.
Published under the standard MIT License.
Full text available at: https://opensource.org/licenses/MIT
*/

import { LocalFileSystem } from "../../kicanvas/services/vfs";

export class FilePicker {
/**
* Open file picker and call callback with the selected files.
*/
static async pick(callback: (vfs: LocalFileSystem) => Promise<void>) {
const files = await FilePicker.open_picker();
if (files.length > 0) {
await callback(new LocalFileSystem(files));
}
}

/**
* Open the file picker
*/
private static open_picker(): Promise<File[]> {
return new Promise((resolve) => {
// because the Window:showOpenFilePicker() method is experimental
// so we use the traditional technique
const input = document.createElement("input");

input.type = "file";
input.style.display = "none";
input.multiple = true;
input.accept = ".kicad_pcb,.kicad_pro,.kicad_sch";

input.onchange = (event) => {
const files = (event.target as HTMLInputElement).files;
if (files && files.length > 0) {
resolve(Array.from(files));
} else {
resolve([]);
}
};

input.oncancel = () => {
resolve([]);
};

input.click();
});
}
}
20 changes: 20 additions & 0 deletions src/kicanvas/elements/kicanvas-shell.css
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,26 @@ section.overlay input::placeholder {
color: var(--fg);
}

section.overlay .link_button {
color: #81eeff;
background: transparent;
font-size: 1em;
font-weight: normal;
text-decoration: underline;
outline: none;
border: none;
box-sizing: border-box;
transition: color var(--transition-time-medium) ease;
}

section.overlay .link_button:focus {
border: var(--button-focus-outline);
}

section.overlay .link_button:hover {
color: var(--fg);
}

section.overlay p.note {
color: var(--input-placeholder);
font-size: 1em;
Expand Down
18 changes: 17 additions & 1 deletion src/kicanvas/elements/kicanvas-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { later } from "../../base/async";
import { DropTarget } from "../../base/dom/drag-drop";
import { FilePicker } from "../../base/dom/file-picker";
import { CSS, attribute, html, query } from "../../base/web-components";
import { KCUIElement, KCUIIconElement } from "../../kc-ui";
import { sprites_url } from "../icons/sprites";
Expand Down Expand Up @@ -76,6 +77,9 @@ class KiCanvasShellElement extends KCUIElement {
@query(`input[name="link"]`, true)
public link_input: HTMLInputElement;

@query(`button[name="open_local"]`, true)
public open_file_button: HTMLButtonElement;

override initialContentCallback() {
const url_params = new URLSearchParams(document.location.search);
const github_paths = url_params.getAll("github");
Expand Down Expand Up @@ -111,6 +115,12 @@ class KiCanvasShellElement extends KCUIElement {
location.searchParams.set("github", link);
window.history.pushState(null, "", location);
});

this.open_file_button.addEventListener("click", async (e) => {
FilePicker.pick(async (vfs) => {
await this.setup_project(vfs);
});
});
}

private async setup_project(vfs: VirtualFileSystem) {
Expand Down Expand Up @@ -166,7 +176,13 @@ class KiCanvasShellElement extends KCUIElement {
type="text"
placeholder="Paste a GitHub link..."
autofocus />
<p>or drag & drop your KiCAD files</p>
<p>
or drag & drop your KiCAD files, or<button
name="open_local"
class="link_button">
open from local
</button>
</p>
<p class="note">
KiCanvas is
<a
Expand Down
32 changes: 32 additions & 0 deletions src/kicanvas/services/vfs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,35 @@ export class DragAndDropFileSystem extends VirtualFileSystem {
initiate_download(await this.get(name));
}
}

/**
* Virtual file system for local files
*/
export class LocalFileSystem extends VirtualFileSystem {
constructor(private files: File[]) {
super();
}

override *list() {
for (const entry of this.files) {
yield entry.name;
}
}

override async has(name: string): Promise<boolean> {
return this.files.find((f) => f.name == name) !== undefined;
}

override async get(name: string): Promise<File> {
const file = this.files.find((f) => f.name == name);
if (file) {
return file;
} else {
throw new Error(`File ${name} not found`);
}
}

override async download(name: string) {
initiate_download(await this.get(name));
}
}