From 3fb9252b581c4feb968fb86c7eac0f36fb1fa78e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B5=A9=E8=BF=9C?= Date: Sun, 7 Jun 2020 17:45:47 +0800 Subject: [PATCH] text context menu & load webpage --- dist/article/article.css | 8 +++- dist/article/article.js | 4 ++ dist/styles.css | 12 +++++- src/components/article.tsx | 47 +++++++++++++++++++---- src/components/cards/card.tsx | 4 +- src/components/context-menu.tsx | 45 ++++++++++++++++++---- src/components/feeds/feed.tsx | 2 +- src/components/root.tsx | 9 ++++- src/containers/article-container.tsx | 4 +- src/containers/context-menu-container.tsx | 15 ++++++-- src/containers/feed-container.tsx | 2 +- src/electron.ts | 4 +- src/scripts/models/app.ts | 38 +++++++++++++++--- src/scripts/utils.ts | 8 +++- 14 files changed, 165 insertions(+), 37 deletions(-) diff --git a/dist/article/article.css b/dist/article/article.css index c333c78..1351e00 100644 --- a/dist/article/article.css +++ b/dist/article/article.css @@ -21,12 +21,17 @@ a:hover, a:active { font-size: 1.25rem; line-height: 1.75rem; font-weight: 600; + margin-block-end: 0; +} +#main > p.date { + color: #484644; + font-size: .875rem; } article { line-height: 1.6; } -article > * { +article * { max-width: 100%; } article img { @@ -39,4 +44,5 @@ article figure { article figure figcaption { font-size: .875rem; color: #484644; + -webkit-user-modify: read-only; } \ No newline at end of file diff --git a/dist/article/article.js b/dist/article/article.js index 1810a39..1295c7c 100644 --- a/dist/article/article.js +++ b/dist/article/article.js @@ -8,4 +8,8 @@ main.innerHTML = decodeURIComponent(window.atob(get("h"))) document.addEventListener("click", event => { event.preventDefault() if (event.target.href) post("request-navigation", event.target.href) +}) +document.addEventListener("contextmenu", event => { + let text = document.getSelection().toString() + if (text) post("context-menu", [event.clientX, event.clientY], text) }) \ No newline at end of file diff --git a/dist/styles.css b/dist/styles.css index d976e70..264d5ef 100644 --- a/dist/styles.css +++ b/dist/styles.css @@ -32,7 +32,13 @@ html, body { background: #a4262c; border-color: #a4262c; } - +.ms-Button--commandBar.active { + background-color: rgb(237, 235, 233); + color: rgb(32, 31, 30); +} +.ms-Button--commandBar.active .ms-Button-icon { + color: rgb(0, 90, 158); +} i.ms-Nav-chevron { line-height: 32px; height: 32px; @@ -43,7 +49,7 @@ i.ms-Nav-chevron { .ms-ActivityItem-activityTypeIcon, .ms-ActivityItem-timeStamp { user-select: none; } -.ms-Label { +.ms-Label, .ms-Spinner-label { user-select: none; } @@ -232,6 +238,7 @@ img.favicon { width: 16px; height: 16px; vertical-align: middle; + user-select: none; } .ms-DetailsList-contentWrapper { max-height: 400px; @@ -329,6 +336,7 @@ img.favicon { } .article .actions .source-name { line-height: 35px; + user-select: none; } .cards-feed-container { diff --git a/src/components/article.tsx b/src/components/article.tsx index daf2dca..1c345ab 100644 --- a/src/components/article.tsx +++ b/src/components/article.tsx @@ -13,10 +13,12 @@ type ArticleProps = { source: RSSSource dismiss: () => void toggleHasRead: (item: RSSItem) => void + textMenu: (text: string, position: [number, number]) => void } type ArticleState = { fontSize: number + loadWebpage: boolean } class Article extends React.Component { @@ -25,7 +27,8 @@ class Article extends React.Component { constructor(props) { super(props) this.state = { - fontSize: this.getFontSize() + fontSize: this.getFontSize(), + loadWebpage: false } } @@ -49,33 +52,55 @@ class Article extends React.Component { }) ipcHandler = event => { - if (event.channel === "request-navigation") { - openExternal(event.args[0]) + switch (event.channel) { + case "request-navigation": { + openExternal(event.args[0]) + break + } + case "context-menu": { + let articlePos = document.getElementById("article").getBoundingClientRect() + let [x, y] = event.args[0] + this.props.textMenu(event.args[1], [x + articlePos.x, y + articlePos.y]) + break + } } } popUpHandler = event => { openExternal(event.url) } + navigationHandler = event => { + openExternal(event.url) + this.props.dismiss() + } componentDidMount = () => { this.webview = document.getElementById("article") this.webview.addEventListener("ipc-message", this.ipcHandler) this.webview.addEventListener("new-window", this.popUpHandler) - this.webview.addEventListener("will-navigate", this.props.dismiss) + this.webview.addEventListener("will-navigate", this.navigationHandler) } componentWillUnmount = () => { this.webview.removeEventListener("ipc-message", this.ipcHandler) this.webview.removeEventListener("new-window", this.popUpHandler) - this.webview.removeEventListener("will-navigate", this.props.dismiss) + this.webview.removeEventListener("will-navigate", this.navigationHandler) } openInBrowser = () => { openExternal(this.props.item.link) } + toggleWebpage = () => { + if (this.state.loadWebpage) { + this.setState({loadWebpage: false}) + } else if (this.props.item.link.startsWith("https://") || this.props.item.link.startsWith("http://")) { + this.setState({loadWebpage: true}) + } + } + articleView = () => "article/article.html?h=" + window.btoa(encodeURIComponent(renderToString(<>

{this.props.item.title}

+

{this.props.item.date.toLocaleString("zh-cn", {hour12: false})}

))) + "&s=" + this.state.fontSize @@ -98,9 +123,15 @@ class Article extends React.Component { iconProps={{iconName: "FavoriteStar"}} /> + { ) diff --git a/src/components/cards/card.tsx b/src/components/cards/card.tsx index 61ddedb..9b6d3e6 100644 --- a/src/components/cards/card.tsx +++ b/src/components/cards/card.tsx @@ -9,7 +9,7 @@ export interface CardProps { item: RSSItem source: RSSSource markRead: (item: RSSItem) => void - contextMenu: (item: RSSItem, e) => void + contextMenu: (feedId: FeedIdType, item: RSSItem, e) => void showItem: (fid: FeedIdType, item: RSSItem) => void } @@ -34,7 +34,7 @@ export class Card extends React.Component { this.openInBrowser() break case 2: - this.props.contextMenu(this.props.item, e) + this.props.contextMenu(this.props.feedId, this.props.item, e) } } } \ No newline at end of file diff --git a/src/components/context-menu.tsx b/src/components/context-menu.tsx index c094899..f6072f1 100644 --- a/src/components/context-menu.tsx +++ b/src/components/context-menu.tsx @@ -1,24 +1,38 @@ import * as React from "react" import { clipboard } from "electron" -import { openExternal } from "../scripts/utils" -import { ContextualMenu, IContextualMenuItem, ContextualMenuItemType } from "office-ui-fabric-react/lib/ContextualMenu" +import { openExternal, cutText, googleSearch } from "../scripts/utils" +import { ContextualMenu, IContextualMenuItem, ContextualMenuItemType, DirectionalHint } from "office-ui-fabric-react/lib/ContextualMenu" import { ContextMenuType } from "../scripts/models/app" import { RSSItem } from "../scripts/models/item" import { ContextReduxProps } from "../containers/context-menu-container" +import { FeedIdType } from "../scripts/models/feed" export type ContextMenuProps = ContextReduxProps & { type: ContextMenuType event?: MouseEvent + position?: [number, number] item?: RSSItem - markRead: Function - markUnread: Function - close: Function + feedId?: FeedIdType + text?: string + showItem: (feedId: FeedIdType, item: RSSItem) => void + markRead: (item: RSSItem) => void + markUnread: (item: RSSItem) => void + close: () => void } export class ContextMenu extends React.Component { getItems = (): IContextualMenuItem[] => { switch (this.props.type) { case ContextMenuType.Item: return [ + { + key: "showItem", + text: "阅读", + iconProps: { iconName: "TextDocument" }, + onClick: () => { + this.props.markRead(this.props.item) + this.props.showItem(this.props.feedId, this.props.item) + } + }, { key: "openInBrowser", text: "在浏览器中打开", @@ -32,7 +46,7 @@ export class ContextMenu extends React.Component { ? { key: "markAsUnread", text: "标为未读", - iconProps: { iconName: "StatusCircleInner", style: { fontSize: 12, textAlign: "center" } }, + iconProps: { iconName: "RadioBtnOn", style: { fontSize: 14, textAlign: "center" } }, onClick: () => { this.props.markUnread(this.props.item) } } : { @@ -60,6 +74,20 @@ export class ContextMenu extends React.Component { onClick: () => { clipboard.writeText(this.props.item.link) } } ] + case ContextMenuType.Text: return [ + { + key: "copyText", + text: "复制", + iconProps: { iconName: "Copy" }, + onClick: () => { clipboard.writeText(this.props.text) } + }, + { + key: "searchText", + text: `使用Google搜索“${cutText(this.props.text, 15)}”`, + iconProps: { iconName: "Search" }, + onClick: () => { googleSearch(this.props.text) } + } + ] default: return [] } } @@ -67,9 +95,10 @@ export class ContextMenu extends React.Component { render() { return this.props.type == ContextMenuType.Hidden ? null : ( this.props.close()} /> + target={this.props.event || this.props.position && {left: this.props.position[0], top: this.props.position[1]}} + onDismiss={this.props.close} /> ) } } \ No newline at end of file diff --git a/src/components/feeds/feed.tsx b/src/components/feeds/feed.tsx index 55ef878..024697d 100644 --- a/src/components/feeds/feed.tsx +++ b/src/components/feeds/feed.tsx @@ -8,7 +8,7 @@ type FeedProps = FeedReduxProps & { items: RSSItem[] sourceMap: Object markRead: (item: RSSItem) => void - contextMenu: (item: RSSItem, e) => void + contextMenu: (feedId: FeedIdType, item: RSSItem, e) => void loadMore: (feed: RSSFeed) => void showItem: (fid: FeedIdType, item: RSSItem) => void } diff --git a/src/components/root.tsx b/src/components/root.tsx index 759879c..7a511f1 100644 --- a/src/components/root.tsx +++ b/src/components/root.tsx @@ -1,7 +1,7 @@ import * as React from "react" import { connect } from 'react-redux' import { ContextMenuContainer } from "../containers/context-menu-container" -import { closeContextMenu } from "../scripts/models/app" +import { closeContextMenu, openTextMenu } from "../scripts/models/app" import PageContainer from "../containers/page-container" import MenuContainer from "../containers/menu-container" import NavContainer from "../containers/nav-container" @@ -9,7 +9,12 @@ import LogMenuContainer from "../containers/log-menu-container" import SettingsContainer from "../containers/settings-container" const Root = ({ dispatch }) => ( -
dispatch(closeContextMenu())}> +
dispatch(closeContextMenu())} + onContextMenu={event => { + let text = document.getSelection().toString() + if (text) dispatch(openTextMenu(text, [event.clientX, event.clientY])) + }}> diff --git a/src/containers/article-container.tsx b/src/containers/article-container.tsx index 6f41f66..df7d9f5 100644 --- a/src/containers/article-container.tsx +++ b/src/containers/article-container.tsx @@ -5,6 +5,7 @@ import { RSSItem, markUnread, markRead } from "../scripts/models/item" import { AppDispatch } from "../scripts/utils" import { dismissItem } from "../scripts/models/page" import Article from "../components/article" +import { openTextMenu } from "../scripts/models/app" type ArticleContainerProps = { itemId: number @@ -26,7 +27,8 @@ const makeMapStateToProps = () => { const mapDispatchToProps = (dispatch: AppDispatch) => { return { dismiss: () => dispatch(dismissItem()), - toggleHasRead: (item: RSSItem) => dispatch(item.hasRead ? markUnread(item) : markRead(item)) + toggleHasRead: (item: RSSItem) => dispatch(item.hasRead ? markUnread(item) : markRead(item)), + textMenu: (text: string, position: [number, number]) => dispatch(openTextMenu(text, position)) } } diff --git a/src/containers/context-menu-container.tsx b/src/containers/context-menu-container.tsx index 58f8f0e..1a1bbba 100644 --- a/src/containers/context-menu-container.tsx +++ b/src/containers/context-menu-container.tsx @@ -4,6 +4,8 @@ import { RootState } from "../scripts/reducer" import { ContextMenuType, closeContextMenu } from "../scripts/models/app" import { ContextMenu } from "../components/context-menu" import { RSSItem, markRead, markUnread } from "../scripts/models/item" +import { showItem } from "../scripts/models/page" +import { FeedIdType } from "../scripts/models/feed" const getContext = (state: RootState) => state.app.contextMenu @@ -14,7 +16,13 @@ const mapStateToProps = createSelector( case ContextMenuType.Item: return { type: context.type, event: context.event, - item: context.target as RSSItem + item: context.target[0], + feedId: context.target[1] + } + case ContextMenuType.Text: return { + type: context.type, + position: context.position, + text: context.target as string } default: return { type: ContextMenuType.Hidden } } @@ -23,8 +31,9 @@ const mapStateToProps = createSelector( const mapDispatchToProps = dispatch => { return { - markRead: item => dispatch(markRead(item)), - markUnread: item => dispatch(markUnread(item)), + showItem: (feedId: FeedIdType, item: RSSItem) => dispatch(showItem(feedId, item)), + markRead: (item: RSSItem) => dispatch(markRead(item)), + markUnread: (item: RSSItem) => dispatch(markUnread(item)), close: () => dispatch(closeContextMenu()) } } diff --git a/src/containers/feed-container.tsx b/src/containers/feed-container.tsx index fc7a858..cef66d4 100644 --- a/src/containers/feed-container.tsx +++ b/src/containers/feed-container.tsx @@ -28,7 +28,7 @@ const makeMapStateToProps = () => { const mapDispatchToProps = dispatch => { return { markRead: (item: RSSItem) => dispatch(markRead(item)), - contextMenu: (item: RSSItem, e) => dispatch(openItemMenu(item, e)), + contextMenu: (feedId: FeedIdType, item: RSSItem, e) => dispatch(openItemMenu(item, feedId, e)), loadMore: (feed: RSSFeed) => dispatch(loadMore(feed)), showItem: (fid: FeedIdType, item: RSSItem) => dispatch(showItem(fid, item)) } diff --git a/src/electron.ts b/src/electron.ts index da62e3b..d5e4a7b 100644 --- a/src/electron.ts +++ b/src/electron.ts @@ -1,4 +1,4 @@ -import { app, ipcMain, BrowserWindow } from "electron" +import { app, ipcMain, BrowserWindow, Menu } from "electron" import windowStateKeeper = require("electron-window-state") let mainWindow: BrowserWindow @@ -29,6 +29,8 @@ function createWindow() { mainWindow.webContents.openDevTools() } +Menu.setApplicationMenu(null) + app.on('ready', createWindow) app.on('window-all-closed', function () { diff --git a/src/scripts/models/app.ts b/src/scripts/models/app.ts index e071b9b..f9276c8 100644 --- a/src/scripts/models/app.ts +++ b/src/scripts/models/app.ts @@ -1,12 +1,12 @@ import { RSSSource, INIT_SOURCES, SourceActionTypes, ADD_SOURCE, UPDATE_SOURCE, DELETE_SOURCE } from "./source" import { RSSItem, ItemActionTypes, FETCH_ITEMS } from "./item" import { ActionStatus, AppThunk, getWindowBreakpoint } from "../utils" -import { INIT_FEEDS, FeedActionTypes, ALL, initFeeds } from "./feed" +import { INIT_FEEDS, FeedActionTypes, ALL, initFeeds, FeedIdType } from "./feed" import { SourceGroupActionTypes, UPDATE_SOURCE_GROUP, ADD_SOURCE_TO_GROUP, DELETE_SOURCE_GROUP, REMOVE_SOURCE_FROM_GROUP } from "./group" import { PageActionTypes, SELECT_PAGE, PageType, selectAllArticles } from "./page" export enum ContextMenuType { - Hidden, Item + Hidden, Item, Text } export enum AppLogType { @@ -50,7 +50,8 @@ export class AppState { contextMenu: { type: ContextMenuType, event?: MouseEvent | string, - target?: RSSItem | RSSSource + position?: [number, number], + target?: [RSSItem, FeedIdType] | RSSSource | string } constructor() { @@ -62,6 +63,7 @@ export class AppState { export const CLOSE_CONTEXT_MENU = "CLOSE_CONTEXT_MENU" export const OPEN_ITEM_MENU = "OPEN_ITEM_MENU" +export const OPEN_TEXT_MENU = "OPEN_TEXT_MENU" interface CloseContextMenuAction { type: typeof CLOSE_CONTEXT_MENU @@ -71,9 +73,16 @@ interface OpenItemMenuAction { type: typeof OPEN_ITEM_MENU event: MouseEvent item: RSSItem + feedId: FeedIdType } -export type ContextMenuActionTypes = CloseContextMenuAction | OpenItemMenuAction +interface OpenTextMenuAction { + type: typeof OPEN_TEXT_MENU + position: [number, number] + item: string +} + +export type ContextMenuActionTypes = CloseContextMenuAction | OpenItemMenuAction | OpenTextMenuAction export const TOGGLE_LOGS = "TOGGLE_LOGS" export interface LogMenuActionType { type: typeof TOGGLE_LOGS } @@ -95,11 +104,20 @@ export function closeContextMenu(): ContextMenuActionTypes { return { type: CLOSE_CONTEXT_MENU } } -export function openItemMenu(item: RSSItem, event: React.MouseEvent): ContextMenuActionTypes { +export function openItemMenu(item: RSSItem, feedId: FeedIdType, event: React.MouseEvent): ContextMenuActionTypes { return { type: OPEN_ITEM_MENU, event: event.nativeEvent, - item: item + item: item, + feedId: feedId + } +} + +export function openTextMenu(text: string, position: [number, number]): ContextMenuActionTypes { + return { + type: OPEN_TEXT_MENU, + position: position, + item: text } } @@ -245,6 +263,14 @@ export function appReducer( contextMenu: { type: ContextMenuType.Item, event: action.event, + target: [action.item, action.feedId] + } + } + case OPEN_TEXT_MENU: return { + ...state, + contextMenu: { + type: ContextMenuType.Text, + position: action.position, target: action.item } } diff --git a/src/scripts/utils.ts b/src/scripts/utils.ts index ce89d52..d4a7377 100644 --- a/src/scripts/utils.ts +++ b/src/scripts/utils.ts @@ -82,4 +82,10 @@ export function openExternal(url: string) { export const urlTest = (s: string) => /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi.test(s) -export const getWindowBreakpoint = () => remote.getCurrentWindow().getSize()[0] >= 1441 \ No newline at end of file +export const getWindowBreakpoint = () => remote.getCurrentWindow().getSize()[0] >= 1441 + +export const cutText = (s: string, length: number) => { + return (s.length <= length) ? s : s.slice(0, length) + "…" +} + +export const googleSearch = (text: string) => openExternal("https://www.google.com/search?q=" + encodeURIComponent(text))