read content in webview

This commit is contained in:
刘浩远 2020-06-06 19:48:08 +08:00
parent 143a295024
commit 288907fdae
19 changed files with 250 additions and 56 deletions

36
dist/article/article.css vendored Normal file
View File

@ -0,0 +1,36 @@
html, body {
font-family: "Segoe UI Regular", "Source Han Sans SC Regular", "Microsoft YaHei", sans-serif;
margin: 16px 48px;
overflow-x: hidden;
}
a {
color: #0078d4;
text-decoration: none;
}
a:hover, a:active {
color: #004578;
text-decoration: underline;
}
#main > p.title {
font-size: 1.25rem;
line-height: 1.75rem;
font-weight: 600;
}
article {
line-height: 1.6;
}
article img {
max-width: 100%;
height: auto;
}
article figure {
margin: 16px 0;
text-align: center;
}
article figure figcaption {
font-size: .875rem;
color: #484644;
}

14
dist/article/article.html vendored Normal file
View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; img-src *; style-src 'self' 'unsafe-inline'; frame-src *">
<title>Hello World!</title>
<link rel="stylesheet" href="article.css" />
</head>
<body>
<div id="main"></div>
<script src="article.js"></script>
</body>
</html>

10
dist/article/article.js vendored Normal file
View File

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

1
dist/article/preload.js vendored Normal file
View File

@ -0,0 +1 @@
global.ipcRenderer = require("electron").ipcRenderer

37
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 {
display: inline-block;
}
.btn-group .btn.system.menu-on {
nav.menu-on .btn-group .btn.system {
color: #fff;
}
.btn-group .btn:hover {
@ -139,17 +139,17 @@ nav.menu-on .btn-group .btn.system, nav.hide-btns .btn-group .btn.system {
}
.btn-group .btn.close:hover {
background-color: #e81123;
color: #fff;
color: #fff !important;
}
.btn-group .btn.close:active {
background-color: #f1707a;
color: #fff;
color: #fff !important;
}
.btn-group .btn.inline-block-wide {
display: none;
}
.menu-container {
.menu-container, .article-container {
position: fixed;
z-index: 5;
left: 0;
@ -159,6 +159,10 @@ nav.menu-on .btn-group .btn.system, nav.hide-btns .btn-group .btn.system {
background-color: #0008;
backdrop-filter: saturate(150%) blur(20px);
}
.article-container {
z-index: 6;
background-color: #fff6;
}
.menu-container .menu {
position: absolute;
left: 0;
@ -266,7 +270,7 @@ img.favicon {
nav.menu-on .btn-group .btn {
display: inline-block;
}
.btn-group .btn.system.menu-on {
nav.menu-on .btn-group .btn.system {
color: #000;
}
.menu-container {
@ -298,16 +302,29 @@ img.favicon {
}
}
.article-wrapper {
margin: 32px auto 0;
width: 860px;
height: calc(100% - 50px);
background-color: #fff;
box-shadow: 0 6.4px 14.4px 0 rgba(0,0,0,.132), 0 1.2px 3.6px 0 rgba(0,0,0,.108);
border-radius: 5px;
overflow: hidden;
}
.article webview {
width: 100%;
height: calc(100vh - 50px);
border: none;
}
.cards-feed-container {
display: inline-flex;
flex-wrap: wrap;
justify-content: center;
justify-content: space-around;
padding: 12px;
}
.cards-feed-container > div {
flex-grow: 1;
display: inline-flex;
justify-content: center;
.cards-feed-container > div.load-more-wrapper, .flex-fix {
text-align: center;
}
.cards-feed-container > div.load-more-wrapper {
width: 100%;

View File

@ -0,0 +1,50 @@
import * as React from "react"
import { renderToString } from "react-dom/server"
import { RSSItem } from "../scripts/models/item"
import { openExternal } from "../scripts/utils"
type ArticleProps = {
item: RSSItem
dismiss: () => void
}
class Article extends React.Component<ArticleProps> {
webview: HTMLWebViewElement
constructor(props) {
super(props)
}
ipcHandler = event => {
if (event.channel === "request-navigation") {
openExternal(event.args[0])
}
}
componentDidMount = () => {
this.webview = document.getElementById("article")
this.webview.addEventListener("ipc-message", this.ipcHandler)
this.webview.addEventListener("will-navigate", this.props.dismiss)
}
componentWillUnmount = () => {
this.webview.removeEventListener("ipc-message", this.ipcHandler)
this.webview.removeEventListener("will-navigate", this.props.dismiss)
}
articleView = () => "article/article.html?h=" + window.btoa(encodeURIComponent(renderToString(<>
<p className="title">{this.props.item.title}</p>
<article dangerouslySetInnerHTML={{__html: this.props.item.content}}></article>
</>)))
render = () => (
<div className="article">
<webview
id="article"
src={this.articleView()}
preload="article/preload.js" />
</div>
)
}
export default Article

View File

@ -2,12 +2,16 @@ import * as React from "react"
import { openExternal } from "../../scripts/utils"
import { RSSSource } from "../../scripts/models/source"
import { RSSItem } from "../../scripts/models/item"
import { FeedIdType } from "../../scripts/models/feed"
export interface CardProps {
feedId: FeedIdType
index: number
item: RSSItem
source: RSSSource
markRead: Function
contextMenu: Function
markRead: (item: RSSItem) => void
contextMenu: (item: RSSItem, e) => void
showItem: (fid: FeedIdType, index: number) => void
}
export class Card extends React.Component<CardProps> {
@ -19,7 +23,8 @@ export class Card extends React.Component<CardProps> {
onClick = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
this.openInBrowser()
this.props.markRead(this.props.item)
this.props.showItem(this.props.feedId, this.props.index)
}
onMouseUp = (e: React.MouseEvent) => {

View File

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

View File

@ -1,15 +1,16 @@
import * as React from "react"
import { RSSItem } from "../../scripts/models/item"
import { FeedReduxProps } from "../../containers/feed-container"
import { RSSFeed } from "../../scripts/models/feed"
import { RSSFeed, FeedIdType } from "../../scripts/models/feed"
type FeedProps = FeedReduxProps & {
feed: RSSFeed
items: RSSItem[]
sourceMap: Object
markRead: Function
contextMenu: Function
loadMore: Function
markRead: (item: RSSItem) => void
contextMenu: (item: RSSItem, e) => void
loadMore: (feed: RSSFeed) => void
showItem: (fid: FeedIdType, index: number) => void
}
export class Feed extends React.Component<FeedProps> { }

View File

@ -8,6 +8,7 @@ import { AnimationClassNames } from "@fluentui/react"
export type MenuProps = {
status: boolean,
display: boolean,
selected: string,
sources: SourceState,
groups: SourceGroup[],
@ -73,8 +74,8 @@ export class Menu extends React.Component<MenuProps> {
})
render() {
return this.props.status ? (
<div className="menu-container" onClick={this.props.toggleMenu}>
return this.props.status && (
<div className="menu-container" onClick={this.props.toggleMenu} style={{display: this.props.display ? "block" : "none"}}>
<div className="menu" onClick={(e) => e.stopPropagation()}>
<div className="btn-group">
<a className="btn hide-wide" title="关闭菜单" onClick={this.props.toggleMenu}><Icon iconName="Back" /></a>
@ -91,6 +92,6 @@ export class Menu extends React.Component<MenuProps> {
</div>
</div>
</div>
) : null
)
}
}

View File

@ -79,11 +79,11 @@ class Nav extends React.Component<NavProps, NavState> {
<a className="btn" title="视图"><Icon iconName="View" /></a>
<a className="btn" title="选项" onClick={this.props.settings}><Icon iconName="Settings" /></a>
<span className="seperator"></span>
<a className={"btn system"+this.menuOn()} title="最小化" onClick={this.minimize} style={{fontSize: 12}}><Icon iconName="Remove" /></a>
<a className={"btn system"+this.menuOn()} title="最大化" onClick={this.maximize}>
<a className="btn system" title="最小化" onClick={this.minimize} style={{fontSize: 12}}><Icon iconName="Remove" /></a>
<a className="btn system" title="最大化" onClick={this.maximize}>
{this.state.maximized ? <Icon iconName="ChromeRestore" style={{fontSize: 11}} /> :<Icon iconName="Checkbox" style={{fontSize: 10}} />}
</a>
<a className={"btn system close"+this.menuOn()} title="关闭" onClick={this.close}><Icon iconName="Cancel" /></a>
<a className="btn system close" title="关闭" onClick={this.close}><Icon iconName="Cancel" /></a>
</div>
{!this.canFetch() &&
<ProgressIndicator

View File

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

View File

@ -2,9 +2,10 @@ import { connect } from "react-redux"
import { createSelector } from "reselect"
import { RootState } from "../scripts/reducer"
import CardsFeed from "../components/feeds/cards-feed"
import { markRead } from "../scripts/models/item"
import { markRead, RSSItem } from "../scripts/models/item"
import { openItemMenu } from "../scripts/models/app"
import { FeedIdType, loadMore } from "../scripts/models/feed"
import { FeedIdType, loadMore, RSSFeed } from "../scripts/models/feed"
import { showItem } from "../scripts/models/page"
interface FeedContainerProps {
feedId: FeedIdType
@ -26,9 +27,10 @@ const makeMapStateToProps = () => {
}
const mapDispatchToProps = dispatch => {
return {
markRead: item => dispatch(markRead(item)),
contextMenu: (item, e) => dispatch(openItemMenu(item, e)),
loadMore: feed => dispatch(loadMore(feed))
markRead: (item: RSSItem) => dispatch(markRead(item)),
contextMenu: (item: RSSItem, e) => dispatch(openItemMenu(item, e)),
loadMore: (feed: RSSFeed) => dispatch(loadMore(feed)),
showItem: (fid: FeedIdType, index: number) => dispatch(showItem(fid, index))
}
}

View File

@ -8,16 +8,16 @@ import { selectAllArticles, selectSources } from "../scripts/models/page"
import { initFeeds } from "../scripts/models/feed"
import { RSSSource } from "../scripts/models/source"
const getStatus = (state: RootState) => state.app.menu && state.app.sourceInit
const getKey = (state: RootState) => state.app.menuKey
const getApp = (state: RootState) => state.app
const getSources = (state: RootState) => state.sources
const getGroups = (state: RootState) => state.groups
const mapStateToProps = createSelector(
[getStatus, getKey, getSources, getGroups],
(status, key, sources, groups) => ({
status: status,
selected: key,
[getApp, getSources, getGroups],
(app, sources, groups) => ({
status: app.sourceInit,
display: app.menu,
selected: app.menuKey,
sources: sources,
groups: groups
})

View File

@ -2,19 +2,30 @@ import { connect } from "react-redux"
import { createSelector } from "reselect"
import { RootState } from "../scripts/reducer"
import Page from "../components/page"
import { AppDispatch } from "../scripts/utils"
import { dismissItem } from "../scripts/models/page"
const getFeeds = (state: RootState) => state.page.feedId
const getPage = (state: RootState) => state.page
const getSettings = (state: RootState) => state.app.settings.display
const getMenu = (state: RootState) => state.app.menu
const getItems = (state: RootState) => state.items
const getFeeds = (state: RootState) => state.feeds
const mapStateToProps = createSelector(
[getFeeds, getSettings, getMenu],
(feeds, settingsOn, menuOn) => ({
feeds: [feeds],
[getPage, getSettings, getMenu, getItems, getFeeds],
(page, settingsOn, menuOn, items, feeds) => ({
feeds: [page.feedId],
settingsOn: settingsOn,
menuOn: menuOn
menuOn: menuOn,
item: page.itemIndex >= 0 // && page.itemIndex < feeds[page.feedId].iids.length
? items[feeds[page.feedId].iids[page.itemIndex]]
: null
})
)
const PageContainer = connect(mapStateToProps)(Page)
const mapDispatchToProps = (dispatch: AppDispatch) => ({
dismissItem: () => dispatch(dismissItem())
})
const PageContainer = connect(mapStateToProps, mapDispatchToProps)(Page)
export default PageContainer

View File

@ -5,6 +5,7 @@ import { RootState } from "../../scripts/reducer"
import SourcesTab from "../../components/settings/sources"
import { addSource, RSSSource, updateSource, deleteSource } from "../../scripts/models/source"
import { importOPML } from "../../scripts/models/group"
import { AppDispatch } from "../../scripts/utils"
const getSources = (state: RootState) => state.sources
@ -15,7 +16,7 @@ const mapStateToProps = createSelector(
})
)
const mapDispatchToProps = dispatch => {
const mapDispatchToProps = (dispatch: AppDispatch) => {
return {
addSource: (url: string) => dispatch(addSource(url)),
updateSourceName: (source: RSSSource, name: string) => {
@ -30,7 +31,7 @@ const mapDispatchToProps = dispatch => {
properties: ["openFile"]
}
)
if (path.length > 0) dispatch(importOPML(path[0]))
if (path && path.length > 0) dispatch(importOPML(path[0]))
}
}
}

View File

@ -19,7 +19,8 @@ function createWindow() {
minHeight: 600,
frame: false,
webPreferences: {
nodeIntegration: true
nodeIntegration: true,
webviewTag: true
}
})
mainWindowState.manage(mainWindow)

View File

@ -2,7 +2,7 @@
<html>
<head>
<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 *">
<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 *">
<title>Hello World!</title>
<link rel="stylesheet" href="styles.css">
</head>

View File

@ -2,6 +2,8 @@ import { ALL, SOURCE, FeedIdType } from "./feed"
import { getWindowBreakpoint } from "../utils"
export const SELECT_PAGE = "SELECT_PAGE"
export const SHOW_ITEM = "SHOW_ITEM"
export const DISMISS_ITEM = "DISMISS_ITEM"
export enum PageType {
AllArticles, Sources, Page
@ -17,9 +19,17 @@ interface SelectPageAction {
title?: string
}
export type PageActionTypes = SelectPageAction
interface ShowItemAction {
type: typeof SHOW_ITEM
feedId: FeedIdType
itemIndex: number
}
export function selectAllArticles(init = false): SelectPageAction {
interface DismissItemAction { type: typeof DISMISS_ITEM }
export type PageActionTypes = SelectPageAction | ShowItemAction | DismissItemAction
export function selectAllArticles(init = false): PageActionTypes {
return {
type: SELECT_PAGE,
keepMenu: getWindowBreakpoint(),
@ -28,7 +38,7 @@ export function selectAllArticles(init = false): SelectPageAction {
}
}
export function selectSources(sids: number[], menuKey: string, title: string) {
export function selectSources(sids: number[], menuKey: string, title: string): PageActionTypes {
return {
type: SELECT_PAGE,
pageType: PageType.Sources,
@ -40,8 +50,19 @@ export function selectSources(sids: number[], menuKey: string, title: string) {
}
}
export function showItem(feedId: FeedIdType, itemIndex: number): PageActionTypes {
return {
type: SHOW_ITEM,
feedId: feedId,
itemIndex: itemIndex
}
}
export const dismissItem = (): PageActionTypes => ({ type: DISMISS_ITEM })
export class PageState {
feedId = ALL
feedId = ALL as FeedIdType
itemIndex = -1
}
export function pageReducer(
@ -61,6 +82,14 @@ export function pageReducer(
}
default: return state
}
case SHOW_ITEM: return {
...state,
itemIndex: action.itemIndex
}
case DISMISS_ITEM: return {
...state,
itemIndex: -1
}
default: return state
}
}