feat(FN-1072): add Electron desktop renderer integration
- Add Electron main-process IPC handlers and preload bridges for API proxying, window controls, update install, and platform lookup - Introduce a desktop renderer entrypoint with DesktopWrapper and a custom frameless TitleBar component - Add Electron-aware renderer utilities including API transport selection and hooks for runtime detection, auto-update events, and deep links - Update dashboard header behavior for Electron mode and expand desktop tests across preload, transport, hooks, and title bar flows - Document the desktop renderer architecture and update desktop package config/dependencies
This commit is contained in:
3
packages/desktop/src/renderer/hooks/index.ts
Normal file
3
packages/desktop/src/renderer/hooks/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./useElectron";
|
||||
export * from "./useAutoUpdate";
|
||||
export * from "./useDeepLink";
|
||||
45
packages/desktop/src/renderer/hooks/useAutoUpdate.ts
Normal file
45
packages/desktop/src/renderer/hooks/useAutoUpdate.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useElectron } from "./useElectron";
|
||||
|
||||
export interface UseAutoUpdateResult {
|
||||
updateAvailable: boolean;
|
||||
updateInfo: Record<string, unknown> | null;
|
||||
downloadAndInstall: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useAutoUpdate(): UseAutoUpdateResult {
|
||||
const { isElectron, electronAPI } = useElectron();
|
||||
const [updateAvailable, setUpdateAvailable] = useState(false);
|
||||
const [updateInfo, setUpdateInfo] = useState<Record<string, unknown> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isElectron || !electronAPI?.onUpdateAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unsubscribe = electronAPI.onUpdateAvailable((info: Record<string, unknown>) => {
|
||||
setUpdateAvailable(true);
|
||||
setUpdateInfo(info);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (typeof unsubscribe === "function") {
|
||||
unsubscribe();
|
||||
}
|
||||
};
|
||||
}, [isElectron, electronAPI]);
|
||||
|
||||
const downloadAndInstall = useCallback(async () => {
|
||||
if (!isElectron || !electronAPI?.installUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
await electronAPI.installUpdate();
|
||||
}, [isElectron, electronAPI]);
|
||||
|
||||
return {
|
||||
updateAvailable,
|
||||
updateInfo,
|
||||
downloadAndInstall,
|
||||
};
|
||||
}
|
||||
57
packages/desktop/src/renderer/hooks/useDeepLink.ts
Normal file
57
packages/desktop/src/renderer/hooks/useDeepLink.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useElectron } from "./useElectron";
|
||||
|
||||
export function parseDeepLink(rawLink: string): string | null {
|
||||
try {
|
||||
const parsed = new URL(rawLink);
|
||||
if (parsed.protocol !== "fusion:") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const segments = parsed.pathname.split("/").filter(Boolean);
|
||||
const resource = parsed.hostname || segments[0];
|
||||
const identifier = parsed.hostname ? segments[0] : segments[1];
|
||||
|
||||
if (!identifier) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (resource === "task" || resource === "project") {
|
||||
return `fusion://${resource}/${decodeURIComponent(identifier)}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface UseDeepLinkResult {
|
||||
lastDeepLink: string | null;
|
||||
}
|
||||
|
||||
export function useDeepLink(): UseDeepLinkResult {
|
||||
const { isElectron, electronAPI } = useElectron();
|
||||
const [lastDeepLink, setLastDeepLink] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isElectron || !electronAPI?.onDeepLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unsubscribe = electronAPI.onDeepLink((rawLink: string) => {
|
||||
const parsed = parseDeepLink(rawLink);
|
||||
if (parsed) {
|
||||
setLastDeepLink(parsed);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (typeof unsubscribe === "function") {
|
||||
unsubscribe();
|
||||
}
|
||||
};
|
||||
}, [isElectron, electronAPI]);
|
||||
|
||||
return { lastDeepLink };
|
||||
}
|
||||
21
packages/desktop/src/renderer/hooks/useElectron.ts
Normal file
21
packages/desktop/src/renderer/hooks/useElectron.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useMemo } from "react";
|
||||
import type { ElectronAPI } from "../types";
|
||||
|
||||
export interface UseElectronResult {
|
||||
isElectron: boolean;
|
||||
electronAPI: ElectronAPI | null;
|
||||
}
|
||||
|
||||
export function useElectron(): UseElectronResult {
|
||||
return useMemo(() => {
|
||||
if (typeof window === "undefined") {
|
||||
return { isElectron: false, electronAPI: null };
|
||||
}
|
||||
|
||||
const electronAPI = window.electronAPI ?? null;
|
||||
return {
|
||||
isElectron: Boolean(electronAPI),
|
||||
electronAPI,
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
Reference in New Issue
Block a user