feat(share): add native share

This commit is contained in:
Johannes Millan 2025-10-22 15:54:06 +02:00
parent 28510df7fb
commit 459b189e26
4 changed files with 146 additions and 35 deletions

View file

@ -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, () => {

View file

@ -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<void> {
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<void> {
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);

View file

@ -209,7 +209,6 @@ export class ShareFormatter {
});
}
// Hashtags
if (options.includeHashtags) {
parts.push('\n#productivity #timetracking #SuperProductivity');

View file

@ -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<ShareResult> {
async shareToTarget(payload: SharePayload, target: ShareTarget): Promise<ShareResult> {
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<ShareResult> {
async tryNativeShare(payload: SharePayload): Promise<ShareResult> {
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<ShareResult> {
async copyToClipboard(text: string, label: string): Promise<ShareResult> {
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) {