add actions to article

This commit is contained in:
刘浩远 2020-06-07 13:03:19 +08:00
parent 288907fdae
commit 430fdf1fd4
20 changed files with 177 additions and 50 deletions

View File

@ -1,7 +1,11 @@
html, body { html, body {
font-family: "Segoe UI Regular", "Source Han Sans SC Regular", "Microsoft YaHei", sans-serif; font-family: "Segoe UI Regular", "Source Han Sans SC Regular", "Microsoft YaHei", sans-serif;
margin: 16px 48px; }
overflow-x: hidden; html {
overflow: hidden scroll;
}
body {
margin: 12px 96px 32px;
} }
a { a {
@ -22,8 +26,10 @@ a:hover, a:active {
article { article {
line-height: 1.6; line-height: 1.6;
} }
article img { article > * {
max-width: 100%; max-width: 100%;
}
article img {
height: auto; height: auto;
} }
article figure { article figure {

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" <meta http-equiv="Content-Security-Policy"
content="default-src 'self'; img-src *; style-src 'self' 'unsafe-inline'; frame-src *"> content="default-src 'self'; img-src *; style-src 'self' 'unsafe-inline'; frame-src *; media-src *">
<title>Hello World!</title> <title>Hello World!</title>
<link rel="stylesheet" href="article.css" /> <link rel="stylesheet" href="article.css" />
</head> </head>

View File

@ -2,9 +2,10 @@ function get(name) {
if (name = (new RegExp('[?&]' + encodeURIComponent(name) + '=([^&]*)')).exec(location.search)) if (name = (new RegExp('[?&]' + encodeURIComponent(name) + '=([^&]*)')).exec(location.search))
return decodeURIComponent(name[1]); return decodeURIComponent(name[1]);
} }
document.documentElement.style.fontSize = get("s") + "px"
let main = document.getElementById("main") let main = document.getElementById("main")
main.innerHTML = decodeURIComponent(window.atob(get("h"))) main.innerHTML = decodeURIComponent(window.atob(get("h")))
document.addEventListener("click", event => { document.addEventListener("click", event => {
event.preventDefault() event.preventDefault()
if (event.target.href) ipcRenderer.sendToHost("request-navigation", event.target.href) if (event.target.href) post("request-navigation", event.target.href)
}) })

View File

@ -1 +1 @@
global.ipcRenderer = require("electron").ipcRenderer global.post = require("electron").ipcRenderer.sendToHost

BIN
dist/icons/fabric-icons-5-f95ba260.woff vendored Normal file

Binary file not shown.

20
dist/styles.css vendored
View File

@ -117,7 +117,7 @@ nav.menu-on .btn-group .btn, nav.hide-btns .btn-group .btn {
nav.menu-on .btn-group .btn.system, nav.hide-btns .btn-group .btn.system { nav.menu-on .btn-group .btn.system, nav.hide-btns .btn-group .btn.system {
display: inline-block; display: inline-block;
} }
nav.menu-on .btn-group .btn.system { nav.menu-on .btn-group .btn.system, nav.item-on .btn-group .btn.system {
color: #fff; color: #fff;
} }
.btn-group .btn:hover { .btn-group .btn:hover {
@ -161,7 +161,6 @@ nav.menu-on .btn-group .btn.system {
} }
.article-container { .article-container {
z-index: 6; z-index: 6;
background-color: #fff6;
} }
.menu-container .menu { .menu-container .menu {
position: absolute; position: absolute;
@ -273,6 +272,9 @@ img.favicon {
nav.menu-on .btn-group .btn.system { nav.menu-on .btn-group .btn.system {
color: #000; color: #000;
} }
nav.item-on .btn-group .btn.system {
color: #fff;
}
.menu-container { .menu-container {
width: 280px; width: 280px;
} }
@ -313,9 +315,21 @@ img.favicon {
} }
.article webview { .article webview {
width: 100%; width: 100%;
height: calc(100vh - 50px); height: calc(100vh - 86px);
border: none; border: none;
} }
.article i.ms-Icon {
color: #161514;
}
.article .actions {
border-bottom: 1px solid #e1dfdd;
}
.article .actions .favicon {
margin-right: 8px;
}
.article .actions .source-name {
line-height: 35px;
}
.cards-feed-container { .cards-feed-container {
display: inline-flex; display: inline-flex;

View File

@ -2,47 +2,122 @@ import * as React from "react"
import { renderToString } from "react-dom/server" import { renderToString } from "react-dom/server"
import { RSSItem } from "../scripts/models/item" import { RSSItem } from "../scripts/models/item"
import { openExternal } from "../scripts/utils" import { openExternal } from "../scripts/utils"
import { Stack, CommandBarButton, IContextualMenuProps } from "@fluentui/react"
import { RSSSource } from "../scripts/models/source"
const FONT_SIZE_STORE_KEY = "fontSize"
const FONT_SIZE_OPTIONS = [12, 13, 14, 15, 16, 17, 18, 19, 20]
type ArticleProps = { type ArticleProps = {
item: RSSItem item: RSSItem
source: RSSSource
dismiss: () => void dismiss: () => void
toggleHasRead: (item: RSSItem) => void
} }
class Article extends React.Component<ArticleProps> { type ArticleState = {
webview: HTMLWebViewElement fontSize: number
}
class Article extends React.Component<ArticleProps, ArticleState> {
webview: HTMLWebViewElement
constructor(props) { constructor(props) {
super(props) super(props)
this.state = {
fontSize: this.getFontSize()
}
} }
getFontSize = () => {
let size = window.localStorage.getItem(FONT_SIZE_STORE_KEY)
return size ? parseInt(size) : 16
}
setFontSize = (size: number) => {
window.localStorage.setItem(FONT_SIZE_STORE_KEY, String(size))
this.setState({fontSize: size})
}
fontMenuProps = (): IContextualMenuProps => ({
items: FONT_SIZE_OPTIONS.map(size => ({
key: String(size),
text: String(size),
canCheck: true,
checked: size === this.state.fontSize,
onClick: () => this.setFontSize(size)
}))
})
ipcHandler = event => { ipcHandler = event => {
if (event.channel === "request-navigation") { if (event.channel === "request-navigation") {
openExternal(event.args[0]) openExternal(event.args[0])
} }
} }
popUpHandler = event => {
openExternal(event.url)
}
componentDidMount = () => { componentDidMount = () => {
this.webview = document.getElementById("article") this.webview = document.getElementById("article")
this.webview.addEventListener("ipc-message", this.ipcHandler) 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.props.dismiss)
} }
componentWillUnmount = () => { componentWillUnmount = () => {
this.webview.removeEventListener("ipc-message", this.ipcHandler) 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.props.dismiss)
} }
openInBrowser = () => {
openExternal(this.props.item.link)
}
articleView = () => "article/article.html?h=" + window.btoa(encodeURIComponent(renderToString(<> articleView = () => "article/article.html?h=" + window.btoa(encodeURIComponent(renderToString(<>
<p className="title">{this.props.item.title}</p> <p className="title">{this.props.item.title}</p>
<article dangerouslySetInnerHTML={{__html: this.props.item.content}}></article> <article dangerouslySetInnerHTML={{__html: this.props.item.content}}></article>
</>))) </>))) + "&s=" + this.state.fontSize
render = () => ( render = () => (
<div className="article"> <div className="article">
<Stack horizontal style={{height: 36}}>
<span style={{width: 96}}></span>
<Stack className="actions" grow horizontal tokens={{childrenGap: 12}}>
<Stack.Item grow>
{this.props.source.iconurl && <img className="favicon" src={this.props.source.iconurl} />}
<span className="source-name">{this.props.source.name}</span>
</Stack.Item>
<CommandBarButton
title={this.props.item.hasRead ? "标为未读" : "标为已读"}
iconProps={this.props.item.hasRead
? {iconName: "RadioBtnOn", style: {fontSize: 14, textAlign: "center"}}
: {iconName: "StatusCircleRing"}}
onClick={() => this.props.toggleHasRead(this.props.item)} />
<CommandBarButton
iconProps={{iconName: "FavoriteStar"}} />
<CommandBarButton
title="字体大小"
iconProps={{iconName: "FontSize"}}
menuIconProps={{style: {display: "none"}}}
menuProps={this.fontMenuProps()} />
<CommandBarButton
title="在浏览器中打开"
iconProps={{iconName: "NavigateExternalInline", style: {marginTop: -4}}}
onClick={this.openInBrowser} />
</Stack>
<Stack horizontal horizontalAlign="end" style={{width: 112}}>
<CommandBarButton
title="关闭"
iconProps={{iconName: "Cancel"}}
onClick={this.props.dismiss} />
</Stack>
</Stack>
<webview <webview
id="article" id="article"
src={this.articleView()} src={this.articleView()}
preload="article/preload.js" /> preload="article/preload.js"
partition="sandbox" />
</div> </div>
) )
} }

View File

@ -6,12 +6,11 @@ import { FeedIdType } from "../../scripts/models/feed"
export interface CardProps { export interface CardProps {
feedId: FeedIdType feedId: FeedIdType
index: number
item: RSSItem item: RSSItem
source: RSSSource source: RSSSource
markRead: (item: RSSItem) => void markRead: (item: RSSItem) => void
contextMenu: (item: RSSItem, e) => void contextMenu: (item: RSSItem, e) => void
showItem: (fid: FeedIdType, index: number) => void showItem: (fid: FeedIdType, item: RSSItem) => void
} }
export class Card extends React.Component<CardProps> { export class Card extends React.Component<CardProps> {
@ -24,7 +23,7 @@ export class Card extends React.Component<CardProps> {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
this.props.markRead(this.props.item) this.props.markRead(this.props.item)
this.props.showItem(this.props.feedId, this.props.index) this.props.showItem(this.props.feedId, this.props.item)
} }
onMouseUp = (e: React.MouseEvent) => { onMouseUp = (e: React.MouseEvent) => {

View File

@ -32,10 +32,9 @@ class CardsFeed extends Feed {
return this.props.feed.loaded && ( return this.props.feed.loaded && (
<div className="cards-feed-container"> <div className="cards-feed-container">
{ {
this.props.items.map((item, index) => ( this.props.items.map((item) => (
<DefaultCard <DefaultCard
feedId={this.props.feed.id} feedId={this.props.feed.id}
index={index}
key={item.id} key={item.id}
item={item} item={item}
source={this.props.sourceMap[item.source]} source={this.props.sourceMap[item.source]}

View File

@ -10,7 +10,7 @@ type FeedProps = FeedReduxProps & {
markRead: (item: RSSItem) => void markRead: (item: RSSItem) => void
contextMenu: (item: RSSItem, e) => void contextMenu: (item: RSSItem, e) => void
loadMore: (feed: RSSFeed) => void loadMore: (feed: RSSFeed) => void
showItem: (fid: FeedIdType, index: number) => void showItem: (fid: FeedIdType, item: RSSItem) => void
} }
export class Feed extends React.Component<FeedProps> { } export class Feed extends React.Component<FeedProps> { }

View File

@ -6,6 +6,7 @@ import { ProgressIndicator } from "@fluentui/react"
type NavProps = { type NavProps = {
state: AppState, state: AppState,
itemShown: boolean,
fetch: () => void, fetch: () => void,
menu: () => void, menu: () => void,
logs: () => void, logs: () => void,
@ -51,6 +52,7 @@ class Nav extends React.Component<NavProps, NavState> {
canFetch = () => this.props.state.sourceInit && this.props.state.feedInit && !this.props.state.fetchingItems canFetch = () => this.props.state.sourceInit && this.props.state.feedInit && !this.props.state.fetchingItems
fetching = () => !this.canFetch() ? " fetching" : "" fetching = () => !this.canFetch() ? " fetching" : ""
menuOn = () => this.props.state.menu ? " menu-on" : "" menuOn = () => this.props.state.menu ? " menu-on" : ""
itemOn = () => this.props.itemShown ? " item-on" : ""
hideButtons = () => this.props.state.settings.display ? "hide-btns" : "" hideButtons = () => this.props.state.settings.display ? "hide-btns" : ""
fetch = () => { fetch = () => {
@ -65,7 +67,7 @@ class Nav extends React.Component<NavProps, NavState> {
render() { render() {
return ( return (
<nav className={this.hideButtons() + this.menuOn()}> <nav className={this.hideButtons() + this.menuOn() + this.itemOn()}>
<div className="btn-group"> <div className="btn-group">
<a className="btn hide-wide" title="菜单" onClick={this.props.menu}><Icon iconName="GlobalNavButton" /></a> <a className="btn hide-wide" title="菜单" onClick={this.props.menu}><Icon iconName="GlobalNavButton" /></a>
</div> </div>

View File

@ -5,12 +5,13 @@ import { RSSItem } from "../scripts/models/item"
import Article from "./article" import Article from "./article"
import { dismissItem } from "../scripts/models/page" import { dismissItem } from "../scripts/models/page"
import { AnimationClassNames } from "@fluentui/react" import { AnimationClassNames } from "@fluentui/react"
import ArticleContainer from "../containers/article-container"
type PageProps = { type PageProps = {
menuOn: boolean menuOn: boolean
settingsOn: boolean settingsOn: boolean
feeds: FeedIdType[] feeds: FeedIdType[]
item: RSSItem itemId: number
dismissItem: () => void dismissItem: () => void
} }
@ -23,10 +24,10 @@ class Page extends React.Component<PageProps> {
<FeedContainer feedId={fid} key={fid} /> <FeedContainer feedId={fid} key={fid} />
))} ))}
</div>} </div>}
{this.props.item && ( {this.props.itemId >= 0 && (
<div className="article-container" onClick={this.props.dismissItem}> <div className="article-container" onClick={this.props.dismissItem}>
<div className={"article-wrapper " + AnimationClassNames.slideUpIn20} onClick={e => e.stopPropagation()}> <div className={"article-wrapper " + AnimationClassNames.slideUpIn20} onClick={e => e.stopPropagation()}>
<Article item={this.props.item} dismiss={dismissItem} /> <ArticleContainer itemId={this.props.itemId} />
</div> </div>
</div> </div>
)} )}

View File

@ -0,0 +1,34 @@
import { connect } from "react-redux"
import { createSelector } from "reselect"
import { RootState } from "../scripts/reducer"
import { RSSItem, markUnread, markRead } from "../scripts/models/item"
import { AppDispatch } from "../scripts/utils"
import { dismissItem } from "../scripts/models/page"
import Article from "../components/article"
type ArticleContainerProps = {
itemId: number
}
const getItem = (state: RootState, props: ArticleContainerProps) => state.items[props.itemId]
const getSource = (state: RootState, props: ArticleContainerProps) => state.sources[state.items[props.itemId].source]
const makeMapStateToProps = () => {
return createSelector(
[getItem, getSource],
(item, source) => ({
item: item,
source: source
})
)
}
const mapDispatchToProps = (dispatch: AppDispatch) => {
return {
dismiss: () => dispatch(dismissItem()),
toggleHasRead: (item: RSSItem) => dispatch(item.hasRead ? markUnread(item) : markRead(item))
}
}
const ArticleContainer = connect(makeMapStateToProps, mapDispatchToProps)(Article)
export default ArticleContainer

View File

@ -30,7 +30,7 @@ const mapDispatchToProps = dispatch => {
markRead: (item: RSSItem) => dispatch(markRead(item)), markRead: (item: RSSItem) => dispatch(markRead(item)),
contextMenu: (item: RSSItem, e) => dispatch(openItemMenu(item, e)), contextMenu: (item: RSSItem, e) => dispatch(openItemMenu(item, e)),
loadMore: (feed: RSSFeed) => dispatch(loadMore(feed)), loadMore: (feed: RSSFeed) => dispatch(loadMore(feed)),
showItem: (fid: FeedIdType, index: number) => dispatch(showItem(fid, index)) showItem: (fid: FeedIdType, item: RSSItem) => dispatch(showItem(fid, item))
} }
} }

View File

@ -6,10 +6,15 @@ import { toggleMenu, toggleLogMenu, toggleSettings } from "../scripts/models/app
import Nav from "../components/nav" import Nav from "../components/nav"
const getState = (state: RootState) => state.app const getState = (state: RootState) => state.app
const getItemShown = (state: RootState) => state.page.itemId >= 0
const mapStateToProps = createSelector(getState, (state) => ({ const mapStateToProps = createSelector(
state: state [getState, getItemShown],
})) (state, itemShown) => ({
state: state,
itemShown: itemShown
}
))
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
fetch: () => dispatch(fetchItems()), fetch: () => dispatch(fetchItems()),

View File

@ -8,18 +8,14 @@ import { dismissItem } from "../scripts/models/page"
const getPage = (state: RootState) => state.page const getPage = (state: RootState) => state.page
const getSettings = (state: RootState) => state.app.settings.display const getSettings = (state: RootState) => state.app.settings.display
const getMenu = (state: RootState) => state.app.menu const getMenu = (state: RootState) => state.app.menu
const getItems = (state: RootState) => state.items
const getFeeds = (state: RootState) => state.feeds
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
[getPage, getSettings, getMenu, getItems, getFeeds], [getPage, getSettings, getMenu],
(page, settingsOn, menuOn, items, feeds) => ({ (page, settingsOn, menuOn) => ({
feeds: [page.feedId], feeds: [page.feedId],
settingsOn: settingsOn, settingsOn: settingsOn,
menuOn: menuOn, menuOn: menuOn,
item: page.itemIndex >= 0 // && page.itemIndex < feeds[page.feedId].iids.length itemId: page.itemId
? items[feeds[page.feedId].iids[page.itemIndex]]
: null
}) })
) )

View File

@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src *; style-src 'self' 'unsafe-inline'; font-src 'self' https://static2.sharepointonline.com; connect-src *; frame-src *"> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src *; style-src 'self' 'unsafe-inline'; font-src 'self' https://static2.sharepointonline.com; connect-src *">
<title>Hello World!</title> <title>Hello World!</title>
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
</head> </head>

View File

@ -193,7 +193,7 @@ export function appReducer(
...state, ...state,
logMenu: { logMenu: {
...state.logMenu, ...state.logMenu,
notify: true, notify: !state.logMenu.display,
logs: [...state.logMenu.logs, new AppLog( logs: [...state.logMenu.logs, new AppLog(
AppLogType.Failure, AppLogType.Failure,
`无法加载订阅源“${action.errSource.name}`, `无法加载订阅源“${action.errSource.name}`,

View File

@ -199,18 +199,12 @@ export function itemReducer(
} }
default: return state default: return state
} }
case MARK_UNREAD:
case MARK_READ: return { case MARK_READ: return {
...state, ...state,
[action.item.id] : { [action.item.id] : {
...action.item, ...action.item,
hasRead: true hasRead: action.type === MARK_READ
}
}
case MARK_UNREAD: return {
...state,
[action.item.id] : {
...action.item,
hasRead: false
} }
} }
case LOAD_MORE: case LOAD_MORE:

View File

@ -1,5 +1,6 @@
import { ALL, SOURCE, FeedIdType } from "./feed" import { ALL, SOURCE, FeedIdType } from "./feed"
import { getWindowBreakpoint } from "../utils" import { getWindowBreakpoint } from "../utils"
import { RSSItem, ItemActionTypes, MARK_READ, MARK_UNREAD } from "./item"
export const SELECT_PAGE = "SELECT_PAGE" export const SELECT_PAGE = "SELECT_PAGE"
export const SHOW_ITEM = "SHOW_ITEM" export const SHOW_ITEM = "SHOW_ITEM"
@ -22,7 +23,7 @@ interface SelectPageAction {
interface ShowItemAction { interface ShowItemAction {
type: typeof SHOW_ITEM type: typeof SHOW_ITEM
feedId: FeedIdType feedId: FeedIdType
itemIndex: number item: RSSItem
} }
interface DismissItemAction { type: typeof DISMISS_ITEM } interface DismissItemAction { type: typeof DISMISS_ITEM }
@ -50,11 +51,11 @@ export function selectSources(sids: number[], menuKey: string, title: string): P
} }
} }
export function showItem(feedId: FeedIdType, itemIndex: number): PageActionTypes { export function showItem(feedId: FeedIdType, item: RSSItem): PageActionTypes {
return { return {
type: SHOW_ITEM, type: SHOW_ITEM,
feedId: feedId, feedId: feedId,
itemIndex: itemIndex item: item
} }
} }
@ -62,7 +63,7 @@ export const dismissItem = (): PageActionTypes => ({ type: DISMISS_ITEM })
export class PageState { export class PageState {
feedId = ALL as FeedIdType feedId = ALL as FeedIdType
itemIndex = -1 itemId = -1
} }
export function pageReducer( export function pageReducer(
@ -84,11 +85,11 @@ export function pageReducer(
} }
case SHOW_ITEM: return { case SHOW_ITEM: return {
...state, ...state,
itemIndex: action.itemIndex itemId: action.item.id
} }
case DISMISS_ITEM: return { case DISMISS_ITEM: return {
...state, ...state,
itemIndex: -1 itemId: -1
} }
default: return state default: return state
} }