use _id & csp update

This commit is contained in:
刘浩远 2020-06-10 11:33:25 +08:00
parent 0128728889
commit 2ff5e13219
22 changed files with 207 additions and 102 deletions

View File

@ -3,12 +3,12 @@
<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 *; media-src *"> content="default-src 'none'; script-src-elem 'sha256-34JRfFnY5YiFDB1MABAIxq6OfvlgM/Ba2el4MdA0WoM='; img-src http://* https://*; style-src 'self' 'unsafe-inline'; frame-src http://* https://*; media-src http://* https://*">
<title>Hello World!</title> <title>Article</title>
<link rel="stylesheet" href="article.css" /> <link rel="stylesheet" href="article.css" />
</head> </head>
<body> <body>
<div id="main"></div> <div id="main"></div>
<script src="article.js"></script> <script integrity="sha256-34JRfFnY5YiFDB1MABAIxq6OfvlgM/Ba2el4MdA0WoM=" src="article.js"></script>
</body> </body>
</html> </html>

View File

@ -3,8 +3,20 @@ function get(name) {
return decodeURIComponent(name[1]); return decodeURIComponent(name[1]);
} }
document.documentElement.style.fontSize = get("s") + "px" document.documentElement.style.fontSize = get("s") + "px"
let html = decodeURIComponent(window.atob(get("h")))
let domParser = new DOMParser()
let dom = domParser.parseFromString(html, "text/html")
let baseEl = dom.createElement('base')
baseEl.setAttribute('href', get("u").split("/").slice(0, 3).join("/"))
dom.head.append(baseEl)
for (let i of dom.querySelectorAll("img")) {
i.src = i.src
}
for (let s of dom.querySelectorAll("script")) {
s.parentNode.removeChild(s)
}
let main = document.getElementById("main") let main = document.getElementById("main")
main.innerHTML = decodeURIComponent(window.atob(get("h"))) main.innerHTML = dom.body.innerHTML
document.addEventListener("click", event => { document.addEventListener("click", event => {
event.preventDefault() event.preventDefault()
if (event.target.href) post("request-navigation", event.target.href) if (event.target.href) post("request-navigation", event.target.href)

1
dist/styles.css vendored
View File

@ -510,6 +510,7 @@ img.favicon {
transition: box-shadow linear .08s; transition: box-shadow linear .08s;
transform: scale(1); transform: scale(1);
cursor: pointer; cursor: pointer;
animation-fill-mode: none;
} }
.card:hover { .card:hover {
box-shadow: #0006 0px 5px 40px; box-shadow: #0006 0px 5px 40px;

View File

@ -13,6 +13,8 @@ type ArticleProps = {
source: RSSSource source: RSSSource
dismiss: () => void dismiss: () => void
toggleHasRead: (item: RSSItem) => void toggleHasRead: (item: RSSItem) => void
toggleStarred: (item: RSSItem) => void
toggleHidden: (item: RSSItem) => void
textMenu: (text: string, position: [number, number]) => void textMenu: (text: string, position: [number, number]) => void
} }
@ -51,6 +53,22 @@ class Article extends React.Component<ArticleProps, ArticleState> {
})) }))
}) })
moreMenuProps = (): IContextualMenuProps => ({
items: [
{
key: "openInBrowser",
text: "在浏览器中打开",
iconProps: {iconName: "NavigateExternalInline"},
onClick: this.openInBrowser
},
{
key: "toggleHidden",
text: this.props.item.hidden ? "取消隐藏" : "隐藏文章",
onClick: () => { this.props.toggleHidden(this.props.item) }
}
]
})
ipcHandler = event => { ipcHandler = event => {
switch (event.channel) { switch (event.channel) {
case "request-navigation": { case "request-navigation": {
@ -84,7 +102,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
} }
} }
componentDidUpdate = (prevProps: ArticleProps) => { componentDidUpdate = (prevProps: ArticleProps) => {
if (prevProps.item.id != this.props.item.id) { if (prevProps.item._id != this.props.item._id) {
this.setState({loadWebpage: this.props.source.openTarget === SourceOpenTarget.Webpage}) this.setState({loadWebpage: this.props.source.openTarget === SourceOpenTarget.Webpage})
} }
this.componentDidMount() this.componentDidMount()
@ -112,7 +130,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
<p className="title">{this.props.item.title}</p> <p className="title">{this.props.item.title}</p>
<p className="date">{this.props.item.date.toLocaleString("zh-cn", {hour12: false})}</p> <p className="date">{this.props.item.date.toLocaleString("zh-cn", {hour12: false})}</p>
<article dangerouslySetInnerHTML={{__html: this.props.item.content}}></article> <article dangerouslySetInnerHTML={{__html: this.props.item.content}}></article>
</>))) + "&s=" + this.state.fontSize </>))) + `&s=${this.state.fontSize}&u=${this.props.item.link}`
render = () => ( render = () => (
<div className="article"> <div className="article">
@ -128,11 +146,13 @@ class Article extends React.Component<ArticleProps, ArticleState> {
<CommandBarButton <CommandBarButton
title={this.props.item.hasRead ? "标为未读" : "标为已读"} title={this.props.item.hasRead ? "标为未读" : "标为已读"}
iconProps={this.props.item.hasRead iconProps={this.props.item.hasRead
? {iconName: "RadioBtnOn", style: {fontSize: 14, textAlign: "center"}} ? {iconName: "StatusCircleRing"}
: {iconName: "StatusCircleRing"}} : {iconName: "RadioBtnOn", style: {fontSize: 14, textAlign: "center"}}}
onClick={() => this.props.toggleHasRead(this.props.item)} /> onClick={() => this.props.toggleHasRead(this.props.item)} />
<CommandBarButton <CommandBarButton
iconProps={{iconName: "FavoriteStar"}} /> title={this.props.item.starred ? "取消星标" : "标为星标"}
iconProps={{iconName: this.props.item.starred ? "FavoriteStarFill" : "FavoriteStar"}}
onClick={() => this.props.toggleStarred(this.props.item)} />
<CommandBarButton <CommandBarButton
title="字体大小" title="字体大小"
disabled={this.state.loadWebpage} disabled={this.state.loadWebpage}
@ -145,9 +165,10 @@ class Article extends React.Component<ArticleProps, ArticleState> {
iconProps={{iconName: "Globe"}} iconProps={{iconName: "Globe"}}
onClick={this.toggleWebpage} /> onClick={this.toggleWebpage} />
<CommandBarButton <CommandBarButton
title="在浏览器中打开" title="更多"
iconProps={{iconName: "NavigateExternalInline", style: {marginTop: -4}}} iconProps={{iconName: "More"}}
onClick={this.openInBrowser} /> menuIconProps={{style: {display: "none"}}}
menuProps={this.moreMenuProps()} />
</Stack> </Stack>
<Stack horizontal horizontalAlign="end" style={{width: 112}}> <Stack horizontal horizontalAlign="end" style={{width: 112}}>
<CommandBarButton <CommandBarButton
@ -158,7 +179,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
</Stack> </Stack>
<webview <webview
id="article" id="article"
key={this.props.item.id + (this.state.loadWebpage ? "_" : "")} key={this.props.item._id + (this.state.loadWebpage ? "_" : "")}
src={this.state.loadWebpage ? this.props.item.link : this.articleView()} src={this.state.loadWebpage ? this.props.item.link : this.articleView()}
preload={this.state.loadWebpage ? null : "article/preload.js"} preload={this.state.loadWebpage ? null : "article/preload.js"}
partition="sandbox" /> partition="sandbox" />

View File

@ -2,15 +2,14 @@ import * as React from "react"
import { openExternal } from "../../scripts/utils" import { openExternal } from "../../scripts/utils"
import { RSSSource, SourceOpenTarget } from "../../scripts/models/source" import { RSSSource, SourceOpenTarget } from "../../scripts/models/source"
import { RSSItem } from "../../scripts/models/item" import { RSSItem } from "../../scripts/models/item"
import { FeedIdType } from "../../scripts/models/feed"
export interface CardProps { export interface CardProps {
feedId: FeedIdType feedId: string
item: RSSItem item: RSSItem
source: RSSSource source: RSSSource
markRead: (item: RSSItem) => void markRead: (item: RSSItem) => void
contextMenu: (feedId: FeedIdType, item: RSSItem, e) => void contextMenu: (feedId: string, item: RSSItem, e) => void
showItem: (fid: FeedIdType, item: RSSItem) => void showItem: (fid: string, item: RSSItem) => void
} }
export class Card extends React.Component<CardProps> { export class Card extends React.Component<CardProps> {

View File

@ -5,7 +5,6 @@ import { ContextualMenu, IContextualMenuItem, ContextualMenuItemType, Directiona
import { ContextMenuType } from "../scripts/models/app" import { ContextMenuType } from "../scripts/models/app"
import { RSSItem } from "../scripts/models/item" import { RSSItem } from "../scripts/models/item"
import { ContextReduxProps } from "../containers/context-menu-container" import { ContextReduxProps } from "../containers/context-menu-container"
import { FeedIdType } from "../scripts/models/feed"
import { ViewType } from "../scripts/models/page" import { ViewType } from "../scripts/models/page"
export type ContextMenuProps = ContextReduxProps & { export type ContextMenuProps = ContextReduxProps & {
@ -13,12 +12,14 @@ export type ContextMenuProps = ContextReduxProps & {
event?: MouseEvent | string event?: MouseEvent | string
position?: [number, number] position?: [number, number]
item?: RSSItem item?: RSSItem
feedId?: FeedIdType feedId?: string
text?: string text?: string
viewType: ViewType viewType: ViewType
showItem: (feedId: FeedIdType, item: RSSItem) => void showItem: (feedId: string, item: RSSItem) => void
markRead: (item: RSSItem) => void markRead: (item: RSSItem) => void
markUnread: (item: RSSItem) => void markUnread: (item: RSSItem) => void
toggleStarred: (item: RSSItem) => void
toggleHidden: (item: RSSItem) => void
switchView: (viewType: ViewType) => void switchView: (viewType: ViewType) => void
close: () => void close: () => void
} }
@ -59,8 +60,15 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
onClick: () => { this.props.markRead(this.props.item) } onClick: () => { this.props.markRead(this.props.item) }
}, },
{ {
key: "markBelowAsRead", key: "toggleStarred",
text: "将以下标为已读" text: this.props.item.starred ? "取消星标" : "标为星标",
iconProps: { iconName: this.props.item.starred ? "FavoriteStar" : "FavoriteStarFill" },
onClick: () => { this.props.toggleStarred(this.props.item) }
},
{
key: "toggleHidden",
text: this.props.item.hidden ? "取消隐藏" : "隐藏文章",
onClick: () => { this.props.toggleHidden(this.props.item) }
}, },
{ {
key: "divider_1", key: "divider_1",

View File

@ -34,8 +34,8 @@ class CardsFeed extends React.Component<FeedProps> {
{ {
this.props.items.map((item) => ( this.props.items.map((item) => (
<DefaultCard <DefaultCard
feedId={this.props.feed.id} feedId={this.props.feed._id}
key={item.id} key={item._id}
item={item} item={item}
source={this.props.sourceMap[item.source]} source={this.props.sourceMap[item.source]}
markRead={this.props.markRead} markRead={this.props.markRead}

View File

@ -1,7 +1,7 @@
import * as React from "react" import * as React from "react"
import { RSSItem } from "../../scripts/models/item" import { RSSItem } from "../../scripts/models/item"
import { FeedReduxProps } from "../../containers/feed-container" import { FeedReduxProps } from "../../containers/feed-container"
import { RSSFeed, FeedIdType } from "../../scripts/models/feed" import { RSSFeed } from "../../scripts/models/feed"
import { ViewType } from "../../scripts/models/page" import { ViewType } from "../../scripts/models/page"
import CardsFeed from "./cards-feed" import CardsFeed from "./cards-feed"
import ListFeed from "./list-feed" import ListFeed from "./list-feed"
@ -12,9 +12,9 @@ export type FeedProps = FeedReduxProps & {
items: RSSItem[] items: RSSItem[]
sourceMap: Object sourceMap: Object
markRead: (item: RSSItem) => void markRead: (item: RSSItem) => void
contextMenu: (feedId: FeedIdType, item: RSSItem, e) => void contextMenu: (feedId: string, item: RSSItem, e) => void
loadMore: (feed: RSSFeed) => void loadMore: (feed: RSSFeed) => void
showItem: (fid: FeedIdType, item: RSSItem) => void showItem: (fid: string, item: RSSItem) => void
} }
export class Feed extends React.Component<FeedProps> { export class Feed extends React.Component<FeedProps> {

View File

@ -10,8 +10,8 @@ class ListFeed extends React.Component<FeedProps> {
{ {
this.props.items.map((item) => ( this.props.items.map((item) => (
<ListCard <ListCard
feedId={this.props.feed.id} feedId={this.props.feed._id}
key={item.id} key={item._id}
item={item} item={item}
source={this.props.sourceMap[item.source]} source={this.props.sourceMap[item.source]}
markRead={this.props.markRead} markRead={this.props.markRead}

View File

@ -1,5 +1,4 @@
import * as React from "react" import * as React from "react"
import { FeedIdType } from "../scripts/models/feed"
import { FeedContainer } from "../containers/feed-container" import { FeedContainer } from "../containers/feed-container"
import { AnimationClassNames, Icon } from "@fluentui/react" import { AnimationClassNames, Icon } from "@fluentui/react"
import ArticleContainer from "../containers/article-container" import ArticleContainer from "../containers/article-container"
@ -8,8 +7,8 @@ import { ViewType } from "../scripts/models/page"
type PageProps = { type PageProps = {
menuOn: boolean menuOn: boolean
settingsOn: boolean settingsOn: boolean
feeds: FeedIdType[] feeds: string[]
itemId: number itemId: string
viewType: ViewType viewType: ViewType
dismissItem: () => void dismissItem: () => void
offsetItem: (offset: number) => void offsetItem: (offset: number) => void
@ -34,7 +33,7 @@ class Page extends React.Component<PageProps> {
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} /> <FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} />
))} ))}
</div>} </div>}
{this.props.itemId >= 0 && ( {this.props.itemId && (
<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()}>
<ArticleContainer itemId={this.props.itemId} /> <ArticleContainer itemId={this.props.itemId} />
@ -54,7 +53,7 @@ class Page extends React.Component<PageProps> {
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} /> <FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} />
))} ))}
</div> </div>
{this.props.itemId >= 0 && ( {this.props.itemId && (
<div className="side-article-wrapper"> <div className="side-article-wrapper">
<ArticleContainer itemId={this.props.itemId} /> <ArticleContainer itemId={this.props.itemId} />
</div> </div>

View File

@ -1,14 +1,14 @@
import { connect } from "react-redux" import { connect } from "react-redux"
import { createSelector } from "reselect" import { createSelector } from "reselect"
import { RootState } from "../scripts/reducer" import { RootState } from "../scripts/reducer"
import { RSSItem, markUnread, markRead } from "../scripts/models/item" import { RSSItem, markUnread, markRead, toggleStarred, toggleHidden } from "../scripts/models/item"
import { AppDispatch } from "../scripts/utils" import { AppDispatch } from "../scripts/utils"
import { dismissItem } from "../scripts/models/page" import { dismissItem } from "../scripts/models/page"
import Article from "../components/article" import Article from "../components/article"
import { openTextMenu } from "../scripts/models/app" import { openTextMenu } from "../scripts/models/app"
type ArticleContainerProps = { type ArticleContainerProps = {
itemId: number itemId: string
} }
const getItem = (state: RootState, props: ArticleContainerProps) => state.items[props.itemId] const getItem = (state: RootState, props: ArticleContainerProps) => state.items[props.itemId]
@ -28,6 +28,8 @@ const mapDispatchToProps = (dispatch: AppDispatch) => {
return { return {
dismiss: () => dispatch(dismissItem()), dismiss: () => dispatch(dismissItem()),
toggleHasRead: (item: RSSItem) => dispatch(item.hasRead ? markUnread(item) : markRead(item)), toggleHasRead: (item: RSSItem) => dispatch(item.hasRead ? markUnread(item) : markRead(item)),
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)),
toggleHidden: (item: RSSItem) => dispatch(toggleHidden(item)),
textMenu: (text: string, position: [number, number]) => dispatch(openTextMenu(text, position)) textMenu: (text: string, position: [number, number]) => dispatch(openTextMenu(text, position))
} }
} }

View File

@ -3,9 +3,8 @@ import { createSelector } from "reselect"
import { RootState } from "../scripts/reducer" import { RootState } from "../scripts/reducer"
import { ContextMenuType, closeContextMenu } from "../scripts/models/app" import { ContextMenuType, closeContextMenu } from "../scripts/models/app"
import { ContextMenu } from "../components/context-menu" import { ContextMenu } from "../components/context-menu"
import { RSSItem, markRead, markUnread } from "../scripts/models/item" import { RSSItem, markRead, markUnread, toggleStarred, toggleHidden } from "../scripts/models/item"
import { showItem, switchView, ViewType } from "../scripts/models/page" import { showItem, switchView, ViewType } from "../scripts/models/page"
import { FeedIdType } from "../scripts/models/feed"
import { setDefaultView } from "../scripts/utils" import { setDefaultView } from "../scripts/utils"
const getContext = (state: RootState) => state.app.contextMenu const getContext = (state: RootState) => state.app.contextMenu
@ -38,9 +37,11 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
showItem: (feedId: FeedIdType, item: RSSItem) => dispatch(showItem(feedId, item)), showItem: (feedId: string, item: RSSItem) => dispatch(showItem(feedId, item)),
markRead: (item: RSSItem) => dispatch(markRead(item)), markRead: (item: RSSItem) => dispatch(markRead(item)),
markUnread: (item: RSSItem) => dispatch(markUnread(item)), markUnread: (item: RSSItem) => dispatch(markUnread(item)),
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)),
toggleHidden: (item: RSSItem) => dispatch(toggleHidden(item)),
switchView: (viewType: ViewType) => { switchView: (viewType: ViewType) => {
setDefaultView(viewType) setDefaultView(viewType)
dispatch(switchView(viewType)) dispatch(switchView(viewType))

View File

@ -3,12 +3,12 @@ import { createSelector } from "reselect"
import { RootState } from "../scripts/reducer" import { RootState } from "../scripts/reducer"
import { markRead, RSSItem } from "../scripts/models/item" import { markRead, RSSItem } from "../scripts/models/item"
import { openItemMenu } from "../scripts/models/app" import { openItemMenu } from "../scripts/models/app"
import { FeedIdType, loadMore, RSSFeed } from "../scripts/models/feed" import { loadMore, RSSFeed } from "../scripts/models/feed"
import { showItem, ViewType } from "../scripts/models/page" import { showItem, ViewType } from "../scripts/models/page"
import { Feed } from "../components/feeds/feed" import { Feed } from "../components/feeds/feed"
interface FeedContainerProps { interface FeedContainerProps {
feedId: FeedIdType feedId: string
viewType: ViewType viewType: ViewType
} }
@ -31,9 +31,9 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
markRead: (item: RSSItem) => dispatch(markRead(item)), markRead: (item: RSSItem) => dispatch(markRead(item)),
contextMenu: (feedId: FeedIdType, item: RSSItem, e) => dispatch(openItemMenu(item, feedId, e)), contextMenu: (feedId: string, item: RSSItem, e) => dispatch(openItemMenu(item, feedId, e)),
loadMore: (feed: RSSFeed) => dispatch(loadMore(feed)), loadMore: (feed: RSSFeed) => dispatch(loadMore(feed)),
showItem: (fid: FeedIdType, item: RSSItem) => dispatch(showItem(fid, item)) showItem: (fid: string, item: RSSItem) => dispatch(showItem(fid, item))
} }
} }

View File

@ -7,7 +7,7 @@ import { ViewType } from "../scripts/models/page"
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) && state.page.viewType !== ViewType.List const getItemShown = (state: RootState) => state.page.itemId && state.page.viewType !== ViewType.List
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
[getState, getItemShown], [getState, getItemShown],

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 *"> <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; img-src *; style-src 'self' 'unsafe-inline'; font-src 'self' https://static2.sharepointonline.com; connect-src https://* http://*">
<title>Fluent Reader</title> <title>Fluent Reader</title>
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
</head> </head>

View File

@ -20,5 +20,6 @@ export const idb = new Datastore<RSSItem>({
if (err) window.console.log(err) if (err) window.console.log(err)
} }
}) })
idb.ensureIndex({ fieldName: "id", unique: true }) idb.removeIndex("id")
idb.update({}, {$unset: {id: true}}, {multi: true})
//idb.remove({}, { multi: true }) //idb.remove({}, { multi: true })

View File

@ -1,7 +1,7 @@
import { RSSSource, INIT_SOURCES, SourceActionTypes, ADD_SOURCE, UPDATE_SOURCE, DELETE_SOURCE } from "./source" import { RSSSource, INIT_SOURCES, SourceActionTypes, ADD_SOURCE, UPDATE_SOURCE, DELETE_SOURCE } from "./source"
import { RSSItem, ItemActionTypes, FETCH_ITEMS } from "./item" import { RSSItem, ItemActionTypes, FETCH_ITEMS } from "./item"
import { ActionStatus, AppThunk, getWindowBreakpoint } from "../utils" import { ActionStatus, AppThunk, getWindowBreakpoint } from "../utils"
import { INIT_FEEDS, FeedActionTypes, ALL, initFeeds, FeedIdType } from "./feed" import { INIT_FEEDS, FeedActionTypes, ALL, initFeeds } from "./feed"
import { SourceGroupActionTypes, UPDATE_SOURCE_GROUP, ADD_SOURCE_TO_GROUP, DELETE_SOURCE_GROUP, REMOVE_SOURCE_FROM_GROUP } from "./group" 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" import { PageActionTypes, SELECT_PAGE, PageType, selectAllArticles } from "./page"
@ -51,7 +51,7 @@ export class AppState {
type: ContextMenuType, type: ContextMenuType,
event?: MouseEvent | string, event?: MouseEvent | string,
position?: [number, number], position?: [number, number],
target?: [RSSItem, FeedIdType] | RSSSource | string target?: [RSSItem, string] | RSSSource | string
} }
constructor() { constructor() {
@ -74,7 +74,7 @@ interface OpenItemMenuAction {
type: typeof OPEN_ITEM_MENU type: typeof OPEN_ITEM_MENU
event: MouseEvent event: MouseEvent
item: RSSItem item: RSSItem
feedId: FeedIdType feedId: string
} }
interface OpenTextMenuAction { interface OpenTextMenuAction {
@ -109,7 +109,7 @@ export function closeContextMenu(): ContextMenuActionTypes {
return { type: CLOSE_CONTEXT_MENU } return { type: CLOSE_CONTEXT_MENU }
} }
export function openItemMenu(item: RSSItem, feedId: FeedIdType, event: React.MouseEvent): ContextMenuActionTypes { export function openItemMenu(item: RSSItem, feedId: string, event: React.MouseEvent): ContextMenuActionTypes {
return { return {
type: OPEN_ITEM_MENU, type: OPEN_ITEM_MENU,
event: event.nativeEvent, event: event.nativeEvent,

View File

@ -6,20 +6,19 @@ import { PageActionTypes, SELECT_PAGE, PageType } from "./page"
export const ALL = "ALL" export const ALL = "ALL"
export const SOURCE = "SOURCE" export const SOURCE = "SOURCE"
export type FeedIdType = number | string
const LOAD_QUANTITY = 50 const LOAD_QUANTITY = 50
export class RSSFeed { export class RSSFeed {
id: FeedIdType _id: string
loaded: boolean loaded: boolean
loading: boolean loading: boolean
allLoaded: boolean allLoaded: boolean
sids: number[] sids: number[]
iids: number[] iids: string[]
constructor (id: FeedIdType, sids=[]) { constructor (id: string = null, sids=[]) {
this.id = id this._id = id
this.sids = sids this.sids = sids
this.iids = [] this.iids = []
this.loaded = false this.loaded = false
@ -44,7 +43,7 @@ export class RSSFeed {
} }
export type FeedState = { export type FeedState = {
[id in FeedIdType]: RSSFeed [_id: string]: RSSFeed
} }
export const INIT_FEEDS = 'INIT_FEEDS' export const INIT_FEEDS = 'INIT_FEEDS'
@ -200,7 +199,7 @@ export function feedReducer(
let nextState = { ...state } let nextState = { ...state }
for (let k of Object.keys(state)) { for (let k of Object.keys(state)) {
if (state[k].loaded) { if (state[k].loaded) {
let iids = action.items.filter(i => state[k].sids.includes(i.source)).map(i => i.id) let iids = action.items.filter(i => state[k].sids.includes(i.source)).map(i => i._id)
if (iids.length > 0) { if (iids.length > 0) {
nextState[k] = { nextState[k] = {
...nextState[k], ...nextState[k],
@ -217,11 +216,11 @@ export function feedReducer(
switch (action.status) { switch (action.status) {
case ActionStatus.Success: return { case ActionStatus.Success: return {
...state, ...state,
[action.feed.id]: { [action.feed._id]: {
...action.feed, ...action.feed,
loaded: true, loaded: true,
allLoaded: action.items.length < LOAD_QUANTITY, allLoaded: action.items.length < LOAD_QUANTITY,
iids: action.items.map(i => i.id) iids: action.items.map(i => i._id)
} }
} }
default: return state default: return state
@ -230,23 +229,23 @@ export function feedReducer(
switch (action.status) { switch (action.status) {
case ActionStatus.Request: return { case ActionStatus.Request: return {
...state, ...state,
[action.feed.id] : { [action.feed._id] : {
...action.feed, ...action.feed,
loading: true loading: true
} }
} }
case ActionStatus.Success: return { case ActionStatus.Success: return {
...state, ...state,
[action.feed.id] : { [action.feed._id] : {
...action.feed, ...action.feed,
loading: false, loading: false,
allLoaded: action.items.length < LOAD_QUANTITY, allLoaded: action.items.length < LOAD_QUANTITY,
iids: [...action.feed.iids, ...action.items.map(i => i.id)] iids: [...action.feed.iids, ...action.items.map(i => i._id)]
} }
} }
case ActionStatus.Failure: return { case ActionStatus.Failure: return {
...state, ...state,
[action.feed.id] : { [action.feed._id] : {
...action.feed, ...action.feed,
loading: false loading: false
} }

View File

@ -1,7 +1,7 @@
import fs = require("fs") import fs = require("fs")
import { SourceActionTypes, ADD_SOURCE, DELETE_SOURCE, addSource } from "./source" import { SourceActionTypes, ADD_SOURCE, DELETE_SOURCE, addSource } from "./source"
import { ActionStatus, AppThunk, domParser, AppDispatch, getWindowBreakpoint } from "../utils" import { ActionStatus, AppThunk, domParser, AppDispatch } from "../utils"
import { saveSettings } from "./app" import { saveSettings } from "./app"
const GROUPS_STORE_KEY = "sourceGroups" const GROUPS_STORE_KEY = "sourceGroups"

View File

@ -5,7 +5,7 @@ import { FeedActionTypes, INIT_FEED, LOAD_MORE } from "./feed"
import Parser = require("@yang991178/rss-parser") import Parser = require("@yang991178/rss-parser")
export class RSSItem { export class RSSItem {
id: number _id: string
source: number source: number
title: string title: string
link: string link: string
@ -14,20 +14,25 @@ export class RSSItem {
thumb?: string thumb?: string
content: string content: string
snippet: string snippet: string
creator: string creator?: string
categories: string[] categories?: string[]
hasRead: boolean hasRead: boolean
starred?: true
hidden?: true
constructor (item: Parser.Item, source: RSSSource) { constructor (item: Parser.Item, source: RSSSource) {
this.source = source.sid this.source = source.sid
this.title = item.title this.title = item.title || ""
this.link = item.link this.link = item.link || ""
this.date = new Date(item.isoDate)
this.fetchedDate = new Date() this.fetchedDate = new Date()
this.date = item.isoDate ? new Date(item.isoDate) : this.fetchedDate
if (item.thumb) this.thumb = item.thumb if (item.thumb) this.thumb = item.thumb
else if (item.image) this.thumb = item.image else if (item.image) this.thumb = item.image
else { else {
let dom = domParser.parseFromString(item.content, "text/html") let dom = domParser.parseFromString(item.content, "text/html")
let baseEl = dom.createElement('base')
baseEl.setAttribute('href', this.link.split("/").slice(0, 3).join("/"))
dom.head.append(baseEl)
let img = dom.querySelector("img") let img = dom.querySelector("img")
if (img && img.src) this.thumb = img.src if (img && img.src) this.thumb = img.src
} }
@ -35,8 +40,8 @@ export class RSSItem {
this.content = item.fullContent this.content = item.fullContent
this.snippet = htmlDecode(item.fullContent) this.snippet = htmlDecode(item.fullContent)
} else { } else {
this.content = item.content this.content = item.content || ""
this.snippet = htmlDecode(item.contentSnippet) this.snippet = htmlDecode(item.contentSnippet || "")
} }
this.creator = item.creator this.creator = item.creator
this.categories = item.categories this.categories = item.categories
@ -45,12 +50,14 @@ export class RSSItem {
} }
export type ItemState = { export type ItemState = {
[id: number]: RSSItem [_id: string]: RSSItem
} }
export const FETCH_ITEMS = 'FETCH_ITEMS' export const FETCH_ITEMS = 'FETCH_ITEMS'
export const MARK_READ = "MARK_READ" export const MARK_READ = "MARK_READ"
export const MARK_UNREAD = "MARK_UNREAD" export const MARK_UNREAD = "MARK_UNREAD"
export const TOGGLE_STARRED = "TOGGLE_STARRED"
export const TOGGLE_HIDDEN = "TOGGLE_HIDDEN"
interface FetchItemsAction { interface FetchItemsAction {
type: typeof FETCH_ITEMS type: typeof FETCH_ITEMS
@ -71,7 +78,17 @@ interface MarkUnreadAction {
item: RSSItem item: RSSItem
} }
export type ItemActionTypes = FetchItemsAction | MarkReadAction | MarkUnreadAction interface ToggleStarredAction {
type: typeof TOGGLE_STARRED
item: RSSItem
}
interface ToggleHiddenAction {
type: typeof TOGGLE_HIDDEN
item: RSSItem
}
export type ItemActionTypes = FetchItemsAction | MarkReadAction | MarkUnreadAction | ToggleStarredAction | ToggleHiddenAction
export function fetchItemsRequest(fetchCount = 0): ItemActionTypes { export function fetchItemsRequest(fetchCount = 0): ItemActionTypes {
return { return {
@ -107,22 +124,15 @@ export function fetchItemsIntermediate(): ItemActionTypes {
export function insertItems(items: RSSItem[]): Promise<RSSItem[]> { export function insertItems(items: RSSItem[]): Promise<RSSItem[]> {
return new Promise<RSSItem[]>((resolve, reject) => { return new Promise<RSSItem[]>((resolve, reject) => {
db.idb.find({}).projection({ id: 1 }).sort({ id: -1 }).limit(1).exec((err, docs) => {
if (err) {
reject(err)
}
let count = (docs.length == 0) ? 0 : (docs[0].id + 1)
items.sort((a, b) => a.date.getTime() - b.date.getTime()) items.sort((a, b) => a.date.getTime() - b.date.getTime())
for (let i of items) i.id = count++ db.idb.insert(items, (err, inserted) => {
db.idb.insert(items, (err) => {
if (err) { if (err) {
reject(err) reject(err)
} else { } else {
resolve(items) resolve(inserted)
} }
}) })
}) })
})
} }
export function fetchItems(): AppThunk<Promise<void>> { export function fetchItems(): AppThunk<Promise<void>> {
@ -145,8 +155,8 @@ export function fetchItems(): AppThunk<Promise<void>> {
} }
}) })
insertItems(items) insertItems(items)
.then(() => { .then(inserted => {
dispatch(fetchItemsSuccess(items.reverse())) dispatch(fetchItemsSuccess(inserted.reverse()))
resolve() resolve()
}) })
.catch(err => { .catch(err => {
@ -159,30 +169,62 @@ export function fetchItems(): AppThunk<Promise<void>> {
} }
} }
export const markReadDone = (item: RSSItem): ItemActionTypes => ({ const markReadDone = (item: RSSItem): ItemActionTypes => ({
type: MARK_READ, type: MARK_READ,
item: item item: item
}) })
export const markUnreadDone = (item: RSSItem): ItemActionTypes => ({ const markUnreadDone = (item: RSSItem): ItemActionTypes => ({
type: MARK_UNREAD, type: MARK_UNREAD,
item: item item: item
}) })
export function markRead(item: RSSItem): AppThunk { export function markRead(item: RSSItem): AppThunk {
return (dispatch) => { return (dispatch) => {
db.idb.update({ id: item.id }, { $set: { hasRead: true } }) db.idb.update({ _id: item._id }, { $set: { hasRead: true } })
dispatch(markReadDone(item)) dispatch(markReadDone(item))
} }
} }
export function markUnread(item: RSSItem): AppThunk { export function markUnread(item: RSSItem): AppThunk {
return (dispatch) => { return (dispatch) => {
db.idb.update({ id: item.id }, { $set: { hasRead: false } }) db.idb.update({ _id: item._id }, { $set: { hasRead: false } })
dispatch(markUnreadDone(item)) dispatch(markUnreadDone(item))
} }
} }
const toggleStarredDone = (item: RSSItem): ItemActionTypes => ({
type: TOGGLE_STARRED,
item: item
})
export function toggleStarred(item: RSSItem): AppThunk {
return (dispatch) => {
if (item.starred === true) {
db.idb.update({ _id: item._id }, { $unset: { starred: true } })
} else {
db.idb.update({ _id: item._id }, { $set: { starred: true } })
}
dispatch(toggleStarredDone(item))
}
}
const toggleHiddenDone = (item: RSSItem): ItemActionTypes => ({
type: TOGGLE_HIDDEN,
item: item
})
export function toggleHidden(item: RSSItem): AppThunk {
return (dispatch) => {
if (item.hidden === true) {
db.idb.update({ _id: item._id }, { $unset: { hidden: true } })
} else {
db.idb.update({ _id: item._id }, { $set: { hidden: true } })
}
dispatch(toggleHiddenDone(item))
}
}
export function itemReducer( export function itemReducer(
state: ItemState = {}, state: ItemState = {},
action: ItemActionTypes | FeedActionTypes action: ItemActionTypes | FeedActionTypes
@ -193,7 +235,7 @@ export function itemReducer(
case ActionStatus.Success: { case ActionStatus.Success: {
let newMap = {} let newMap = {}
for (let i of action.items) { for (let i of action.items) {
newMap[i.id] = i newMap[i._id] = i
} }
return {...newMap, ...state} return {...newMap, ...state}
} }
@ -202,18 +244,36 @@ export function itemReducer(
case MARK_UNREAD: case MARK_UNREAD:
case MARK_READ: return { case MARK_READ: return {
...state, ...state,
[action.item.id] : { [action.item._id] : {
...action.item, ...action.item,
hasRead: action.type === MARK_READ hasRead: action.type === MARK_READ
} }
} }
case TOGGLE_STARRED: {
let newItem = { ...action.item }
if (newItem.starred === true) delete newItem.starred
else newItem.starred = true
return {
...state,
[newItem._id]: newItem
}
}
case TOGGLE_HIDDEN: {
let newItem = { ...action.item }
if (newItem.hidden === true) delete newItem.hidden
else newItem.hidden = true
return {
...state,
[newItem._id]: newItem
}
}
case LOAD_MORE: case LOAD_MORE:
case INIT_FEED: { case INIT_FEED: {
switch (action.status) { switch (action.status) {
case ActionStatus.Success: { case ActionStatus.Success: {
let nextState = { ...state } let nextState = { ...state }
for (let i of action.items) { for (let i of action.items) {
nextState[i.id] = i nextState[i._id] = i
} }
return nextState return nextState
} }

View File

@ -1,4 +1,4 @@
import { ALL, SOURCE, FeedIdType, loadMore } from "./feed" import { ALL, SOURCE, loadMore } from "./feed"
import { getWindowBreakpoint, AppThunk, getDefaultView } from "../utils" import { getWindowBreakpoint, AppThunk, getDefaultView } from "../utils"
import { RSSItem, markRead } from "./item" import { RSSItem, markRead } from "./item"
import { SourceActionTypes, DELETE_SOURCE } from "./source" import { SourceActionTypes, DELETE_SOURCE } from "./source"
@ -34,7 +34,7 @@ interface SwitchViewAction {
interface ShowItemAction { interface ShowItemAction {
type: typeof SHOW_ITEM type: typeof SHOW_ITEM
feedId: FeedIdType feedId: string
item: RSSItem item: RSSItem
} }
@ -70,7 +70,7 @@ export function switchView(viewType: ViewType): PageActionTypes {
} }
} }
export function showItem(feedId: FeedIdType, item: RSSItem): PageActionTypes { export function showItem(feedId: string, item: RSSItem): PageActionTypes {
return { return {
type: SHOW_ITEM, type: SHOW_ITEM,
feedId: feedId, feedId: feedId,
@ -109,8 +109,8 @@ export function showOffsetItem(offset: number): AppThunk {
export class PageState { export class PageState {
viewType = getDefaultView() viewType = getDefaultView()
feedId = ALL as FeedIdType feedId = ALL
itemId = -1 itemId = null as string
} }
export function pageReducer( export function pageReducer(
@ -133,16 +133,16 @@ export function pageReducer(
case SWITCH_VIEW: return { case SWITCH_VIEW: return {
...state, ...state,
viewType: action.viewType, viewType: action.viewType,
itemId: action.viewType === ViewType.List ? state.itemId : -1 itemId: action.viewType === ViewType.List ? state.itemId : null
} }
case SHOW_ITEM: return { case SHOW_ITEM: return {
...state, ...state,
itemId: action.item.id itemId: action.item._id
} }
case DELETE_SOURCE: case DELETE_SOURCE:
case DISMISS_ITEM: return { case DISMISS_ITEM: return {
...state, ...state,
itemId: -1 itemId: null
} }
default: return state default: return state
} }

View File

@ -2,6 +2,7 @@ import { shell, remote } from "electron"
import { ThunkAction, ThunkDispatch } from "redux-thunk" import { ThunkAction, ThunkDispatch } from "redux-thunk"
import { AnyAction } from "redux" import { AnyAction } from "redux"
import { RootState } from "./reducer" import { RootState } from "./reducer"
import URL = require("url")
export enum ActionStatus { export enum ActionStatus {
Request, Success, Failure, Intermediate Request, Success, Failure, Intermediate
@ -49,6 +50,7 @@ export function setProxy(address = null) {
import ElectronProxyAgent = require("@yang991178/electron-proxy-agent") import ElectronProxyAgent = require("@yang991178/electron-proxy-agent")
import { ViewType } from "./models/page" import { ViewType } from "./models/page"
import { RSSSource } from "./models/source"
let agent = new ElectronProxyAgent(remote.getCurrentWebContents().session) let agent = new ElectronProxyAgent(remote.getCurrentWebContents().session)
export const rssParser = new Parser({ export const rssParser = new Parser({
customFields: customFields, customFields: customFields,