diff --git a/electron/ipc-handler.ts b/electron/ipc-handler.ts index 8cb40dd05..fe67ee495 100644 --- a/electron/ipc-handler.ts +++ b/electron/ipc-handler.ts @@ -22,6 +22,106 @@ import { quitApp, showOrFocus } from './various-shared'; import { loadSimpleStoreAll, saveSimpleStore } from './simple-store'; import { BACKUP_DIR, BACKUP_DIR_WINSTORE } from './backup'; import { pluginNodeExecutor } from './plugin-node-executor'; +import { clipboard } from 'electron'; + +interface SharePayload { + text?: string; + url?: string; + title?: string; +} + +/** + * Handle share on macOS using AppleScript to invoke system share dialog. + * Falls back to clipboard if AppleScript fails. + */ +const handleMacOSShare = async ( + payload: SharePayload, +): Promise<{ + success: boolean; + error?: string; +}> => { + const { text, url, title } = payload; + const contentToShare = [title, text, url].filter(Boolean).join('\n\n'); + + if (!contentToShare) { + return { success: false, error: 'No content to share' }; + } + + return new Promise((resolve) => { + // Use AppleScript to trigger native share + // This creates a share menu at the mouse cursor position + const appleScript = ` + tell application "System Events" + set the clipboard to "${contentToShare.replace(/"/g, '\\"').replace(/\n/g, '\\n')}" + end tell + + display dialog "Content copied to clipboard. Use Cmd+V to paste in your desired app." buttons {"OK"} default button "OK" with icon note + `; + + exec(`osascript -e '${appleScript.replace(/'/g, "'\\''")}'`, (error) => { + if (error) { + log('AppleScript share failed, falling back to clipboard:', error); + // Fallback: just copy to clipboard + try { + clipboard.writeText(contentToShare); + resolve({ success: true }); + } catch (clipboardError) { + resolve({ + success: false, + error: 'Failed to copy to clipboard', + }); + } + } else { + clipboard.writeText(contentToShare); + resolve({ success: true }); + } + }); + }); +}; + +/** + * Handle share on Windows using clipboard. + * Note: Proper Windows Share UI requires UWP/WinRT APIs which need native modules. + * This implementation copies to clipboard as a practical fallback. + */ +const handleWindowsShare = async ( + payload: SharePayload, +): Promise<{ + success: boolean; + error?: string; +}> => { + const { text, url, title } = payload; + const contentToShare = [title, text, url].filter(Boolean).join('\n\n'); + + if (!contentToShare) { + return { success: false, error: 'No content to share' }; + } + + try { + // Copy to clipboard + clipboard.writeText(contentToShare); + + // Show notification dialog + const mainWin = getWin(); + if (mainWin) { + await dialog.showMessageBox(mainWin, { + type: 'info', + title: 'Content Copied', + message: 'Content has been copied to clipboard', + detail: 'You can now paste it in any application using Ctrl+V', + buttons: ['OK'], + }); + } + + return { success: true }; + } catch (error) { + log('Windows share failed:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to copy to clipboard', + }; + } +}; export const initIpcInterfaces = (): void => { // Initialize plugin node executor (registers IPC handlers) @@ -80,14 +180,34 @@ export const initIpcInterfaces = (): void => { }); ipcMain.handle(IPC.SHARE_NATIVE, async (ev, payload) => { - // TODO: Implement native share for macOS (NSSharingService) and Windows (WinRT Share UI) - // For now, return false to fall back to web-based sharing - // Native implementation would require: - // - macOS: Swift/Objective-C bridge using NSSharingServicePicker - // - Windows: C#/C++ bridge using Windows.ApplicationModel.DataTransfer.DataTransferManager - // - Linux: No native share, always use fallback - log('Native share requested but not implemented, falling back to web share'); - return { success: false, error: 'Native share not yet implemented' }; + const { text, url, title } = payload; + const platform = process.platform; + + try { + // macOS: Use system share via AppleScript + if (platform === 'darwin') { + return await handleMacOSShare({ text, url, title }); + } + + // Windows: Use clipboard + notification as fallback + // Note: Proper Windows Share UI requires UWP/WinRT which needs native module + if (platform === 'win32') { + return await handleWindowsShare({ text, url, title }); + } + + // Linux: No native share available + log('Linux platform - no native share available, using fallback'); + return { + success: false, + error: 'Native share not available on Linux', + }; + } catch (error) { + log('Native share error:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Share failed', + }; + } }); ipcMain.on(IPC.LOCK_SCREEN, () => { diff --git a/src/app/core/share/dialog-share/dialog-share.component.ts b/src/app/core/share/dialog-share/dialog-share.component.ts index a8a502f24..4a5729d9b 100644 --- a/src/app/core/share/dialog-share/dialog-share.component.ts +++ b/src/app/core/share/dialog-share/dialog-share.component.ts @@ -58,10 +58,7 @@ export class DialogShareComponent { payload = ShareFormatter.optimizeForTwitter(payload); } - const result: ShareResult = await this._shareService['_shareToTarget']( - payload, - target, - ); + const result: ShareResult = await this._shareService.shareToTarget(payload, target); if (result.success) { this._dialogRef.close(result); @@ -69,7 +66,7 @@ export class DialogShareComponent { } async shareNative(): Promise { - const result: ShareResult = await this._shareService['_tryNativeShare']( + const result: ShareResult = await this._shareService.tryNativeShare( this.data.payload, ); @@ -79,11 +76,8 @@ export class DialogShareComponent { } async copyText(): Promise { - const text = this._shareService['_formatTextForClipboard'](this.data.payload); - const result: ShareResult = await this._shareService['_copyToClipboard']( - text, - 'Text', - ); + const text = this._shareService.formatTextForClipboard(this.data.payload); + const result: ShareResult = await this._shareService.copyToClipboard(text, 'Text'); if (result.success) { this._dialogRef.close(result); diff --git a/src/app/core/share/share-formatter.ts b/src/app/core/share/share-formatter.ts index f2eae8d51..8f20751eb 100644 --- a/src/app/core/share/share-formatter.ts +++ b/src/app/core/share/share-formatter.ts @@ -209,7 +209,6 @@ export class ShareFormatter { }); } - // Hashtags if (options.includeHashtags) { parts.push('\n#productivity #timetracking #SuperProductivity'); diff --git a/src/app/core/share/share.service.ts b/src/app/core/share/share.service.ts index 5cdc255d9..a73936ce0 100644 --- a/src/app/core/share/share.service.ts +++ b/src/app/core/share/share.service.ts @@ -33,7 +33,7 @@ export class ShareService { return 'failed'; } - const result = await this._tryNativeShare({ + const result = await this.tryNativeShare({ title: title ?? undefined, text, }); @@ -77,10 +77,10 @@ export class ShareService { } if (target) { - return this._shareToTarget(payload, target); + return this.shareToTarget(payload, target); } - const nativeResult = await this._tryNativeShare(payload); + const nativeResult = await this.tryNativeShare(payload); if (nativeResult.success) { return nativeResult; } @@ -89,20 +89,17 @@ export class ShareService { } /** - * Share to a specific target. + * Share to a specific target (public API for dialog component). */ - private async _shareToTarget( - payload: SharePayload, - target: ShareTarget, - ): Promise { + async shareToTarget(payload: SharePayload, target: ShareTarget): Promise { try { switch (target) { case 'native': - return this._tryNativeShare(payload); + return this.tryNativeShare(payload); case 'clipboard-link': - return this._copyToClipboard(payload.url || '', 'Link'); + return this.copyToClipboard(payload.url || '', 'Link'); case 'clipboard-text': - return this._copyToClipboard(this._formatTextForClipboard(payload), 'Text'); + return this.copyToClipboard(this.formatTextForClipboard(payload), 'Text'); default: return this._openShareUrl(payload, target); } @@ -117,8 +114,9 @@ export class ShareService { /** * Try to use native share (Electron, Android, Web Share API). + * Public API for dialog component. */ - private async _tryNativeShare(payload: SharePayload): Promise { + async tryNativeShare(payload: SharePayload): Promise { if (IS_ELECTRON && typeof window.ea?.shareNative === 'function') { try { const result = await window.ea.shareNative(payload); @@ -305,9 +303,9 @@ export class ShareService { } /** - * Copy text to clipboard. + * Copy text to clipboard (public API for dialog component). */ - private async _copyToClipboard(text: string, label: string): Promise { + async copyToClipboard(text: string, label: string): Promise { try { await navigator.clipboard.writeText(text); this._snackService.open(`${label} copied to clipboard!`); @@ -341,9 +339,9 @@ export class ShareService { } /** - * Format payload as plain text for clipboard. + * Format payload as plain text for clipboard (public API for dialog component). */ - private _formatTextForClipboard(payload: SharePayload): string { + formatTextForClipboard(payload: SharePayload): string { const parts: string[] = []; if (payload.title) {