| 1 | import type { BrowserWindow, Dialog, MessageBoxOptions, OpenDialogOptions, SaveDialogOptions } from "electron"; |
| 2 | import { join } from "node:path"; |
| 3 | import { bool, str, strList, type Params } from "./params.js"; |
| 4 | |
| 5 | export interface DialogFilter { |
| 6 | name: string; |
| 7 | extensions: string[]; |
| 8 | } |
| 9 | |
| 10 | // Wails filters carry `*.png;*.jpg` patterns; Electron wants bare extensions. |
| 11 | export function fileFiltersFromPatterns(value: unknown): DialogFilter[] { |
| 12 | if (!Array.isArray(value)) return []; |
| 13 | const filters: DialogFilter[] = []; |
| 14 | for (const entry of value) { |
| 15 | if (typeof entry !== "object" || entry === null) continue; |
| 16 | const { displayName, pattern } = entry as { displayName?: unknown; pattern?: unknown }; |
| 17 | if (typeof pattern !== "string") continue; |
| 18 | const extensions = pattern |
| 19 | .split(";") |
| 20 | .map((part) => part.trim()) |
| 21 | .filter((part) => part !== "") |
| 22 | .map((part) => (part === "*" || part === "*.*" ? "*" : part.replace(/^\*?\./, ""))) |
| 23 | .filter((part) => part !== ""); |
| 24 | if (extensions.length === 0) continue; |
| 25 | filters.push({ name: typeof displayName === "string" && displayName !== "" ? displayName : pattern, extensions }); |
| 26 | } |
| 27 | return filters; |
| 28 | } |
| 29 | |
| 30 | export type DialogApi = Pick<Dialog, "showOpenDialog" | "showSaveDialog" | "showMessageBox">; |
| 31 | |
| 32 | type MessageType = "info" | "warning" | "error" | "question"; |
| 33 | |
| 34 | function messageType(value: string): MessageType { |
| 35 | return value === "warning" || value === "error" || value === "question" ? value : "info"; |
| 36 | } |
| 37 | |
| 38 | export class DialogHost { |
| 39 | constructor(private readonly dialog: DialogApi, private readonly parent: () => BrowserWindow | undefined) {} |
| 40 | |
| 41 | private open(options: OpenDialogOptions) { |
| 42 | const parent = this.parent(); |
| 43 | return parent ? this.dialog.showOpenDialog(parent, options) : this.dialog.showOpenDialog(options); |
| 44 | } |
| 45 | |
| 46 | private save(options: SaveDialogOptions) { |
| 47 | const parent = this.parent(); |
| 48 | return parent ? this.dialog.showSaveDialog(parent, options) : this.dialog.showSaveDialog(options); |
| 49 | } |
| 50 | |
| 51 | private box(options: MessageBoxOptions) { |
| 52 | const parent = this.parent(); |
| 53 | return parent ? this.dialog.showMessageBox(parent, options) : this.dialog.showMessageBox(options); |
| 54 | } |
| 55 | |
| 56 | async openDirectory(params: Params): Promise<{ path: string }> { |
| 57 | const result = await this.open({ |
| 58 | title: str(params, "title") || undefined, |
| 59 | defaultPath: str(params, "defaultDirectory") || undefined, |
| 60 | properties: ["openDirectory", "createDirectory"], |
| 61 | }); |
| 62 | return { path: result.canceled ? "" : (result.filePaths[0] ?? "") }; |
| 63 | } |
| 64 | |
| 65 | async openFile(params: Params): Promise<{ paths: string[] }> { |
| 66 | const result = await this.open({ |
| 67 | title: str(params, "title") || undefined, |
| 68 | defaultPath: str(params, "defaultDirectory") || undefined, |
| 69 | filters: fileFiltersFromPatterns(params.filters), |
| 70 | properties: bool(params, "multiple") ? ["openFile", "multiSelections"] : ["openFile"], |
| 71 | }); |
| 72 | return { paths: result.canceled ? [] : result.filePaths }; |
| 73 | } |
| 74 | |
| 75 | async saveFile(params: Params): Promise<{ path: string }> { |
| 76 | const directory = str(params, "defaultDirectory"); |
| 77 | const filename = str(params, "defaultFilename"); |
| 78 | const defaultPath = directory && filename ? join(directory, filename) : directory || filename || undefined; |
| 79 | const result = await this.save({ |
| 80 | title: str(params, "title") || undefined, |
| 81 | defaultPath, |
| 82 | filters: fileFiltersFromPatterns(params.filters), |
| 83 | }); |
| 84 | return { path: result.canceled ? "" : (result.filePath ?? "") }; |
| 85 | } |
| 86 | |
| 87 | async message(params: Params): Promise<{ button: string }> { |
| 88 | const buttons = strList(params, "buttons"); |
| 89 | const labels = buttons.length > 0 ? buttons : ["OK"]; |
| 90 | const defaultIndex = labels.indexOf(str(params, "defaultButton")); |
| 91 | const cancelIndex = labels.indexOf(str(params, "cancelButton")); |
| 92 | const result = await this.box({ |
| 93 | type: messageType(str(params, "type")), |
| 94 | title: str(params, "title"), |
| 95 | message: str(params, "message"), |
| 96 | buttons: labels, |
| 97 | defaultId: defaultIndex >= 0 ? defaultIndex : 0, |
| 98 | cancelId: cancelIndex >= 0 ? cancelIndex : -1, |
| 99 | noLink: true, |
| 100 | }); |
| 101 | return { button: labels[result.response] ?? "" }; |
| 102 | } |
| 103 | } |
| 104 |