image context menu

This commit is contained in:
刘浩远 2020-07-25 21:42:24 +08:00
parent 7a5ad6deb6
commit ebb85847fe
10 changed files with 98 additions and 9 deletions

View File

@ -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")

View File

@ -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<ArticleProps, ArticleState> {
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()
}

View File

@ -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<ContextMenuProps> {
},
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",

View File

@ -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())
}
}

View File

@ -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 }
}
}

View File

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

View File

@ -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

View File

@ -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",

View File

@ -83,7 +83,10 @@
"starredOnly": "仅星标文章",
"fullSearch": "在正文中搜索",
"showHidden": "显示隐藏文章",
"manageSources": "管理订阅源"
"manageSources": "管理订阅源",
"saveImageAs": "将图像另存为",
"copyImage": "复制图像",
"copyImageURL": "复制图像链接"
},
"searchEngine": {
"name": "搜索引擎",

View File

@ -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