mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
feat(share): add native share
This commit is contained in:
parent
28510df7fb
commit
459b189e26
4 changed files with 146 additions and 35 deletions
|
|
@ -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, () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -209,7 +209,6 @@ export class ShareFormatter {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
// Hashtags
|
||||
if (options.includeHashtags) {
|
||||
parts.push('\n#productivity #timetracking #SuperProductivity');
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue