mirror of
https://github.com/nicotsx/zerobyte.git
synced 2026-07-30 17:46:14 -04:00
feat(desktop): add native shell capabilities (#1002)
* feat(desktop): add native shell capabilities * refactor: pr feedback
This commit is contained in:
2
app/client/types/desktop.d.ts
vendored
2
app/client/types/desktop.d.ts
vendored
@@ -1,5 +1,7 @@
|
||||
type ZerobyteDesktopApi = {
|
||||
chooseFolder: () => Promise<string | null>;
|
||||
openMainWindow: (path?: string) => Promise<void>;
|
||||
quit: () => void;
|
||||
setTheme: (theme: "light" | "dark") => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { app, BrowserWindow, dialog, ipcMain, nativeTheme } from "electron";
|
||||
import { app, BrowserWindow, dialog, ipcMain, nativeTheme, type OpenDialogOptions } from "electron";
|
||||
import { toMessage } from "@zerobyte/core/utils";
|
||||
import { startDesktopRuntime, type DesktopRuntime } from "./desktop-runtime";
|
||||
import { createDesktopSession } from "./desktop-session";
|
||||
import { createDesktopWindow } from "./desktop-window";
|
||||
import { saveSecurityScopedBookmark, startAccessingSavedBookmarks } from "./security-scoped-bookmarks";
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let runtime: DesktopRuntime | null = null;
|
||||
let stopAccessingBookmarks: (() => void) | null = null;
|
||||
let isQuitting = false;
|
||||
|
||||
const quitApp = () => {
|
||||
@@ -38,6 +40,43 @@ const focusMainWindow = () => {
|
||||
mainWindow.focus();
|
||||
};
|
||||
|
||||
const isTrustedDesktopSender = (senderUrl?: string) => {
|
||||
if (!runtime || !senderUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return new URL(senderUrl).origin === new URL(runtime.url).origin;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const chooseFolder = async () => {
|
||||
const dialogOptions: OpenDialogOptions = {
|
||||
properties: ["openDirectory", "createDirectory"],
|
||||
securityScopedBookmarks: true,
|
||||
};
|
||||
|
||||
const result = mainWindow
|
||||
? await dialog.showOpenDialog(mainWindow, dialogOptions)
|
||||
: await dialog.showOpenDialog(dialogOptions);
|
||||
|
||||
if (result.canceled || !result.filePaths[0]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedPath = result.filePaths[0];
|
||||
const bookmark = result.bookmarks?.[0];
|
||||
if (process.platform === "darwin" && (process as NodeJS.Process & { mas?: boolean }).mas && !bookmark) {
|
||||
throw new Error("Failed to create security-scoped bookmark");
|
||||
}
|
||||
|
||||
await saveSecurityScopedBookmark(selectedPath, bookmark);
|
||||
|
||||
return selectedPath;
|
||||
};
|
||||
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
app.quit();
|
||||
} else {
|
||||
@@ -46,12 +85,15 @@ if (!app.requestSingleInstanceLock()) {
|
||||
void app.whenReady().then(async () => {
|
||||
try {
|
||||
nativeTheme.themeSource = "dark";
|
||||
stopAccessingBookmarks = await startAccessingSavedBookmarks();
|
||||
runtime = await startDesktopRuntime((status) => {
|
||||
dialog.showErrorBox("Zerobyte stopped", `Server process exited with ${status}`);
|
||||
});
|
||||
await createDesktopSession(runtime.url, runtime.launchSecret);
|
||||
await createWindow();
|
||||
} catch (error) {
|
||||
stopAccessingBookmarks?.();
|
||||
stopAccessingBookmarks = null;
|
||||
dialog.showErrorBox("Zerobyte failed to start", toMessage(error));
|
||||
quitApp();
|
||||
}
|
||||
@@ -62,11 +104,39 @@ app.on("before-quit", () => {
|
||||
isQuitting = true;
|
||||
runtime?.stop();
|
||||
runtime = null;
|
||||
stopAccessingBookmarks?.();
|
||||
stopAccessingBookmarks = null;
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {});
|
||||
|
||||
ipcMain.on("desktop:set-theme", (_event, theme) => {
|
||||
ipcMain.handle("desktop:choose-folder", (event) => {
|
||||
if (!isTrustedDesktopSender(event.senderFrame?.url)) {
|
||||
throw new Error("Invalid desktop IPC sender");
|
||||
}
|
||||
|
||||
return chooseFolder();
|
||||
});
|
||||
ipcMain.handle("desktop:open-main-window", (event, appPath?: unknown) => {
|
||||
if (!isTrustedDesktopSender(event.senderFrame?.url)) {
|
||||
throw new Error("Invalid desktop IPC sender");
|
||||
}
|
||||
|
||||
if (
|
||||
appPath !== undefined &&
|
||||
(typeof appPath !== "string" || !appPath.startsWith("/") || appPath.startsWith("//"))
|
||||
) {
|
||||
throw new Error("Invalid app path");
|
||||
}
|
||||
|
||||
return createWindow(appPath);
|
||||
});
|
||||
ipcMain.on("desktop:quit", (event) => {
|
||||
if (!isTrustedDesktopSender(event.senderFrame?.url)) return;
|
||||
quitApp();
|
||||
});
|
||||
ipcMain.on("desktop:set-theme", (event, theme) => {
|
||||
if (!isTrustedDesktopSender(event.senderFrame?.url)) return;
|
||||
if (theme === "light" || theme === "dark") {
|
||||
nativeTheme.themeSource = theme;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { contextBridge, ipcRenderer } from "electron";
|
||||
|
||||
contextBridge.exposeInMainWorld("zerobyteDesktop", {
|
||||
chooseFolder: () => ipcRenderer.invoke("desktop:choose-folder"),
|
||||
openMainWindow: (path?: string) => ipcRenderer.invoke("desktop:open-main-window", path),
|
||||
quit: () => ipcRenderer.send("desktop:quit"),
|
||||
setTheme: (theme: "light" | "dark") => ipcRenderer.send("desktop:set-theme", theme),
|
||||
});
|
||||
|
||||
74
apps/desktop/electron/security-scoped-bookmarks.ts
Normal file
74
apps/desktop/electron/security-scoped-bookmarks.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { safeJsonParse } from "@zerobyte/core/utils";
|
||||
import { app } from "electron";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
type SecurityScopedBookmarkGrant = {
|
||||
path: string;
|
||||
bookmark: string;
|
||||
};
|
||||
|
||||
const activeAccessorsByPath = new Map<string, () => void>();
|
||||
|
||||
const getStorePath = () => path.join(app.getPath("userData"), "data", "security-scoped-bookmarks.json");
|
||||
|
||||
const readGrants = async (): Promise<SecurityScopedBookmarkGrant[]> => {
|
||||
try {
|
||||
const file = await fs.readFile(getStorePath(), "utf-8");
|
||||
return safeJsonParse<SecurityScopedBookmarkGrant[]>(file) ?? [];
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return [];
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const writeGrants = async (grants: SecurityScopedBookmarkGrant[]) => {
|
||||
const storePath = getStorePath();
|
||||
await fs.mkdir(path.dirname(storePath), { recursive: true });
|
||||
await fs.writeFile(storePath, JSON.stringify(grants, null, 2), "utf-8");
|
||||
};
|
||||
|
||||
const startAccessingBookmark = (bookmarkPath: string, bookmark: string) => {
|
||||
if (process.platform !== "darwin") {
|
||||
return;
|
||||
}
|
||||
|
||||
const stopAccessing = app.startAccessingSecurityScopedResource(bookmark) as () => void;
|
||||
activeAccessorsByPath.get(bookmarkPath)?.();
|
||||
activeAccessorsByPath.set(bookmarkPath, stopAccessing);
|
||||
};
|
||||
|
||||
export const saveSecurityScopedBookmark = async (selectedPath: string, bookmark?: string) => {
|
||||
if (!bookmark) return;
|
||||
|
||||
startAccessingBookmark(selectedPath, bookmark);
|
||||
|
||||
const grants = (await readGrants()).filter((grant) => grant.path !== selectedPath);
|
||||
|
||||
grants.push({ path: selectedPath, bookmark });
|
||||
|
||||
await writeGrants(grants);
|
||||
};
|
||||
|
||||
export const startAccessingSavedBookmarks = async () => {
|
||||
if (process.platform !== "darwin") {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
for (const grant of await readGrants()) {
|
||||
try {
|
||||
startAccessingBookmark(grant.path, grant.bookmark);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const stopAccessing of activeAccessorsByPath.values()) {
|
||||
stopAccessing();
|
||||
}
|
||||
|
||||
activeAccessorsByPath.clear();
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user