2020-11-07 17:59:37 +02:00
|
|
|
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
|
|
|
|
import { _ } from '@joplin/lib/locale';
|
2021-04-08 11:30:12 +02:00
|
|
|
import { copyHtmlToClipboard } from './clipboardUtils';
|
2020-06-04 19:24:11 +02:00
|
|
|
|
2020-10-09 19:35:46 +02:00
|
|
|
const bridge = require('electron').remote.require('./bridge').default;
|
2020-05-09 20:18:41 +02:00
|
|
|
const Menu = bridge().Menu;
|
|
|
|
const MenuItem = bridge().MenuItem;
|
2021-01-22 19:41:11 +02:00
|
|
|
import Resource from '@joplin/lib/models/Resource';
|
2021-05-13 10:34:03 +02:00
|
|
|
import BaseItem from '@joplin/lib/models/BaseItem';
|
|
|
|
import BaseModel from '@joplin/lib/BaseModel';
|
2021-05-03 16:13:51 +02:00
|
|
|
import { processPastedHtml } from './resourceHandling';
|
2021-05-13 10:34:03 +02:00
|
|
|
import { NoteEntity, ResourceEntity } from '@joplin/lib/services/database/types';
|
2020-05-09 20:18:41 +02:00
|
|
|
const fs = require('fs-extra');
|
|
|
|
const { clipboard } = require('electron');
|
2020-11-07 17:59:37 +02:00
|
|
|
const { toSystemSlashes } = require('@joplin/lib/path-utils');
|
2020-05-09 20:18:41 +02:00
|
|
|
|
|
|
|
export enum ContextMenuItemType {
|
|
|
|
None = '',
|
|
|
|
Image = 'image',
|
|
|
|
Resource = 'resource',
|
|
|
|
Text = 'text',
|
|
|
|
Link = 'link',
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface ContextMenuOptions {
|
2020-11-12 21:29:22 +02:00
|
|
|
itemType: ContextMenuItemType;
|
|
|
|
resourceId: string;
|
|
|
|
linkToCopy: string;
|
|
|
|
textToCopy: string;
|
|
|
|
htmlToCopy: string;
|
|
|
|
insertContent: Function;
|
|
|
|
isReadOnly?: boolean;
|
2020-05-09 20:18:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
interface ContextMenuItem {
|
2020-11-12 21:29:22 +02:00
|
|
|
label: string;
|
|
|
|
onAction: Function;
|
|
|
|
isActive: Function;
|
2020-05-09 20:18:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
interface ContextMenuItems {
|
2020-11-12 21:13:28 +02:00
|
|
|
[key: string]: ContextMenuItem;
|
2020-05-09 20:18:41 +02:00
|
|
|
}
|
|
|
|
|
2020-11-12 21:13:28 +02:00
|
|
|
async function resourceInfo(options: ContextMenuOptions): Promise<any> {
|
2020-05-09 20:18:41 +02:00
|
|
|
const resource = options.resourceId ? await Resource.load(options.resourceId) : null;
|
|
|
|
const resourcePath = resource ? Resource.fullPath(resource) : '';
|
|
|
|
return { resource, resourcePath };
|
|
|
|
}
|
|
|
|
|
2020-11-12 21:13:28 +02:00
|
|
|
function handleCopyToClipboard(options: ContextMenuOptions) {
|
2020-10-23 14:21:37 +02:00
|
|
|
if (options.textToCopy) {
|
|
|
|
clipboard.writeText(options.textToCopy);
|
|
|
|
} else if (options.htmlToCopy) {
|
2021-04-08 11:30:12 +02:00
|
|
|
copyHtmlToClipboard(options.htmlToCopy);
|
2020-10-23 14:21:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-13 10:34:03 +02:00
|
|
|
export async function openItemById(itemId: string, dispatch: Function, hash: string = '') {
|
|
|
|
|
|
|
|
const item = await BaseItem.loadItemById(itemId);
|
|
|
|
|
|
|
|
if (!item) throw new Error(`No item with ID ${itemId}`);
|
|
|
|
|
|
|
|
if (item.type_ === BaseModel.TYPE_RESOURCE) {
|
|
|
|
const resource = item as ResourceEntity;
|
|
|
|
const localState = await Resource.localState(resource);
|
|
|
|
if (localState.fetch_status !== Resource.FETCH_STATUS_DONE || !!resource.encryption_blob_encrypted) {
|
|
|
|
if (localState.fetch_status === Resource.FETCH_STATUS_ERROR) {
|
|
|
|
bridge().showErrorMessageBox(`${_('There was an error downloading this attachment:')}\n\n${localState.fetch_error}`);
|
|
|
|
} else {
|
|
|
|
bridge().showErrorMessageBox(_('This attachment is not downloaded or not decrypted yet'));
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
await ResourceEditWatcher.instance().openAndWatch(resource.id);
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
bridge().showErrorMessageBox(error.message);
|
|
|
|
}
|
|
|
|
} else if (item.type_ === BaseModel.TYPE_NOTE) {
|
|
|
|
const note = item as NoteEntity;
|
|
|
|
|
|
|
|
dispatch({
|
|
|
|
type: 'FOLDER_AND_NOTE_SELECT',
|
|
|
|
folderId: note.parent_id,
|
|
|
|
noteId: note.id,
|
|
|
|
hash,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
throw new Error(`Unsupported item type: ${item.type_}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function menuItems(dispatch: Function): ContextMenuItems {
|
2020-05-09 20:18:41 +02:00
|
|
|
return {
|
|
|
|
open: {
|
|
|
|
label: _('Open...'),
|
2020-11-12 21:13:28 +02:00
|
|
|
onAction: async (options: ContextMenuOptions) => {
|
2021-05-13 10:34:03 +02:00
|
|
|
await openItemById(options.resourceId, dispatch);
|
2020-05-09 20:18:41 +02:00
|
|
|
},
|
2020-11-12 21:13:28 +02:00
|
|
|
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
|
2020-05-09 20:18:41 +02:00
|
|
|
},
|
|
|
|
saveAs: {
|
|
|
|
label: _('Save as...'),
|
2020-11-12 21:13:28 +02:00
|
|
|
onAction: async (options: ContextMenuOptions) => {
|
2020-05-09 20:18:41 +02:00
|
|
|
const { resourcePath, resource } = await resourceInfo(options);
|
|
|
|
const filePath = bridge().showSaveDialog({
|
|
|
|
defaultPath: resource.filename ? resource.filename : resource.title,
|
|
|
|
});
|
|
|
|
if (!filePath) return;
|
|
|
|
await fs.copy(resourcePath, filePath);
|
|
|
|
},
|
2020-11-12 21:13:28 +02:00
|
|
|
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
|
2020-05-09 20:18:41 +02:00
|
|
|
},
|
|
|
|
revealInFolder: {
|
|
|
|
label: _('Reveal file in folder'),
|
2020-11-12 21:13:28 +02:00
|
|
|
onAction: async (options: ContextMenuOptions) => {
|
2020-05-09 20:18:41 +02:00
|
|
|
const { resourcePath } = await resourceInfo(options);
|
|
|
|
bridge().showItemInFolder(resourcePath);
|
|
|
|
},
|
2020-11-12 21:13:28 +02:00
|
|
|
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
|
2020-05-09 20:18:41 +02:00
|
|
|
},
|
|
|
|
copyPathToClipboard: {
|
|
|
|
label: _('Copy path to clipboard'),
|
2020-11-12 21:13:28 +02:00
|
|
|
onAction: async (options: ContextMenuOptions) => {
|
2020-05-09 20:18:41 +02:00
|
|
|
const { resourcePath } = await resourceInfo(options);
|
|
|
|
clipboard.writeText(toSystemSlashes(resourcePath));
|
|
|
|
},
|
2020-11-12 21:13:28 +02:00
|
|
|
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
|
2020-05-09 20:18:41 +02:00
|
|
|
},
|
2020-08-02 13:16:42 +02:00
|
|
|
cut: {
|
|
|
|
label: _('Cut'),
|
2020-11-12 21:13:28 +02:00
|
|
|
onAction: async (options: ContextMenuOptions) => {
|
2020-10-23 14:21:37 +02:00
|
|
|
handleCopyToClipboard(options);
|
2020-08-02 13:16:42 +02:00
|
|
|
options.insertContent('');
|
|
|
|
},
|
2020-11-12 21:13:28 +02:00
|
|
|
isActive: (_itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.isReadOnly && (!!options.textToCopy || !!options.htmlToCopy),
|
2020-08-02 13:16:42 +02:00
|
|
|
},
|
2020-05-09 20:18:41 +02:00
|
|
|
copy: {
|
|
|
|
label: _('Copy'),
|
2020-11-12 21:13:28 +02:00
|
|
|
onAction: async (options: ContextMenuOptions) => {
|
2020-10-23 14:21:37 +02:00
|
|
|
handleCopyToClipboard(options);
|
2020-05-09 20:18:41 +02:00
|
|
|
},
|
2020-11-12 21:13:28 +02:00
|
|
|
isActive: (_itemType: ContextMenuItemType, options: ContextMenuOptions) => !!options.textToCopy || !!options.htmlToCopy,
|
2020-08-02 13:16:42 +02:00
|
|
|
},
|
|
|
|
paste: {
|
|
|
|
label: _('Paste'),
|
2020-11-12 21:13:28 +02:00
|
|
|
onAction: async (options: ContextMenuOptions) => {
|
2021-05-03 16:13:51 +02:00
|
|
|
const pastedHtml = clipboard.readHTML();
|
|
|
|
let content = pastedHtml ? pastedHtml : clipboard.readText();
|
|
|
|
|
|
|
|
if (pastedHtml) {
|
|
|
|
content = await processPastedHtml(pastedHtml);
|
|
|
|
}
|
|
|
|
|
2020-08-02 13:16:42 +02:00
|
|
|
options.insertContent(content);
|
|
|
|
},
|
2020-11-12 21:13:28 +02:00
|
|
|
isActive: (_itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.isReadOnly && (!!clipboard.readText() || !!clipboard.readHTML()),
|
2020-05-09 20:18:41 +02:00
|
|
|
},
|
|
|
|
copyLinkUrl: {
|
|
|
|
label: _('Copy Link Address'),
|
2020-11-12 21:13:28 +02:00
|
|
|
onAction: async (options: ContextMenuOptions) => {
|
2020-09-08 01:29:31 +02:00
|
|
|
clipboard.writeText(options.linkToCopy !== null ? options.linkToCopy : options.textToCopy);
|
2020-05-09 20:18:41 +02:00
|
|
|
},
|
2020-11-12 21:13:28 +02:00
|
|
|
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => itemType === ContextMenuItemType.Link || !!options.linkToCopy,
|
2020-05-09 20:18:41 +02:00
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-05-13 10:34:03 +02:00
|
|
|
export default async function contextMenu(options: ContextMenuOptions, dispatch: Function) {
|
2020-05-09 20:18:41 +02:00
|
|
|
const menu = new Menu();
|
|
|
|
|
2021-05-13 10:34:03 +02:00
|
|
|
const items = menuItems(dispatch);
|
2020-05-09 20:18:41 +02:00
|
|
|
|
2020-08-02 13:16:42 +02:00
|
|
|
if (!('readyOnly' in options)) options.isReadOnly = true;
|
|
|
|
|
2020-05-09 20:18:41 +02:00
|
|
|
for (const itemKey in items) {
|
|
|
|
const item = items[itemKey];
|
|
|
|
|
2020-08-02 13:16:42 +02:00
|
|
|
if (!item.isActive(options.itemType, options)) continue;
|
2020-05-09 20:18:41 +02:00
|
|
|
|
|
|
|
menu.append(new MenuItem({
|
|
|
|
label: item.label,
|
|
|
|
click: () => {
|
|
|
|
item.onAction(options);
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
return menu;
|
|
|
|
}
|