Enable native dialogs across shells
This commit is contained in:
39
packages/ui/src/lib/native/electron/functions.ts
Normal file
39
packages/ui/src/lib/native/electron/functions.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { NativeDialogOptions } from "../native-functions"
|
||||
|
||||
interface ElectronDialogResult {
|
||||
canceled?: boolean
|
||||
paths?: string[]
|
||||
path?: string | null
|
||||
}
|
||||
|
||||
interface ElectronAPI {
|
||||
openDialog?: (options: NativeDialogOptions) => Promise<ElectronDialogResult>
|
||||
}
|
||||
|
||||
function coerceFirstPath(result?: ElectronDialogResult | null): string | null {
|
||||
if (!result || result.canceled) {
|
||||
return null
|
||||
}
|
||||
const paths = Array.isArray(result.paths) ? result.paths : result.path ? [result.path] : []
|
||||
if (paths.length === 0) {
|
||||
return null
|
||||
}
|
||||
return paths[0] ?? null
|
||||
}
|
||||
|
||||
export async function openElectronNativeDialog(options: NativeDialogOptions): Promise<string | null> {
|
||||
if (typeof window === "undefined") {
|
||||
return null
|
||||
}
|
||||
const api = (window as Window & { electronAPI?: ElectronAPI }).electronAPI
|
||||
if (!api?.openDialog) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const result = await api.openDialog(options)
|
||||
return coerceFirstPath(result)
|
||||
} catch (error) {
|
||||
console.error("[native] electron dialog failed", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
37
packages/ui/src/lib/native/native-functions.ts
Normal file
37
packages/ui/src/lib/native/native-functions.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { runtimeEnv } from "../runtime-env"
|
||||
import type { NativeDialogOptions } from "./types"
|
||||
import { openElectronNativeDialog } from "./electron/functions"
|
||||
import { openTauriNativeDialog } from "./tauri/functions"
|
||||
|
||||
export type { NativeDialogOptions, NativeDialogFilter, NativeDialogMode } from "./types"
|
||||
|
||||
function resolveNativeHandler(): ((options: NativeDialogOptions) => Promise<string | null>) | null {
|
||||
switch (runtimeEnv.host) {
|
||||
case "electron":
|
||||
return openElectronNativeDialog
|
||||
case "tauri":
|
||||
return openTauriNativeDialog
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function supportsNativeDialogs(): boolean {
|
||||
return resolveNativeHandler() !== null
|
||||
}
|
||||
|
||||
async function openNativeDialog(options: NativeDialogOptions): Promise<string | null> {
|
||||
const handler = resolveNativeHandler()
|
||||
if (!handler) {
|
||||
return null
|
||||
}
|
||||
return handler(options)
|
||||
}
|
||||
|
||||
export async function openNativeFolderDialog(options?: Omit<NativeDialogOptions, "mode">): Promise<string | null> {
|
||||
return openNativeDialog({ mode: "directory", ...(options ?? {}) })
|
||||
}
|
||||
|
||||
export async function openNativeFileDialog(options?: Omit<NativeDialogOptions, "mode">): Promise<string | null> {
|
||||
return openNativeDialog({ mode: "file", ...(options ?? {}) })
|
||||
}
|
||||
55
packages/ui/src/lib/native/tauri/functions.ts
Normal file
55
packages/ui/src/lib/native/tauri/functions.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { NativeDialogOptions } from "../native-functions"
|
||||
|
||||
interface TauriDialogModule {
|
||||
open?: (
|
||||
options: {
|
||||
title?: string
|
||||
defaultPath?: string
|
||||
filters?: { name?: string; extensions: string[] }[]
|
||||
directory?: boolean
|
||||
multiple?: boolean
|
||||
},
|
||||
) => Promise<string | string[] | null>
|
||||
}
|
||||
|
||||
interface TauriBridge {
|
||||
dialog?: TauriDialogModule
|
||||
}
|
||||
|
||||
export async function openTauriNativeDialog(options: NativeDialogOptions): Promise<string | null> {
|
||||
if (typeof window === "undefined") {
|
||||
return null
|
||||
}
|
||||
|
||||
const tauriBridge = (window as Window & { __TAURI__?: TauriBridge }).__TAURI__
|
||||
const dialogApi = tauriBridge?.dialog
|
||||
if (!dialogApi?.open) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await dialogApi.open({
|
||||
title: options.title,
|
||||
defaultPath: options.defaultPath,
|
||||
directory: options.mode === "directory",
|
||||
multiple: false,
|
||||
filters: options.filters?.map((filter) => ({
|
||||
name: filter.name,
|
||||
extensions: filter.extensions,
|
||||
})),
|
||||
})
|
||||
|
||||
if (!response) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
return response[0] ?? null
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error("[native] tauri dialog failed", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
13
packages/ui/src/lib/native/types.ts
Normal file
13
packages/ui/src/lib/native/types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export type NativeDialogMode = "directory" | "file"
|
||||
|
||||
export interface NativeDialogFilter {
|
||||
name?: string
|
||||
extensions: string[]
|
||||
}
|
||||
|
||||
export interface NativeDialogOptions {
|
||||
mode: NativeDialogMode
|
||||
title?: string
|
||||
defaultPath?: string
|
||||
filters?: NativeDialogFilter[]
|
||||
}
|
||||
Reference in New Issue
Block a user