text context menu & load webpage

This commit is contained in:
刘浩远 2020-06-07 17:45:47 +08:00
parent b35f21a3cf
commit 3fb9252b58
14 changed files with 165 additions and 37 deletions

View File

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

View File

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

12
dist/styles.css vendored
View File

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

View File

@ -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<ArticleProps, ArticleState> {
@ -25,7 +27,8 @@ class Article extends React.Component<ArticleProps, ArticleState> {
constructor(props) {
super(props)
this.state = {
fontSize: this.getFontSize()
fontSize: this.getFontSize(),
loadWebpage: false
}
}
@ -49,33 +52,55 @@ class Article extends React.Component<ArticleProps, ArticleState> {
})
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(<>
<p className="title">{this.props.item.title}</p>
<p className="date">{this.props.item.date.toLocaleString("zh-cn", {hour12: false})}</p>
<article dangerouslySetInnerHTML={{__html: this.props.item.content}}></article>
</>))) + "&s=" + this.state.fontSize
@ -98,9 +123,15 @@ class Article extends React.Component<ArticleProps, ArticleState> {
iconProps={{iconName: "FavoriteStar"}} />
<CommandBarButton
title="字体大小"
disabled={this.state.loadWebpage}
iconProps={{iconName: "FontSize"}}
menuIconProps={{style: {display: "none"}}}
menuProps={this.fontMenuProps()} />
<CommandBarButton
title="加载网页"
className={this.state.loadWebpage ? "active" : ""}
iconProps={{iconName: "Globe"}}
onClick={this.toggleWebpage} />
<CommandBarButton
title="在浏览器中打开"
iconProps={{iconName: "NavigateExternalInline", style: {marginTop: -4}}}
@ -114,9 +145,9 @@ class Article extends React.Component<ArticleProps, ArticleState> {
</Stack>
</Stack>
<webview
id="article"
src={this.articleView()}
preload="article/preload.js"
id="article"
src={this.state.loadWebpage ? this.props.item.link : this.articleView()}
preload={this.state.loadWebpage ? null : "article/preload.js"}
partition="sandbox" />
</div>
)

View File

@ -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<CardProps> {
this.openInBrowser()
break
case 2:
this.props.contextMenu(this.props.item, e)
this.props.contextMenu(this.props.feedId, this.props.item, e)
}
}
}

View File

@ -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<ContextMenuProps> {
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<ContextMenuProps> {
? {
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<ContextMenuProps> {
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<ContextMenuProps> {
render() {
return this.props.type == ContextMenuType.Hidden ? null : (
<ContextualMenu
directionalHint={DirectionalHint.bottomLeftEdge}
items={this.getItems()}
target={this.props.event}
onDismiss={() => this.props.close()} />
target={this.props.event || this.props.position && {left: this.props.position[0], top: this.props.position[1]}}
onDismiss={this.props.close} />
)
}
}

View File

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

View File

@ -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 }) => (
<div id="root" onMouseDown={() => dispatch(closeContextMenu())}>
<div id="root"
onMouseDown={() => dispatch(closeContextMenu())}
onContextMenu={event => {
let text = document.getSelection().toString()
if (text) dispatch(openTextMenu(text, [event.clientX, event.clientY]))
}}>
<NavContainer />
<PageContainer />
<LogMenuContainer />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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