diff --git a/src/bridges/utils.ts b/src/bridges/utils.ts index 789128c..aa838a6 100644 --- a/src/bridges/utils.ts +++ b/src/bridges/utils.ts @@ -1,4 +1,5 @@ import { ipcRenderer } from "electron" +import { ImageCallbackTypes } from "../schema-types" const utilsBridge = { platform: process.platform, @@ -54,6 +55,9 @@ const utilsBridge = { callback(pos, text) }) }, + imageCallback: (type: ImageCallbackTypes) => { + ipcRenderer.invoke("image-callback", type) + }, addWebviewKeydownListener: (callback: (event: Electron.Input) => any) => { ipcRenderer.removeAllListeners("webview-keydown") diff --git a/src/components/article.tsx b/src/components/article.tsx index aa610ff..e7ee012 100644 --- a/src/components/article.tsx +++ b/src/components/article.tsx @@ -19,6 +19,7 @@ type ArticleProps = { toggleStarred: (item: RSSItem) => void toggleHidden: (item: RSSItem) => void textMenu: (text: string, position: [number, number]) => void + imageMenu: (position: [number, number]) => void dismissContextMenu: () => void } @@ -95,7 +96,8 @@ class Article extends React.Component { contextMenuHandler = (pos: [number, number], text: string) => { if (pos) { - this.props.textMenu(text, pos) + if (text) this.props.textMenu(text, pos) + else this.props.imageMenu(pos) } else { this.props.dismissContextMenu() } diff --git a/src/components/context-menu.tsx b/src/components/context-menu.tsx index c6e4345..cfbbfdf 100644 --- a/src/components/context-menu.tsx +++ b/src/components/context-menu.tsx @@ -6,7 +6,7 @@ import { ContextualMenu, IContextualMenuItem, ContextualMenuItemType, Directiona import { ContextMenuType } from "../scripts/models/app" import { RSSItem } from "../scripts/models/item" import { ContextReduxProps } from "../containers/context-menu-container" -import { ViewType } from "../schema-types" +import { ViewType, ImageCallbackTypes } from "../schema-types" import { FilterType } from "../scripts/models/feed" export type ContextMenuProps = ContextReduxProps & { @@ -152,6 +152,32 @@ export class ContextMenu extends React.Component { }, getSearchItem(this.props.text) ] + case ContextMenuType.Image: return [ + { + key: "openInBrowser", + text: intl.get("openExternal"), + iconProps: { iconName: "NavigateExternalInline" }, + onClick: () => { window.utils.imageCallback(ImageCallbackTypes.OpenExternal) } + }, + { + key: "saveImageAs", + text: intl.get("context.saveImageAs"), + iconProps: { iconName: "SaveTemplate" }, + onClick: () => { window.utils.imageCallback(ImageCallbackTypes.SaveAs) } + }, + { + key: "copyImage", + text: intl.get("context.copyImage"), + iconProps: { iconName: "FileImage" }, + onClick: () => { window.utils.imageCallback(ImageCallbackTypes.Copy) } + }, + { + key: "copyImageURL", + text: intl.get("context.copyImageURL"), + iconProps: { iconName: "Link" }, + onClick: () => { window.utils.imageCallback(ImageCallbackTypes.CopyLink) } + } + ] case ContextMenuType.View: return [ { key: "section_1", diff --git a/src/containers/article-container.tsx b/src/containers/article-container.tsx index 4f0cd7e..897221d 100644 --- a/src/containers/article-container.tsx +++ b/src/containers/article-container.tsx @@ -5,7 +5,7 @@ import { RSSItem, markUnread, markRead, toggleStarred, toggleHidden, itemShortcu import { AppDispatch } from "../scripts/utils" import { dismissItem, showOffsetItem } from "../scripts/models/page" import Article from "../components/article" -import { openTextMenu, closeContextMenu } from "../scripts/models/app" +import { openTextMenu, closeContextMenu, openImageMenu } from "../scripts/models/app" type ArticleContainerProps = { itemId: string @@ -35,6 +35,7 @@ const mapDispatchToProps = (dispatch: AppDispatch) => { toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)), toggleHidden: (item: RSSItem) => dispatch(toggleHidden(item)), textMenu: (text: string, position: [number, number]) => dispatch(openTextMenu(text, position)), + imageMenu: (position: [number, number]) => dispatch(openImageMenu(position)), dismissContextMenu: () => dispatch(closeContextMenu()) } } diff --git a/src/containers/context-menu-container.tsx b/src/containers/context-menu-container.tsx index e6e2c7e..ecc5b65 100644 --- a/src/containers/context-menu-container.tsx +++ b/src/containers/context-menu-container.tsx @@ -38,6 +38,10 @@ const mapStateToProps = createSelector( event: context.event, sids: context.target } + case ContextMenuType.Image: return { + type: context.type, + position: context.position + } default: return { type: ContextMenuType.Hidden } } } diff --git a/src/main/utils.ts b/src/main/utils.ts index c3ae5c9..d1fb2f0 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -1,6 +1,7 @@ import { ipcMain, shell, dialog, app, session, webContents, clipboard } from "electron" import { WindowManager } from "./window" import fs = require("fs") +import { ImageCallbackTypes } from "../schema-types" export function openExternal(url: string) { if (url.startsWith("https://") || url.startsWith("http://")) @@ -88,8 +89,29 @@ export function setUtilsListeners(manager: WindowManager) { } }) contents.on("context-menu", (_, params) => { - if (params.selectionText && manager.hasWindow()) { - manager.mainWindow.webContents.send("webview-context-menu", [params.x, params.y], params.selectionText) + if ((params.hasImageContents || params.selectionText) && manager.hasWindow()) { + if (params.hasImageContents) { + ipcMain.removeHandler("image-callback") + ipcMain.handleOnce("image-callback", (_, type: ImageCallbackTypes) => { + switch (type) { + case ImageCallbackTypes.OpenExternal: + openExternal(params.srcURL) + break + case ImageCallbackTypes.SaveAs: + contents.session.downloadURL(params.srcURL) + break + case ImageCallbackTypes.Copy: + contents.copyImageAt(params.x, params.y) + break + case ImageCallbackTypes.CopyLink: + clipboard.writeText(params.srcURL) + break + } + }) + manager.mainWindow.webContents.send("webview-context-menu", [params.x, params.y]) + } else { + manager.mainWindow.webContents.send("webview-context-menu", [params.x, params.y], params.selectionText) + } contents.executeJavaScript(`new Promise(resolve => { const dismiss = () => { document.removeEventListener("mousedown", dismiss) diff --git a/src/schema-types.ts b/src/schema-types.ts index 6da8ead..b1fc4ef 100644 --- a/src/schema-types.ts +++ b/src/schema-types.ts @@ -32,6 +32,10 @@ export const enum SearchEngines { Google, Bing, Baidu, DuckDuckGo } +export const enum ImageCallbackTypes { + OpenExternal, SaveAs, Copy, CopyLink +} + export type SchemaTypes = { version: string theme: ThemeSettings diff --git a/src/scripts/i18n/en-US.json b/src/scripts/i18n/en-US.json index a44c6a7..302040e 100644 --- a/src/scripts/i18n/en-US.json +++ b/src/scripts/i18n/en-US.json @@ -83,7 +83,10 @@ "starredOnly": "Starred only", "fullSearch": "Search in full text", "showHidden": "Show hidden articles", - "manageSources": "Manage sources" + "manageSources": "Manage sources", + "saveImageAs": "Save image as …", + "copyImage": "Copy image", + "copyImageURL": "Copy image link" }, "searchEngine": { "name": "Search engine", diff --git a/src/scripts/i18n/zh-CN.json b/src/scripts/i18n/zh-CN.json index 3907713..4bc4608 100644 --- a/src/scripts/i18n/zh-CN.json +++ b/src/scripts/i18n/zh-CN.json @@ -83,7 +83,10 @@ "starredOnly": "仅星标文章", "fullSearch": "在正文中搜索", "showHidden": "显示隐藏文章", - "manageSources": "管理订阅源" + "manageSources": "管理订阅源", + "saveImageAs": "将图像另存为", + "copyImage": "复制图像", + "copyImageURL": "复制图像链接" }, "searchEngine": { "name": "搜索引擎", diff --git a/src/scripts/models/app.ts b/src/scripts/models/app.ts index 70bd0ca..1d7ecff 100644 --- a/src/scripts/models/app.ts +++ b/src/scripts/models/app.ts @@ -10,7 +10,7 @@ import locales from "../i18n/_locales" import * as db from "../db" export const enum ContextMenuType { - Hidden, Item, Text, View, Group + Hidden, Item, Text, View, Group, Image } export const enum AppLogType { @@ -74,6 +74,7 @@ export const OPEN_ITEM_MENU = "OPEN_ITEM_MENU" export const OPEN_TEXT_MENU = "OPEN_TEXT_MENU" export const OPEN_VIEW_MENU = "OPEN_VIEW_MENU" export const OPEN_GROUP_MENU = "OPEN_GROUP_MENU" +export const OPEN_IMAGE_MENU = "OPEN_IMAGE_MENU" interface CloseContextMenuAction { type: typeof CLOSE_CONTEXT_MENU @@ -102,8 +103,13 @@ interface OpenGroupMenuAction { sids: number[] } +interface OpenImageMenuAction { + type: typeof OPEN_IMAGE_MENU + position: [number, number] +} + export type ContextMenuActionTypes = CloseContextMenuAction | OpenItemMenuAction - | OpenTextMenuAction | OpenViewMenuAction | OpenGroupMenuAction + | OpenTextMenuAction | OpenViewMenuAction | OpenGroupMenuAction | OpenImageMenuAction export const TOGGLE_LOGS = "TOGGLE_LOGS" export const PUSH_NOTIFICATION = "PUSH_NOTIFICATION" @@ -163,6 +169,13 @@ export function openGroupMenu(sids: number[], event: React.MouseEvent): ContextM } } +export function openImageMenu(position: [number, number]): ContextMenuActionTypes { + return { + type: OPEN_IMAGE_MENU, + position: position + } +} + export function toggleMenu(): AppThunk { return (dispatch, getState) => { dispatch({ type: TOGGLE_MENU }) @@ -422,6 +435,13 @@ export function appReducer( target: action.sids } } + case OPEN_IMAGE_MENU: return { + ...state, + contextMenu: { + type: ContextMenuType.Image, + position: action.position + } + } case TOGGLE_MENU: return { ...state, menu: !state.menu