mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-02-06 04:13:45 +01:00
text context menu & load webpage
This commit is contained in:
parent
b35f21a3cf
commit
3fb9252b58
8
dist/article/article.css
vendored
8
dist/article/article.css
vendored
@ -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;
|
||||
}
|
4
dist/article/article.js
vendored
4
dist/article/article.js
vendored
@ -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
12
dist/styles.css
vendored
@ -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 {
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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} />
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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 />
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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 () {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
Loading…
x
Reference in New Issue
Block a user