From 0275df601d3a18f320be9a95ab79ee0c3420d7b7 Mon Sep 17 00:00:00 2001 From: logikonline Date: Fri, 23 Jan 2026 10:17:35 -0500 Subject: [PATCH] feat(secrets): add native file dialog support and documentation Adds support for native file dialogs through the ui:dialogs permission. Includes comprehensive documentation and demo methods showing how to use saveFile, showSaveDialog, and showOpenDialog APIs. The dialogs API allows addons to: - Show native save dialogs and write files directly - Show open dialogs to select files/directories - Configure file filters, default paths, and dialog titles Also adds demo methods (saveFileDemo, openFileDemo) to the starter addon that demonstrate proper usage patterns including permission checks and error handling. --- README.md | 51 ++++++++++++++++++++++- addon.json | 3 +- main/index.js | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b760256..4d17ebb 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,8 @@ The manifest defines everything about your addon. "network:localhost", // Make local network requests "network:external", // Make external network requests "settings:store", // Store addon settings - "notifications:show" // Show system notifications + "notifications:show", // Show system notifications + "ui:dialogs" // Access native file dialogs ] ``` @@ -187,6 +188,7 @@ context.settings // Settings storage context.events // Event subscription context.ipc // IPC communication context.hostPort // Port of native host (if configured) +context.dialogs // Native file dialogs (requires ui:dialogs permission) ``` ### Invoke Method @@ -263,6 +265,53 @@ context.settings.onDidChange((key, value) => { }); ``` +### Native File Dialogs + +Addons with the `ui:dialogs` permission can access native file dialogs via `context.dialogs`: + +```javascript +// Check if dialogs are available +if (!this.context?.dialogs) { + return { error: 'Dialogs not available' }; +} + +// Save file with native dialog +const filePath = await this.context.dialogs.saveFile(content, { + defaultPath: 'export.txt', + filters: [ + { name: 'Text Files', extensions: ['txt'] }, + { name: 'All Files', extensions: ['*'] } + ], + title: 'Save Export' +}); +// Returns: string (file path) or undefined if canceled + +// Open file with native dialog +const result = await this.context.dialogs.showOpenDialog({ + filters: [ + { name: 'Text Files', extensions: ['txt', 'md'] }, + { name: 'All Files', extensions: ['*'] } + ], + title: 'Select File', + multiSelections: false, // Allow multiple file selection + openDirectory: false // Select directories instead of files +}); +// Returns: { canceled: boolean, filePaths: string[] } + +// Show save dialog without writing file +const saveResult = await this.context.dialogs.showSaveDialog({ + defaultPath: 'document.txt', + filters: [{ name: 'Text', extensions: ['txt'] }], + title: 'Choose Save Location' +}); +// Returns: { canceled: boolean, filePath?: string } +``` + +**Methods:** +- `saveFile(content, options)` - Show save dialog and write content to selected file +- `showSaveDialog(options)` - Show save dialog, returns path only (you handle writing) +- `showOpenDialog(options)` - Show open dialog, returns selected path(s) + ### License Status (Optional) For commercial addons: diff --git a/addon.json b/addon.json index 167630f..01ce9cc 100644 --- a/addon.json +++ b/addon.json @@ -53,7 +53,8 @@ "network:localhost", "network:external", "settings:store", - "notifications:show" + "notifications:show", + "ui:dialogs" ], "_comment_capabilities": "What your addon can do - GitCaddy uses these to find addons", diff --git a/main/index.js b/main/index.js index 7f7471d..251153f 100644 --- a/main/index.js +++ b/main/index.js @@ -201,6 +201,13 @@ class MyFirstAddon { case 'performAction': return this.performAction(args[0]); + // ---- Dialog Methods ---- + case 'saveFileDemo': + return this.saveFileDemo(args[0], args[1]); + + case 'openFileDemo': + return this.openFileDemo(); + // ---- Host Methods ---- // These call the .NET host process case 'callHelloWorld': @@ -429,7 +436,9 @@ class MyFirstAddon { 'Settings storage', 'IPC communication', 'Event handling', + 'Native file dialogs', ], + hasDialogs: !!this.context?.dialogs, }; } @@ -483,6 +492,100 @@ class MyFirstAddon { }; } + // ============================================================ + // DIALOG METHODS + // These methods demonstrate the native file dialogs API + // Requires "ui:dialogs" permission in addon.json + // ============================================================ + + /** + * Save content to a file using a native Save dialog. + * + * Demonstrates context.dialogs.saveFile() which shows a native + * file picker and saves the content to the selected location. + * + * @param {string} content - Content to save + * @param {string} [filename] - Suggested filename + * @returns {Promise<{success: boolean, filePath?: string, error?: string}>} + */ + async saveFileDemo(content, filename) { + // Check if dialogs API is available + if (!this.context?.dialogs) { + this.context?.log.warn('Dialogs API not available - missing ui:dialogs permission?'); + return { success: false, error: 'Dialogs not available' }; + } + + try { + // Show native save dialog and write file + const filePath = await this.context.dialogs.saveFile(content || 'Hello from addon!', { + defaultPath: filename || 'addon-export.txt', + filters: [ + { name: 'Text Files', extensions: ['txt'] }, + { name: 'All Files', extensions: ['*'] } + ], + title: 'Save File Demo' + }); + + if (filePath) { + this.context?.log.info(`File saved to: ${filePath}`); + this.context?.ipc.send('show-notification', { + title: 'File Saved', + body: `Saved to: ${filePath}`, + }); + return { success: true, filePath }; + } else { + // User canceled + return { success: false, canceled: true }; + } + } catch (error) { + this.context?.log.error('Failed to save file:', error); + return { success: false, error: error.message }; + } + } + + /** + * Open a file using a native Open dialog. + * + * Demonstrates context.dialogs.showOpenDialog() which shows a native + * file picker and returns the selected file path(s). + * + * @returns {Promise<{success: boolean, filePaths?: string[], error?: string}>} + */ + async openFileDemo() { + // Check if dialogs API is available + if (!this.context?.dialogs) { + this.context?.log.warn('Dialogs API not available - missing ui:dialogs permission?'); + return { success: false, error: 'Dialogs not available' }; + } + + try { + // Show native open dialog + const result = await this.context.dialogs.showOpenDialog({ + filters: [ + { name: 'Text Files', extensions: ['txt', 'md', 'json'] }, + { name: 'All Files', extensions: ['*'] } + ], + title: 'Open File Demo', + multiSelections: false + }); + + if (!result.canceled && result.filePaths.length > 0) { + this.context?.log.info(`File selected: ${result.filePaths[0]}`); + this.context?.ipc.send('show-notification', { + title: 'File Selected', + body: `Selected: ${result.filePaths[0]}`, + }); + return { success: true, filePaths: result.filePaths }; + } else { + // User canceled + return { success: false, canceled: true }; + } + } catch (error) { + this.context?.log.error('Failed to open file:', error); + return { success: false, error: error.message }; + } + } + // ============================================================ // HOST METHODS // These methods communicate with the .NET host process @@ -560,6 +663,14 @@ class MyFirstAddon { * @property {IAddonIPC} ipc - IPC communication * @property {number} [hostPort] - Port of native host (if configured) * @property {IAppStateProxy} [appState] - Access to app state + * @property {IAddonDialogs} [dialogs] - Native file dialogs (requires ui:dialogs permission) + */ + +/** + * @typedef {Object} IAddonDialogs + * @property {function} showSaveDialog - Show native save dialog + * @property {function} showOpenDialog - Show native open dialog + * @property {function} saveFile - Show save dialog and write content to file */ /**