diff --git a/packages/electron-app/electron/main/ipc.ts b/packages/electron-app/electron/main/ipc.ts index 60509dcb..3e88adf2 100644 --- a/packages/electron-app/electron/main/ipc.ts +++ b/packages/electron-app/electron/main/ipc.ts @@ -1,4 +1,5 @@ import { BrowserWindow, Notification, dialog, ipcMain, powerSaveBlocker, type OpenDialogOptions } from "electron" +import fs from "fs" import type { CliProcessManager, CliStatus } from "./process-manager" let wakeLockId: number | null = null @@ -65,6 +66,24 @@ export function setupCliIPC(mainWindow: BrowserWindow, cliManager: CliProcessMan return { canceled: result.canceled, paths: result.filePaths } }) + ipcMain.handle("filesystem:getDirectoryPaths", async (_event, paths: unknown): Promise => { + if (!Array.isArray(paths)) { + return [] + } + + const directories = paths.filter((value): value is string => { + if (typeof value !== "string" || value.trim().length === 0) { + return false + } + try { + return fs.statSync(value).isDirectory() + } catch { + return false + } + }) + return directories + }) + ipcMain.handle("power:setWakeLock", async (_event, enabled: boolean): Promise<{ enabled: boolean }> => { const next = Boolean(enabled) if (next) { diff --git a/packages/electron-app/electron/preload/index.cjs b/packages/electron-app/electron/preload/index.cjs index 0ab3736a..75bad994 100644 --- a/packages/electron-app/electron/preload/index.cjs +++ b/packages/electron-app/electron/preload/index.cjs @@ -1,4 +1,4 @@ -const { contextBridge, ipcRenderer } = require("electron") +const { contextBridge, ipcRenderer, webUtils } = require("electron") const electronAPI = { onCliStatus: (callback) => { @@ -12,6 +12,14 @@ const electronAPI = { getCliStatus: () => ipcRenderer.invoke("cli:getStatus"), restartCli: () => ipcRenderer.invoke("cli:restart"), openDialog: (options) => ipcRenderer.invoke("dialog:open", options), + getDirectoryPaths: (paths) => ipcRenderer.invoke("filesystem:getDirectoryPaths", paths), + getPathForFile: (file) => { + try { + return webUtils.getPathForFile(file) + } catch { + return null + } + }, setWakeLock: (enabled) => ipcRenderer.invoke("power:setWakeLock", Boolean(enabled)), showNotification: (payload) => ipcRenderer.invoke("notifications:show", payload), } diff --git a/packages/tauri-app/src-tauri/src/main.rs b/packages/tauri-app/src-tauri/src/main.rs index 06505675..ac402932 100644 --- a/packages/tauri-app/src-tauri/src/main.rs +++ b/packages/tauri-app/src-tauri/src/main.rs @@ -35,7 +35,6 @@ fn cli_restart(app: AppHandle, state: tauri::State) -> Result bool { cfg!(debug_assertions) || std::env::var("TAURI_DEV").is_ok() } @@ -46,7 +45,10 @@ fn should_allow_internal(url: &Url) -> bool { // On Windows/WebView2, Tauri serves the app assets from `tauri.localhost`. // This must be treated as an internal origin or the navigation guard will // redirect it to the system browser and the app will appear blank. - "http" | "https" => matches!(url.host_str(), Some("127.0.0.1" | "localhost" | "tauri.localhost")), + "http" | "https" => matches!( + url.host_str(), + Some("127.0.0.1" | "localhost" | "tauri.localhost") + ), _ => false, } } @@ -66,6 +68,39 @@ fn intercept_navigation(webview: &Webview, url: &Url) -> bool { false } +fn collect_directory_paths(paths: &[std::path::PathBuf]) -> Vec { + paths + .iter() + .filter_map(|path| match std::fs::metadata(path) { + Ok(metadata) if metadata.is_dir() => Some(path.to_string_lossy().to_string()), + _ => None, + }) + .collect() +} + +fn emit_window_event(app_handle: &AppHandle, window_label: &str, event_name: &str) { + if let Some(window) = app_handle.get_webview_window(window_label) { + let _ = window.emit(event_name, ()); + } +} + +fn emit_folder_drop_event( + app_handle: &AppHandle, + window_label: &str, + event_name: &str, + paths: &[std::path::PathBuf], +) { + let directories = collect_directory_paths(paths); + + if directories.is_empty() { + return; + } + + if let Some(window) = app_handle.get_webview_window(window_label) { + let _ = window.emit(event_name, json!({ "paths": directories })); + } +} + fn main() { let navigation_guard: TauriPlugin = PluginBuilder::new("external-link-guard") .on_navigation(|webview, url| intercept_navigation(webview, url)) @@ -187,6 +222,27 @@ fn main() { app.exit(0); }); } + tauri::RunEvent::WindowEvent { + label, + event: tauri::WindowEvent::DragDrop(tauri::DragDropEvent::Enter { paths, .. }), + .. + } => { + emit_folder_drop_event(&app_handle, &label, "desktop:folder-drag-enter", &paths); + } + tauri::RunEvent::WindowEvent { + label, + event: tauri::WindowEvent::DragDrop(tauri::DragDropEvent::Drop { paths, .. }), + .. + } => { + emit_folder_drop_event(&app_handle, &label, "desktop:folder-drop", &paths); + } + tauri::RunEvent::WindowEvent { + label, + event: tauri::WindowEvent::DragDrop(tauri::DragDropEvent::Leave), + .. + } => { + emit_window_event(&app_handle, &label, "desktop:folder-drag-leave"); + } tauri::RunEvent::WindowEvent { event: tauri::WindowEvent::CloseRequested { api, .. }, .. @@ -234,13 +290,16 @@ fn build_menu(app: &AppHandle) -> tauri::Result<()> { "new_instance", "New Instance", true, - Some("CmdOrCtrl+N") + Some("CmdOrCtrl+N"), )?; - + let file_menu = SubmenuBuilder::new(app, "File") .item(&new_instance_item) .separator() - .text(if is_mac { "close" } else { "quit" }, if is_mac { "Close" } else { "Quit" }) + .text( + if is_mac { "close" } else { "quit" }, + if is_mac { "Close" } else { "Quit" }, + ) .build()?; submenus.push(file_menu); @@ -263,7 +322,6 @@ fn build_menu(app: &AppHandle) -> tauri::Result<()> { .text("force_reload", "Force Reload") .text("toggle_devtools", "Toggle Developer Tools") .separator() - .separator() .text("toggle_fullscreen", "Toggle Full Screen") .build()?; @@ -277,9 +335,12 @@ fn build_menu(app: &AppHandle) -> tauri::Result<()> { submenus.push(window_menu); // Build the main menu with all submenus - let submenu_refs: Vec<&dyn tauri::menu::IsMenuItem<_>> = submenus.iter().map(|s| s as &dyn tauri::menu::IsMenuItem<_>).collect(); + let submenu_refs: Vec<&dyn tauri::menu::IsMenuItem<_>> = submenus + .iter() + .map(|s| s as &dyn tauri::menu::IsMenuItem<_>) + .collect(); let menu = MenuBuilder::new(app).items(&submenu_refs).build()?; - + app.set_menu(menu)?; Ok(()) } diff --git a/packages/ui/src/components/folder-selection-view.tsx b/packages/ui/src/components/folder-selection-view.tsx index 5406b055..66362a8b 100644 --- a/packages/ui/src/components/folder-selection-view.tsx +++ b/packages/ui/src/components/folder-selection-view.tsx @@ -7,11 +7,13 @@ import DirectoryBrowserDialog from "./directory-browser-dialog" import Kbd from "./kbd" import { ThemeModeToggle } from "./theme-mode-toggle" import { openNativeFolderDialog, supportsNativeDialogs } from "../lib/native/native-functions" +import { useFolderDrop } from "../lib/hooks/use-folder-drop" import VersionPill from "./version-pill" import { DiscordSymbolIcon, GitHubMarkIcon } from "./brand-icons" import { githubStars } from "../stores/github-stars" import { formatCompactCount } from "../lib/formatters" import { useI18n, type Locale } from "../lib/i18n" +import { showAlertDialog } from "../stores/alerts" const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href @@ -193,6 +195,31 @@ const FolderSelectionView: Component = (props) => { }) }) + function dropTargetBlocked() { + return isLoading() || isFolderBrowserOpen() || Boolean(props.advancedSettingsOpen) + } + + function showInvalidFolderDropAlert() { + showAlertDialog(t("folderSelection.drop.invalidMessage"), { + title: t("folderSelection.drop.invalidTitle"), + variant: "warning", + }) + } + + + const folderDrop = useFolderDrop({ + enabled: () => !dropTargetBlocked(), + onInvalidDrop: showInvalidFolderDropAlert, + onDrop: async (paths) => { + const firstPath = paths[0] + if (!firstPath) { + showInvalidFolderDropAlert() + return + } + handleFolderSelect(firstPath) + }, + }) + function formatRelativeTime(timestamp: number): string { const seconds = Math.floor((Date.now() - timestamp) / 1000) const minutes = Math.floor(seconds / 60) @@ -317,6 +344,10 @@ const FolderSelectionView: Component = (props) => {
= (props) => {
+ + + + onDrop: (paths: string[]) => void | Promise + onInvalidDrop?: () => void +} + +interface FolderDropBindings { + onDragEnter: (event: DragEvent) => void + onDragOver: (event: DragEvent) => void + onDragLeave: (event: DragEvent) => void + onDrop: (event: DragEvent) => void +} + +export function useFolderDrop(options: UseFolderDropOptions): { + isActive: Accessor + isSupported: boolean + bind: FolderDropBindings +} { + const [isActive, setIsActive] = createSignal(false) + const [dragDepth, setDragDepth] = createSignal(0) + const isSupported = supportsDesktopFolderDrop() + + function reset() { + setDragDepth(0) + setIsActive(false) + } + + async function handleResolvedPaths(paths: string[]) { + reset() + if (!options.enabled()) { + return + } + const directoryPaths = await normalizeDroppedDirectoryPaths(paths) + if (directoryPaths.length === 0) { + options.onInvalidDrop?.() + return + } + await options.onDrop(directoryPaths) + } + + createEffect(() => { + if (!options.enabled()) { + reset() + } + }) + + onMount(() => { + if (!isSupported) { + return + } + + let disposeNativeDrop = () => {} + let disposeNativeState = () => {} + + void listenForNativeFolderDrops((paths) => { + if (!options.enabled()) { + return + } + void handleResolvedPaths(paths) + }).then((dispose) => { + disposeNativeDrop = dispose + }) + + void listenForNativeFolderDropState((state) => { + if (!options.enabled()) { + reset() + return + } + if (state === "enter") { + setIsActive(true) + return + } + reset() + }).then((dispose) => { + disposeNativeState = dispose + }) + + onCleanup(() => { + disposeNativeDrop() + disposeNativeState() + }) + }) + + const bind: FolderDropBindings = { + onDragEnter(event) { + if (!isSupported || runtimeEnv.host === "tauri" || !options.enabled() || !containsFileDrop(event)) { + return + } + event.preventDefault() + setDragDepth((prev) => prev + 1) + setIsActive(true) + }, + onDragOver(event) { + if (!isSupported || runtimeEnv.host === "tauri" || !options.enabled() || !containsFileDrop(event)) { + return + } + event.preventDefault() + if (event.dataTransfer) { + event.dataTransfer.dropEffect = "copy" + } + setIsActive(true) + }, + onDragLeave(event) { + if (!isSupported || runtimeEnv.host === "tauri" || !containsFileDrop(event)) { + return + } + event.preventDefault() + const nextDepth = Math.max(0, dragDepth() - 1) + setDragDepth(nextDepth) + if (nextDepth === 0) { + setIsActive(false) + } + }, + onDrop(event) { + if (!isSupported) { + return + } + event.preventDefault() + event.stopPropagation() + + if (!options.enabled()) { + reset() + return + } + + if (runtimeEnv.host === "tauri") { + reset() + return + } + + const paths = extractDroppedDirectoryPaths(event) + if (paths.length === 0) { + reset() + options.onInvalidDrop?.() + return + } + + void handleResolvedPaths(paths) + }, + } + + return { + isActive, + isSupported, + bind, + } +} diff --git a/packages/ui/src/lib/i18n/messages/en/folderSelection.ts b/packages/ui/src/lib/i18n/messages/en/folderSelection.ts index 0f943d71..8a86aad5 100644 --- a/packages/ui/src/lib/i18n/messages/en/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/en/folderSelection.ts @@ -31,6 +31,11 @@ export const folderSelectionMessages = { "folderSelection.loading.title": "Starting instance...", "folderSelection.loading.subtitle": "Hang tight while we prepare your workspace.", + "folderSelection.drop.title": "Drop a folder to open it", + "folderSelection.drop.subtitle": "Start a new instance in the dropped folder.", + "folderSelection.drop.invalidTitle": "Couldn't open dropped item", + "folderSelection.drop.invalidMessage": "Drop a folder to start a new instance.", + "folderSelection.dialog.title": "Select Workspace", "folderSelection.dialog.description": "Select workspace to start coding.", } as const diff --git a/packages/ui/src/lib/i18n/messages/es/folderSelection.ts b/packages/ui/src/lib/i18n/messages/es/folderSelection.ts index a5bc4947..3879dcbf 100644 --- a/packages/ui/src/lib/i18n/messages/es/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/es/folderSelection.ts @@ -31,6 +31,11 @@ export const folderSelectionMessages = { "folderSelection.loading.title": "Iniciando instancia...", "folderSelection.loading.subtitle": "Espera un momento mientras preparamos tu workspace.", + "folderSelection.drop.title": "Suelta una carpeta para abrirla", + "folderSelection.drop.subtitle": "Inicia una nueva instancia en la carpeta soltada.", + "folderSelection.drop.invalidTitle": "No se pudo abrir el elemento soltado", + "folderSelection.drop.invalidMessage": "Suelta una carpeta para iniciar una nueva instancia.", + "folderSelection.dialog.title": "Seleccionar workspace", "folderSelection.dialog.description": "Selecciona un workspace para empezar a programar.", } as const diff --git a/packages/ui/src/lib/i18n/messages/fr/folderSelection.ts b/packages/ui/src/lib/i18n/messages/fr/folderSelection.ts index 92d5998a..0fc4a7ba 100644 --- a/packages/ui/src/lib/i18n/messages/fr/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/fr/folderSelection.ts @@ -31,6 +31,11 @@ export const folderSelectionMessages = { "folderSelection.loading.title": "Démarrage de l'instance...", "folderSelection.loading.subtitle": "Patientez pendant que nous préparons votre espace de travail.", + "folderSelection.drop.title": "Déposez un dossier pour l'ouvrir", + "folderSelection.drop.subtitle": "Démarrez une nouvelle instance dans le dossier déposé.", + "folderSelection.drop.invalidTitle": "Impossible d'ouvrir l'élément déposé", + "folderSelection.drop.invalidMessage": "Déposez un dossier pour démarrer une nouvelle instance.", + "folderSelection.dialog.title": "Sélectionner l'espace de travail", "folderSelection.dialog.description": "Sélectionnez un espace de travail pour commencer à coder.", } as const diff --git a/packages/ui/src/lib/i18n/messages/ja/folderSelection.ts b/packages/ui/src/lib/i18n/messages/ja/folderSelection.ts index 619d9e89..02291481 100644 --- a/packages/ui/src/lib/i18n/messages/ja/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/ja/folderSelection.ts @@ -31,6 +31,11 @@ export const folderSelectionMessages = { "folderSelection.loading.title": "インスタンスを起動中...", "folderSelection.loading.subtitle": "ワークスペースを準備しています。しばらくお待ちください。", + "folderSelection.drop.title": "フォルダをドロップして開く", + "folderSelection.drop.subtitle": "ドロップしたフォルダで新しいインスタンスを開始します。", + "folderSelection.drop.invalidTitle": "ドロップした項目を開けませんでした", + "folderSelection.drop.invalidMessage": "新しいインスタンスを開始するにはフォルダをドロップしてください。", + "folderSelection.dialog.title": "ワークスペースを選択", "folderSelection.dialog.description": "コーディングを開始するワークスペースを選択してください。", } as const diff --git a/packages/ui/src/lib/i18n/messages/ru/folderSelection.ts b/packages/ui/src/lib/i18n/messages/ru/folderSelection.ts index de868689..34d7d6c6 100644 --- a/packages/ui/src/lib/i18n/messages/ru/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/ru/folderSelection.ts @@ -31,6 +31,11 @@ export const folderSelectionMessages = { "folderSelection.loading.title": "Запуск экземпляра…", "folderSelection.loading.subtitle": "Подождите, пока мы подготовим рабочее пространство.", + "folderSelection.drop.title": "Перетащите папку, чтобы открыть ее", + "folderSelection.drop.subtitle": "Запустите новый экземпляр в перетащенной папке.", + "folderSelection.drop.invalidTitle": "Не удалось открыть перетащенный элемент", + "folderSelection.drop.invalidMessage": "Перетащите папку, чтобы запустить новый экземпляр.", + "folderSelection.dialog.title": "Выберите рабочее пространство", "folderSelection.dialog.description": "Выберите рабочее пространство, чтобы начать писать код.", } as const diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/folderSelection.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/folderSelection.ts index 034dcb0b..4315823a 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/folderSelection.ts @@ -31,6 +31,11 @@ export const folderSelectionMessages = { "folderSelection.loading.title": "正在启动实例...", "folderSelection.loading.subtitle": "正在准备你的工作区,请稍候。", + "folderSelection.drop.title": "拖放文件夹以打开", + "folderSelection.drop.subtitle": "在拖放的文件夹中启动一个新实例。", + "folderSelection.drop.invalidTitle": "无法打开拖放的项目", + "folderSelection.drop.invalidMessage": "请拖放一个文件夹来启动新实例。", + "folderSelection.dialog.title": "选择工作区", "folderSelection.dialog.description": "选择工作区以开始编码。", } as const diff --git a/packages/ui/src/lib/native/desktop-file-drop.ts b/packages/ui/src/lib/native/desktop-file-drop.ts new file mode 100644 index 00000000..6691fb63 --- /dev/null +++ b/packages/ui/src/lib/native/desktop-file-drop.ts @@ -0,0 +1,155 @@ +import { getLogger } from "../logger" +import { runtimeEnv } from "../runtime-env" + +const log = getLogger("actions") + +type NativeFolderDropState = "enter" | "leave" + +interface TauriFolderDropPayload { + paths?: unknown +} + +function normalizePathList(input: unknown): string[] { + if (!Array.isArray(input)) { + return [] + } + return input.filter((value): value is string => typeof value === "string" && value.trim().length > 0) +} + +function getFilePath(file: File): string | null { + if (typeof file.path === "string" && file.path.trim().length > 0) { + return file.path + } + if (runtimeEnv.host === "electron") { + const electronPath = (window as Window & { electronAPI?: ElectronAPI }).electronAPI?.getPathForFile?.(file) + if (typeof electronPath === "string" && electronPath.trim().length > 0) { + return electronPath + } + } + return null +} + +async function resolveElectronDirectoryPaths(paths: string[]): Promise { + const api = (window as Window & { electronAPI?: ElectronAPI }).electronAPI + if (!api?.getDirectoryPaths || paths.length === 0) { + return [] + } + try { + return await api.getDirectoryPaths(paths) + } catch (error) { + log.error("[native] failed to validate dropped directory paths", error) + return [] + } +} + +export function supportsDesktopFolderDrop(): boolean { + return runtimeEnv.platform === "desktop" && runtimeEnv.host !== "web" +} + +export function containsFileDrop(event: DragEvent): boolean { + const types = event.dataTransfer?.types + if (!types) { + return false + } + return Array.from(types).includes("Files") +} + +export function extractDroppedDirectoryPaths(event: DragEvent): string[] { + const dataTransfer = event.dataTransfer + if (!dataTransfer) { + return [] + } + + const directoryHints = new Set() + for (const item of Array.from(dataTransfer.items ?? [])) { + if (item.kind !== "file") { + continue + } + const entry = item.webkitGetAsEntry?.() + if (!entry?.isDirectory) { + continue + } + const file = item.getAsFile() + const filePath = file ? getFilePath(file) : null + if (filePath) { + directoryHints.add(filePath) + } + } + + const paths = new Set() + for (const file of Array.from(dataTransfer.files ?? [])) { + const filePath = getFilePath(file) + if (!filePath) { + continue + } + if (directoryHints.size > 0 && !directoryHints.has(filePath)) { + continue + } + paths.add(filePath) + } + + return Array.from(paths) +} + +export async function normalizeDroppedDirectoryPaths(paths: string[]): Promise { + const uniquePaths = Array.from(new Set(paths.filter((path) => typeof path === "string" && path.trim().length > 0))) + if (uniquePaths.length === 0) { + return [] + } + if (runtimeEnv.host === "electron") { + return resolveElectronDirectoryPaths(uniquePaths) + } + return uniquePaths +} + +export async function listenForNativeFolderDrops(onDrop: (paths: string[]) => void): Promise<() => void> { + if (runtimeEnv.host !== "tauri") { + return () => {} + } + + const eventApi = window.__TAURI__?.event + if (!eventApi?.listen) { + return () => {} + } + + try { + const unlisten = await eventApi.listen("desktop:folder-drop", (event) => { + const payload = (event.payload ?? {}) as TauriFolderDropPayload + const paths = normalizePathList(payload.paths) + if (paths.length > 0) { + onDrop(paths) + } + }) + return () => { + unlisten() + } + } catch (error) { + log.error("[native] failed to listen for folder-drop event", error) + return () => {} + } +} + +export async function listenForNativeFolderDropState(onState: (state: NativeFolderDropState) => void): Promise<() => void> { + if (runtimeEnv.host !== "tauri") { + return () => {} + } + + const eventApi = window.__TAURI__?.event + if (!eventApi?.listen) { + return () => {} + } + + try { + const [unlistenEnter, unlistenLeave] = await Promise.all([ + eventApi.listen("desktop:folder-drag-enter", () => onState("enter")), + eventApi.listen("desktop:folder-drag-leave", () => onState("leave")), + ]) + return () => { + unlistenEnter() + unlistenLeave() + } + } catch (error) { + log.error("[native] failed to listen for folder-drop state", error) + return () => {} + } +} diff --git a/packages/ui/src/styles/components/folder-drop.css b/packages/ui/src/styles/components/folder-drop.css new file mode 100644 index 00000000..b070e60e --- /dev/null +++ b/packages/ui/src/styles/components/folder-drop.css @@ -0,0 +1,39 @@ +.folder-drop-overlay { + position: absolute; + inset: 0; + z-index: 40; + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem; + background: color-mix(in srgb, var(--folder-overlay-bg) 88%, var(--accent-primary) 12%); + backdrop-filter: blur(3px); + pointer-events: none; +} + +.folder-drop-card { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-sm); + width: min(560px, 100%); + padding: 2rem; + border: 2px dashed var(--accent-primary); + border-radius: var(--radius-xl); + background-color: color-mix(in srgb, var(--surface-base) 92%, var(--accent-primary) 8%); + box-shadow: var(--folder-card-shadow); + text-align: center; +} + +.folder-drop-title { + font-size: var(--font-size-xl); + font-weight: var(--font-weight-semibold); + color: var(--text-primary); +} + +.folder-drop-subtext { + max-width: 32rem; + font-size: var(--font-size-base); + line-height: var(--line-height-relaxed); + color: var(--text-secondary); +} diff --git a/packages/ui/src/styles/controls.css b/packages/ui/src/styles/controls.css index 44f02851..253c82e3 100644 --- a/packages/ui/src/styles/controls.css +++ b/packages/ui/src/styles/controls.css @@ -1,5 +1,6 @@ @import "./components/buttons.css"; @import "./components/badges.css"; +@import "./components/folder-drop.css"; @import "./components/folder-loading.css"; @import "./components/dropdown.css"; @import "./components/selector.css"; diff --git a/packages/ui/src/types/global.d.ts b/packages/ui/src/types/global.d.ts index e0558004..e0e6c93b 100644 --- a/packages/ui/src/types/global.d.ts +++ b/packages/ui/src/types/global.d.ts @@ -27,11 +27,26 @@ declare global { getCliStatus?: () => Promise restartCli?: () => Promise openDialog?: (options: ElectronDialogOptions) => Promise + getDirectoryPaths?: (paths: string[]) => Promise + getPathForFile?: (file: File) => string | null setWakeLock?: (enabled: boolean) => Promise<{ enabled: boolean }> showNotification?: (payload: { title: string; body: string }) => Promise<{ ok: boolean; reason?: string }> } + interface File { + path?: string + } + + interface FileSystemEntry { + isDirectory: boolean + isFile: boolean + } + + interface DataTransferItem { + webkitGetAsEntry?: () => FileSystemEntry | null + } + interface TauriDialogModule { open?: (options: Record) => Promise save?: (options: Record) => Promise @@ -40,6 +55,9 @@ declare global { interface TauriBridge { invoke?: (cmd: string, args?: Record) => Promise dialog?: TauriDialogModule + event?: { + listen: (event: string, handler: (payload: { payload: unknown }) => void) => Promise<() => void> + } } interface Window {