Skip to content

Commit 9da19a9

Browse files
committed
feat: handle Electron lazy binary download + on-device file picker
Electron no longer downloads its binary on npm install (postinstall removed for supply-chain security; ELECTRON_SKIP_BINARY_DOWNLOAD is gone). It now downloads on first launch (npm run dev). Add a 'download:electron' script (install-electron bin) to pre-fetch deterministically; document in README. Resolve mobile protocol import: add @capawesome/capacitor-file-picker and wire beginLocalProtocolImport's Capacitor branch to pick a .netcanvas (readData), and implement extractProtocol's Capacitor branch (JSZip + Filesystem, unzipping into Directory.Data/protocols/<uid>). JSZip is dynamic-imported so it is not bundled into the Electron renderer. Plugin synced into ios/ and android/. Lint clean; 540 tests pass; web + electron builds green.
1 parent a3d3fe4 commit 9da19a9

15 files changed

Lines changed: 2592 additions & 20 deletions

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ This project requires Node.js 24 (LTS). A `.node-version` / `.nvmrc` file is pro
2222
2. Install the project dependencies by typing `npm install`.
2323
3. Refer to the development tasks section below to learn how to test and build the app.
2424

25+
> **Note:** As of recent versions, Electron no longer downloads its binary during `npm install` (the `postinstall` step was removed for supply-chain security). The binary is fetched automatically the first time Electron is launched (e.g. `npm run dev`). To pre-download it explicitly, run `npm run download:electron`. Use `ELECTRON_INSTALL_PLATFORM` / `ELECTRON_INSTALL_ARCH` to fetch a binary for a different target.
26+
2527
## Development Tasks
2628

2729
|`npm run <script>`|Description|

android/app/capacitor.build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dependencies {
1616
implementation project(':capacitor-keyboard')
1717
implementation project(':capacitor-network')
1818
implementation project(':capacitor-status-bar')
19+
implementation project(':capawesome-capacitor-file-picker')
1920

2021
}
2122

android/capacitor.settings.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ project(':capacitor-network').projectDir = new File('../node_modules/@capacitor/
2222

2323
include ':capacitor-status-bar'
2424
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
25+
26+
include ':capawesome-capacitor-file-picker'
27+
project(':capawesome-capacitor-file-picker').projectDir = new File('../node_modules/@capawesome/capacitor-file-picker/android')

ios/App/CapApp-SPM/Package.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ let package = Package(
1818
.package(name: "CapacitorFilesystem", path: "../../../node_modules/@capacitor/filesystem"),
1919
.package(name: "CapacitorKeyboard", path: "../../../node_modules/@capacitor/keyboard"),
2020
.package(name: "CapacitorNetwork", path: "../../../node_modules/@capacitor/network"),
21-
.package(name: "CapacitorStatusBar", path: "../../../node_modules/@capacitor/status-bar")
21+
.package(name: "CapacitorStatusBar", path: "../../../node_modules/@capacitor/status-bar"),
22+
.package(name: "CapawesomeCapacitorFilePicker", path: "../../../node_modules/@capawesome/capacitor-file-picker")
2223
],
2324
targets: [
2425
.target(
@@ -32,7 +33,8 @@ let package = Package(
3233
.product(name: "CapacitorFilesystem", package: "CapacitorFilesystem"),
3334
.product(name: "CapacitorKeyboard", package: "CapacitorKeyboard"),
3435
.product(name: "CapacitorNetwork", package: "CapacitorNetwork"),
35-
.product(name: "CapacitorStatusBar", package: "CapacitorStatusBar")
36+
.product(name: "CapacitorStatusBar", package: "CapacitorStatusBar"),
37+
.product(name: "CapawesomeCapacitorFilePicker", package: "CapawesomeCapacitorFilePicker")
3638
]
3739
)
3840
]
Lines changed: 160 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24803,7 +24803,7 @@ var Encoding;
2480324803
Encoding2["UTF16"] = "utf16";
2480424804
})(Encoding || (Encoding = {}));
2480524805
const Filesystem = registerPlugin("Filesystem", {
24806-
web: () => __vitePreload(() => import("./web-DUFlwq02.js"), true ? [] : void 0, import.meta.url).then((m2) => new m2.FilesystemWeb())
24806+
web: () => __vitePreload(() => import("./web-2fBXITDz.js"), true ? [] : void 0, import.meta.url).then((m2) => new m2.FilesystemWeb())
2480724807
});
2480824808
f$V();
2480924809
const trimPath = fpExports.trimChars("/ ");
@@ -76177,7 +76177,7 @@ actions.untouch;
7617776177
actions.updateSyncWarnings;
7617876178
actions.updateSyncErrors;
7617976179
const Device = registerPlugin("Device", {
76180-
web: () => __vitePreload(() => import("./web-Cbp09VvC.js"), true ? [] : void 0, import.meta.url).then((m2) => new m2.DeviceWeb())
76180+
web: () => __vitePreload(() => import("./web-CPdKRLiP.js"), true ? [] : void 0, import.meta.url).then((m2) => new m2.DeviceWeb())
7618176181
});
7618276182
let cachedDeviceInfo = null;
7618376183
const initDeviceInfo = async () => {
@@ -76837,6 +76837,115 @@ const store$1 = createStore(
7683776837
)
7683876838
);
7683976839
persistor = persistStore(store$1, { manualPersist: true });
76840+
class FilePickerWeb extends WebPlugin {
76841+
constructor() {
76842+
super(...arguments);
76843+
this.ERROR_PICK_FILE_CANCELED = "pickFiles canceled.";
76844+
}
76845+
async checkPermissions() {
76846+
throw this.unimplemented("Not implemented on web.");
76847+
}
76848+
async convertHeicToJpeg(_options) {
76849+
throw this.unimplemented("Not implemented on web.");
76850+
}
76851+
async copyFile(_options) {
76852+
throw this.unimplemented("Not implemented on web.");
76853+
}
76854+
async pickFiles(options) {
76855+
const pickedFiles = await this.openFilePicker(options);
76856+
if (!pickedFiles) {
76857+
throw new Error(this.ERROR_PICK_FILE_CANCELED);
76858+
}
76859+
const result = {
76860+
files: []
76861+
};
76862+
for (const pickedFile of pickedFiles) {
76863+
const file = {
76864+
blob: pickedFile,
76865+
modifiedAt: pickedFile.lastModified,
76866+
mimeType: this.getMimeTypeFromUrl(pickedFile),
76867+
name: this.getNameFromUrl(pickedFile),
76868+
path: void 0,
76869+
size: this.getSizeFromUrl(pickedFile)
76870+
};
76871+
if (options === null || options === void 0 ? void 0 : options.readData) {
76872+
file.data = await this.getDataFromFile(pickedFile);
76873+
}
76874+
result.files.push(file);
76875+
}
76876+
return result;
76877+
}
76878+
async pickDirectory() {
76879+
throw this.unimplemented("Not implemented on web.");
76880+
}
76881+
async pickImages(options) {
76882+
return this.pickFiles(Object.assign({ types: ["image/*"] }, options));
76883+
}
76884+
async pickMedia(options) {
76885+
return this.pickFiles(Object.assign({ types: ["image/*", "video/*"] }, options));
76886+
}
76887+
async pickVideos(options) {
76888+
return this.pickFiles(Object.assign({ types: ["video/*"] }, options));
76889+
}
76890+
async requestPermissions(_options) {
76891+
throw this.unimplemented("Not implemented on web.");
76892+
}
76893+
async openFilePicker(options) {
76894+
var _a2;
76895+
const accept2 = ((_a2 = options === null || options === void 0 ? void 0 : options.types) === null || _a2 === void 0 ? void 0 : _a2.join(",")) || "";
76896+
const limit = (options === null || options === void 0 ? void 0 : options.limit) === void 0 ? 0 : options.limit;
76897+
return new Promise((resolve) => {
76898+
let onChangeFired = false;
76899+
const input = document.createElement("input");
76900+
input.type = "file";
76901+
input.accept = accept2;
76902+
input.multiple = limit === 0;
76903+
input.addEventListener("change", () => {
76904+
onChangeFired = true;
76905+
const files = Array.from(input.files || []);
76906+
resolve(files);
76907+
}, { once: true });
76908+
window.addEventListener("focus", async () => {
76909+
await this.wait(1e3);
76910+
if (onChangeFired) {
76911+
return;
76912+
}
76913+
resolve(void 0);
76914+
}, { once: true });
76915+
input.click();
76916+
});
76917+
}
76918+
async getDataFromFile(file) {
76919+
return new Promise((resolve, reject) => {
76920+
const reader = new FileReader();
76921+
reader.readAsDataURL(file);
76922+
reader.onload = () => {
76923+
const result = typeof reader.result === "string" ? reader.result : "";
76924+
const splittedResult = result.split("base64,");
76925+
const base64 = splittedResult[1] || "";
76926+
resolve(base64);
76927+
};
76928+
reader.onerror = (error) => {
76929+
reject(error);
76930+
};
76931+
});
76932+
}
76933+
getNameFromUrl(file) {
76934+
return file.name;
76935+
}
76936+
getMimeTypeFromUrl(file) {
76937+
return file.type;
76938+
}
76939+
getSizeFromUrl(file) {
76940+
return file.size;
76941+
}
76942+
async wait(delayMs) {
76943+
return new Promise((resolve) => setTimeout(resolve, delayMs));
76944+
}
76945+
}
76946+
const FilePicker = registerPlugin("FilePicker", {
76947+
web: () => new FilePickerWeb()
76948+
});
7684076949
var classnames = { exports: {} };
7684176950
var hasRequiredClassnames;
7684276951
function requireClassnames() {
@@ -118396,6 +118505,40 @@ const extractProtocol = inEnvironment((environment) => {
118396118505
return window.NCAPI.protocol.extract(protocolFile, destination).then(() => protocolName);
118397118506
};
118398118507
}
118508+
if (environment === environments.CAPACITOR) {
118509+
return async (protocolData) => {
118510+
if (!protocolData) {
118511+
throw new Error("protocolFile is required");
118512+
}
118513+
const protocolName = uuid$1();
118514+
const baseDir = `protocols/${protocolName}`;
118515+
const { default: JSZip } = await __vitePreload(async () => {
118516+
const { default: JSZip2 } = await import("./jszip.min-CIKoWVI7.js").then((n2) => n2.j);
118517+
return { default: JSZip2 };
118518+
}, true ? [] : void 0, import.meta.url);
118519+
const zip = await JSZip.loadAsync(protocolData, { base64: true });
118520+
const entries = Object.values(zip.files);
118521+
await Filesystem.mkdir({ path: baseDir, directory: Directory.Data, recursive: true }).catch(() => {
118522+
});
118523+
for (let i2 = 0; i2 < entries.length; i2 += 1) {
118524+
const entry = entries[i2];
118525+
const target2 = `${baseDir}/${entry.name}`;
118526+
if (entry.dir) {
118527+
await Filesystem.mkdir({ path: target2, directory: Directory.Data, recursive: true }).catch(() => {
118528+
});
118529+
} else {
118530+
const data = await entry.async("base64");
118531+
await Filesystem.writeFile({
118532+
path: target2,
118533+
data,
118534+
directory: Directory.Data,
118535+
recursive: true
118536+
});
118537+
}
118538+
}
118539+
return protocolName;
118540+
};
118541+
}
118399118542
return () => Promise.reject(new Error("extractProtocol() not available on platform"));
118400118543
});
118401118544
var helpers;
@@ -178405,6 +178548,13 @@ const beginLocalProtocolImport = () => {
178405178548
return void 0;
178406178549
}
178407178550
if (isCapacitor()) {
178551+
FilePicker.pickFiles({ limit: 1, readData: true }).then((result) => {
178552+
const file = result.files && result.files[0];
178553+
if (file && file.data) {
178554+
importProtocolFromFile(file.data, file.name);
178555+
}
178556+
}).catch(() => {
178557+
});
178408178558
return void 0;
178409178559
}
178410178560
return Error("Environment not supported");
@@ -178414,7 +178564,7 @@ const importProtocolFromFile = (filePath, name) => {
178414178564
let protocolUid;
178415178565
let previousUid;
178416178566
const filename = filenameFromPath(filePath);
178417-
const protocolName = protocolNameFromFilename(filename);
178567+
const protocolName = protocolNameFromFilename(name || filename);
178418178568
const toastUUID = uuid$1();
178419178569
dispatch$1(actionCreators$9.addToast({
178420178570
id: toastUUID,
@@ -230647,7 +230797,7 @@ function requireCompareVersions() {
230647230797
var compareVersionsExports = requireCompareVersions();
230648230798
const compareVersions = /* @__PURE__ */ getDefaultExportFromCjs$1(compareVersionsExports);
230649230799
const App$2 = registerPlugin("App", {
230650-
web: () => __vitePreload(() => import("./web-DwVQYyxD.js"), true ? [] : void 0, import.meta.url).then((m2) => new m2.AppWeb())
230800+
web: () => __vitePreload(() => import("./web-CP4kXA_q.js"), true ? [] : void 0, import.meta.url).then((m2) => new m2.AppWeb())
230651230801
});
230652230802
const getVersion = () => {
230653230803
if (isElectron()) {
@@ -230659,7 +230809,7 @@ const getVersion = () => {
230659230809
return Promise.resolve("0.0.0");
230660230810
};
230661230811
const Browser = registerPlugin("Browser", {
230662-
web: () => __vitePreload(() => import("./web-DGsySVkC.js"), true ? [] : void 0, import.meta.url).then((m2) => new m2.BrowserWeb())
230812+
web: () => __vitePreload(() => import("./web-DLfIk6vV.js"), true ? [] : void 0, import.meta.url).then((m2) => new m2.BrowserWeb())
230663230813
});
230664230814
const openExternalLink = (href) => {
230665230815
if (isElectron()) {
@@ -262989,7 +263139,11 @@ if (document.readyState === "complete") {
262989263139
};
262990263140
}
262991263141
export {
263142+
Buffer as B,
262992263143
Encoding as E,
262993263144
WebPlugin as W,
262994-
buildRequestInit as b
263145+
buildRequestInit as b,
263146+
commonjsGlobal$1 as c,
263147+
getDefaultExportFromCjs$1 as g,
263148+
process$1 as p
262995263149
};

0 commit comments

Comments
 (0)