mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-02-07 23:38:41 +01:00
use _id & csp update
This commit is contained in:
parent
0128728889
commit
2ff5e13219
6
dist/article/article.html
vendored
6
dist/article/article.html
vendored
@ -3,12 +3,12 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; img-src *; style-src 'self' 'unsafe-inline'; frame-src *; media-src *">
|
||||
<title>Hello World!</title>
|
||||
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>Article</title>
|
||||
<link rel="stylesheet" href="article.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
<script src="article.js"></script>
|
||||
<script integrity="sha256-34JRfFnY5YiFDB1MABAIxq6OfvlgM/Ba2el4MdA0WoM=" src="article.js"></script>
|
||||
</body>
|
||||
</html>
|
14
dist/article/article.js
vendored
14
dist/article/article.js
vendored
@ -3,8 +3,20 @@ function get(name) {
|
||||
return decodeURIComponent(name[1]);
|
||||
}
|
||||
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")
|
||||
main.innerHTML = decodeURIComponent(window.atob(get("h")))
|
||||
main.innerHTML = dom.body.innerHTML
|
||||
document.addEventListener("click", event => {
|
||||
event.preventDefault()
|
||||
if (event.target.href) post("request-navigation", event.target.href)
|
||||
|
1
dist/styles.css
vendored
1
dist/styles.css
vendored
@ -510,6 +510,7 @@ img.favicon {
|
||||
transition: box-shadow linear .08s;
|
||||
transform: scale(1);
|
||||
cursor: pointer;
|
||||
animation-fill-mode: none;
|
||||
}
|
||||
.card:hover {
|
||||
box-shadow: #0006 0px 5px 40px;
|
||||
|
@ -13,6 +13,8 @@ type ArticleProps = {
|
||||
source: RSSSource
|
||||
dismiss: () => void
|
||||
toggleHasRead: (item: RSSItem) => void
|
||||
toggleStarred: (item: RSSItem) => void
|
||||
toggleHidden: (item: RSSItem) => 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 => {
|
||||
switch (event.channel) {
|
||||
case "request-navigation": {
|
||||
@ -84,7 +102,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
||||
}
|
||||
}
|
||||
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.componentDidMount()
|
||||
@ -112,7 +130,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
||||
<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
|
||||
</>))) + `&s=${this.state.fontSize}&u=${this.props.item.link}`
|
||||
|
||||
render = () => (
|
||||
<div className="article">
|
||||
@ -128,11 +146,13 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
||||
<CommandBarButton
|
||||
title={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)} />
|
||||
<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
|
||||
title="字体大小"
|
||||
disabled={this.state.loadWebpage}
|
||||
@ -145,9 +165,10 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
||||
iconProps={{iconName: "Globe"}}
|
||||
onClick={this.toggleWebpage} />
|
||||
<CommandBarButton
|
||||
title="在浏览器中打开"
|
||||
iconProps={{iconName: "NavigateExternalInline", style: {marginTop: -4}}}
|
||||
onClick={this.openInBrowser} />
|
||||
title="更多"
|
||||
iconProps={{iconName: "More"}}
|
||||
menuIconProps={{style: {display: "none"}}}
|
||||
menuProps={this.moreMenuProps()} />
|
||||
</Stack>
|
||||
<Stack horizontal horizontalAlign="end" style={{width: 112}}>
|
||||
<CommandBarButton
|
||||
@ -158,7 +179,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
||||
</Stack>
|
||||
<webview
|
||||
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()}
|
||||
preload={this.state.loadWebpage ? null : "article/preload.js"}
|
||||
partition="sandbox" />
|
||||
|
@ -2,15 +2,14 @@ import * as React from "react"
|
||||
import { openExternal } from "../../scripts/utils"
|
||||
import { RSSSource, SourceOpenTarget } from "../../scripts/models/source"
|
||||
import { RSSItem } from "../../scripts/models/item"
|
||||
import { FeedIdType } from "../../scripts/models/feed"
|
||||
|
||||
export interface CardProps {
|
||||
feedId: FeedIdType
|
||||
feedId: string
|
||||
item: RSSItem
|
||||
source: RSSSource
|
||||
markRead: (item: RSSItem) => void
|
||||
contextMenu: (feedId: FeedIdType, item: RSSItem, e) => void
|
||||
showItem: (fid: FeedIdType, item: RSSItem) => void
|
||||
contextMenu: (feedId: string, item: RSSItem, e) => void
|
||||
showItem: (fid: string, item: RSSItem) => void
|
||||
}
|
||||
|
||||
export class Card extends React.Component<CardProps> {
|
||||
|
@ -5,7 +5,6 @@ import { ContextualMenu, IContextualMenuItem, ContextualMenuItemType, Directiona
|
||||
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"
|
||||
import { ViewType } from "../scripts/models/page"
|
||||
|
||||
export type ContextMenuProps = ContextReduxProps & {
|
||||
@ -13,12 +12,14 @@ export type ContextMenuProps = ContextReduxProps & {
|
||||
event?: MouseEvent | string
|
||||
position?: [number, number]
|
||||
item?: RSSItem
|
||||
feedId?: FeedIdType
|
||||
feedId?: string
|
||||
text?: string
|
||||
viewType: ViewType
|
||||
showItem: (feedId: FeedIdType, item: RSSItem) => void
|
||||
showItem: (feedId: string, item: RSSItem) => void
|
||||
markRead: (item: RSSItem) => void
|
||||
markUnread: (item: RSSItem) => void
|
||||
toggleStarred: (item: RSSItem) => void
|
||||
toggleHidden: (item: RSSItem) => void
|
||||
switchView: (viewType: ViewType) => void
|
||||
close: () => void
|
||||
}
|
||||
@ -59,8 +60,15 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
|
||||
onClick: () => { this.props.markRead(this.props.item) }
|
||||
},
|
||||
{
|
||||
key: "markBelowAsRead",
|
||||
text: "将以下标为已读"
|
||||
key: "toggleStarred",
|
||||
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",
|
||||
|
@ -34,8 +34,8 @@ class CardsFeed extends React.Component<FeedProps> {
|
||||
{
|
||||
this.props.items.map((item) => (
|
||||
<DefaultCard
|
||||
feedId={this.props.feed.id}
|
||||
key={item.id}
|
||||
feedId={this.props.feed._id}
|
||||
key={item._id}
|
||||
item={item}
|
||||
source={this.props.sourceMap[item.source]}
|
||||
markRead={this.props.markRead}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as React from "react"
|
||||
import { RSSItem } from "../../scripts/models/item"
|
||||
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 CardsFeed from "./cards-feed"
|
||||
import ListFeed from "./list-feed"
|
||||
@ -12,9 +12,9 @@ export type FeedProps = FeedReduxProps & {
|
||||
items: RSSItem[]
|
||||
sourceMap: Object
|
||||
markRead: (item: RSSItem) => void
|
||||
contextMenu: (feedId: FeedIdType, item: RSSItem, e) => void
|
||||
contextMenu: (feedId: string, item: RSSItem, e) => void
|
||||
loadMore: (feed: RSSFeed) => void
|
||||
showItem: (fid: FeedIdType, item: RSSItem) => void
|
||||
showItem: (fid: string, item: RSSItem) => void
|
||||
}
|
||||
|
||||
export class Feed extends React.Component<FeedProps> {
|
||||
|
@ -10,8 +10,8 @@ class ListFeed extends React.Component<FeedProps> {
|
||||
{
|
||||
this.props.items.map((item) => (
|
||||
<ListCard
|
||||
feedId={this.props.feed.id}
|
||||
key={item.id}
|
||||
feedId={this.props.feed._id}
|
||||
key={item._id}
|
||||
item={item}
|
||||
source={this.props.sourceMap[item.source]}
|
||||
markRead={this.props.markRead}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import * as React from "react"
|
||||
import { FeedIdType } from "../scripts/models/feed"
|
||||
import { FeedContainer } from "../containers/feed-container"
|
||||
import { AnimationClassNames, Icon } from "@fluentui/react"
|
||||
import ArticleContainer from "../containers/article-container"
|
||||
@ -8,8 +7,8 @@ import { ViewType } from "../scripts/models/page"
|
||||
type PageProps = {
|
||||
menuOn: boolean
|
||||
settingsOn: boolean
|
||||
feeds: FeedIdType[]
|
||||
itemId: number
|
||||
feeds: string[]
|
||||
itemId: string
|
||||
viewType: ViewType
|
||||
dismissItem: () => void
|
||||
offsetItem: (offset: number) => void
|
||||
@ -34,7 +33,7 @@ class Page extends React.Component<PageProps> {
|
||||
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} />
|
||||
))}
|
||||
</div>}
|
||||
{this.props.itemId >= 0 && (
|
||||
{this.props.itemId && (
|
||||
<div className="article-container" onClick={this.props.dismissItem}>
|
||||
<div className={"article-wrapper " + AnimationClassNames.slideUpIn20} onClick={e => e.stopPropagation()}>
|
||||
<ArticleContainer itemId={this.props.itemId} />
|
||||
@ -54,7 +53,7 @@ class Page extends React.Component<PageProps> {
|
||||
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} />
|
||||
))}
|
||||
</div>
|
||||
{this.props.itemId >= 0 && (
|
||||
{this.props.itemId && (
|
||||
<div className="side-article-wrapper">
|
||||
<ArticleContainer itemId={this.props.itemId} />
|
||||
</div>
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { connect } from "react-redux"
|
||||
import { createSelector } from "reselect"
|
||||
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 { dismissItem } from "../scripts/models/page"
|
||||
import Article from "../components/article"
|
||||
import { openTextMenu } from "../scripts/models/app"
|
||||
|
||||
type ArticleContainerProps = {
|
||||
itemId: number
|
||||
itemId: string
|
||||
}
|
||||
|
||||
const getItem = (state: RootState, props: ArticleContainerProps) => state.items[props.itemId]
|
||||
@ -28,6 +28,8 @@ const mapDispatchToProps = (dispatch: AppDispatch) => {
|
||||
return {
|
||||
dismiss: () => dispatch(dismissItem()),
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,8 @@ import { createSelector } from "reselect"
|
||||
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 { RSSItem, markRead, markUnread, toggleStarred, toggleHidden } from "../scripts/models/item"
|
||||
import { showItem, switchView, ViewType } from "../scripts/models/page"
|
||||
import { FeedIdType } from "../scripts/models/feed"
|
||||
import { setDefaultView } from "../scripts/utils"
|
||||
|
||||
const getContext = (state: RootState) => state.app.contextMenu
|
||||
@ -38,9 +37,11 @@ const mapStateToProps = createSelector(
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
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)),
|
||||
markUnread: (item: RSSItem) => dispatch(markUnread(item)),
|
||||
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)),
|
||||
toggleHidden: (item: RSSItem) => dispatch(toggleHidden(item)),
|
||||
switchView: (viewType: ViewType) => {
|
||||
setDefaultView(viewType)
|
||||
dispatch(switchView(viewType))
|
||||
|
@ -3,12 +3,12 @@ import { createSelector } from "reselect"
|
||||
import { RootState } from "../scripts/reducer"
|
||||
import { markRead, RSSItem } from "../scripts/models/item"
|
||||
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 { Feed } from "../components/feeds/feed"
|
||||
|
||||
interface FeedContainerProps {
|
||||
feedId: FeedIdType
|
||||
feedId: string
|
||||
viewType: ViewType
|
||||
}
|
||||
|
||||
@ -31,9 +31,9 @@ const makeMapStateToProps = () => {
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
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)),
|
||||
showItem: (fid: FeedIdType, item: RSSItem) => dispatch(showItem(fid, item))
|
||||
showItem: (fid: string, item: RSSItem) => dispatch(showItem(fid, item))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ import { ViewType } from "../scripts/models/page"
|
||||
import Nav from "../components/nav"
|
||||
|
||||
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(
|
||||
[getState, getItemShown],
|
||||
|
@ -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 '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>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
|
@ -20,5 +20,6 @@ export const idb = new Datastore<RSSItem>({
|
||||
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 })
|
@ -1,7 +1,7 @@
|
||||
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, 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 { PageActionTypes, SELECT_PAGE, PageType, selectAllArticles } from "./page"
|
||||
|
||||
@ -51,7 +51,7 @@ export class AppState {
|
||||
type: ContextMenuType,
|
||||
event?: MouseEvent | string,
|
||||
position?: [number, number],
|
||||
target?: [RSSItem, FeedIdType] | RSSSource | string
|
||||
target?: [RSSItem, string] | RSSSource | string
|
||||
}
|
||||
|
||||
constructor() {
|
||||
@ -74,7 +74,7 @@ interface OpenItemMenuAction {
|
||||
type: typeof OPEN_ITEM_MENU
|
||||
event: MouseEvent
|
||||
item: RSSItem
|
||||
feedId: FeedIdType
|
||||
feedId: string
|
||||
}
|
||||
|
||||
interface OpenTextMenuAction {
|
||||
@ -109,7 +109,7 @@ export function closeContextMenu(): ContextMenuActionTypes {
|
||||
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 {
|
||||
type: OPEN_ITEM_MENU,
|
||||
event: event.nativeEvent,
|
||||
|
@ -6,20 +6,19 @@ import { PageActionTypes, SELECT_PAGE, PageType } from "./page"
|
||||
|
||||
export const ALL = "ALL"
|
||||
export const SOURCE = "SOURCE"
|
||||
export type FeedIdType = number | string
|
||||
|
||||
const LOAD_QUANTITY = 50
|
||||
|
||||
export class RSSFeed {
|
||||
id: FeedIdType
|
||||
_id: string
|
||||
loaded: boolean
|
||||
loading: boolean
|
||||
allLoaded: boolean
|
||||
sids: number[]
|
||||
iids: number[]
|
||||
iids: string[]
|
||||
|
||||
constructor (id: FeedIdType, sids=[]) {
|
||||
this.id = id
|
||||
constructor (id: string = null, sids=[]) {
|
||||
this._id = id
|
||||
this.sids = sids
|
||||
this.iids = []
|
||||
this.loaded = false
|
||||
@ -44,7 +43,7 @@ export class RSSFeed {
|
||||
}
|
||||
|
||||
export type FeedState = {
|
||||
[id in FeedIdType]: RSSFeed
|
||||
[_id: string]: RSSFeed
|
||||
}
|
||||
|
||||
export const INIT_FEEDS = 'INIT_FEEDS'
|
||||
@ -200,7 +199,7 @@ export function feedReducer(
|
||||
let nextState = { ...state }
|
||||
for (let k of Object.keys(state)) {
|
||||
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) {
|
||||
nextState[k] = {
|
||||
...nextState[k],
|
||||
@ -217,11 +216,11 @@ export function feedReducer(
|
||||
switch (action.status) {
|
||||
case ActionStatus.Success: return {
|
||||
...state,
|
||||
[action.feed.id]: {
|
||||
[action.feed._id]: {
|
||||
...action.feed,
|
||||
loaded: true,
|
||||
allLoaded: action.items.length < LOAD_QUANTITY,
|
||||
iids: action.items.map(i => i.id)
|
||||
iids: action.items.map(i => i._id)
|
||||
}
|
||||
}
|
||||
default: return state
|
||||
@ -230,23 +229,23 @@ export function feedReducer(
|
||||
switch (action.status) {
|
||||
case ActionStatus.Request: return {
|
||||
...state,
|
||||
[action.feed.id] : {
|
||||
[action.feed._id] : {
|
||||
...action.feed,
|
||||
loading: true
|
||||
}
|
||||
}
|
||||
case ActionStatus.Success: return {
|
||||
...state,
|
||||
[action.feed.id] : {
|
||||
[action.feed._id] : {
|
||||
...action.feed,
|
||||
loading: false,
|
||||
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 {
|
||||
...state,
|
||||
[action.feed.id] : {
|
||||
[action.feed._id] : {
|
||||
...action.feed,
|
||||
loading: false
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import fs = require("fs")
|
||||
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"
|
||||
|
||||
const GROUPS_STORE_KEY = "sourceGroups"
|
||||
|
@ -5,7 +5,7 @@ import { FeedActionTypes, INIT_FEED, LOAD_MORE } from "./feed"
|
||||
import Parser = require("@yang991178/rss-parser")
|
||||
|
||||
export class RSSItem {
|
||||
id: number
|
||||
_id: string
|
||||
source: number
|
||||
title: string
|
||||
link: string
|
||||
@ -14,20 +14,25 @@ export class RSSItem {
|
||||
thumb?: string
|
||||
content: string
|
||||
snippet: string
|
||||
creator: string
|
||||
categories: string[]
|
||||
creator?: string
|
||||
categories?: string[]
|
||||
hasRead: boolean
|
||||
starred?: true
|
||||
hidden?: true
|
||||
|
||||
constructor (item: Parser.Item, source: RSSSource) {
|
||||
this.source = source.sid
|
||||
this.title = item.title
|
||||
this.link = item.link
|
||||
this.date = new Date(item.isoDate)
|
||||
this.title = item.title || ""
|
||||
this.link = item.link || ""
|
||||
this.fetchedDate = new Date()
|
||||
this.date = item.isoDate ? new Date(item.isoDate) : this.fetchedDate
|
||||
if (item.thumb) this.thumb = item.thumb
|
||||
else if (item.image) this.thumb = item.image
|
||||
else {
|
||||
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")
|
||||
if (img && img.src) this.thumb = img.src
|
||||
}
|
||||
@ -35,8 +40,8 @@ export class RSSItem {
|
||||
this.content = item.fullContent
|
||||
this.snippet = htmlDecode(item.fullContent)
|
||||
} else {
|
||||
this.content = item.content
|
||||
this.snippet = htmlDecode(item.contentSnippet)
|
||||
this.content = item.content || ""
|
||||
this.snippet = htmlDecode(item.contentSnippet || "")
|
||||
}
|
||||
this.creator = item.creator
|
||||
this.categories = item.categories
|
||||
@ -45,12 +50,14 @@ export class RSSItem {
|
||||
}
|
||||
|
||||
export type ItemState = {
|
||||
[id: number]: RSSItem
|
||||
[_id: string]: RSSItem
|
||||
}
|
||||
|
||||
export const FETCH_ITEMS = 'FETCH_ITEMS'
|
||||
export const MARK_READ = "MARK_READ"
|
||||
export const MARK_UNREAD = "MARK_UNREAD"
|
||||
export const TOGGLE_STARRED = "TOGGLE_STARRED"
|
||||
export const TOGGLE_HIDDEN = "TOGGLE_HIDDEN"
|
||||
|
||||
interface FetchItemsAction {
|
||||
type: typeof FETCH_ITEMS
|
||||
@ -71,7 +78,17 @@ interface MarkUnreadAction {
|
||||
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 {
|
||||
return {
|
||||
@ -107,20 +124,13 @@ export function fetchItemsIntermediate(): ItemActionTypes {
|
||||
|
||||
export function insertItems(items: RSSItem[]): Promise<RSSItem[]> {
|
||||
return new Promise<RSSItem[]>((resolve, reject) => {
|
||||
db.idb.find({}).projection({ id: 1 }).sort({ id: -1 }).limit(1).exec((err, docs) => {
|
||||
items.sort((a, b) => a.date.getTime() - b.date.getTime())
|
||||
db.idb.insert(items, (err, inserted) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(inserted)
|
||||
}
|
||||
let count = (docs.length == 0) ? 0 : (docs[0].id + 1)
|
||||
items.sort((a, b) => a.date.getTime() - b.date.getTime())
|
||||
for (let i of items) i.id = count++
|
||||
db.idb.insert(items, (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(items)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -145,8 +155,8 @@ export function fetchItems(): AppThunk<Promise<void>> {
|
||||
}
|
||||
})
|
||||
insertItems(items)
|
||||
.then(() => {
|
||||
dispatch(fetchItemsSuccess(items.reverse()))
|
||||
.then(inserted => {
|
||||
dispatch(fetchItemsSuccess(inserted.reverse()))
|
||||
resolve()
|
||||
})
|
||||
.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,
|
||||
item: item
|
||||
})
|
||||
|
||||
export const markUnreadDone = (item: RSSItem): ItemActionTypes => ({
|
||||
const markUnreadDone = (item: RSSItem): ItemActionTypes => ({
|
||||
type: MARK_UNREAD,
|
||||
item: item
|
||||
})
|
||||
|
||||
export function markRead(item: RSSItem): AppThunk {
|
||||
return (dispatch) => {
|
||||
db.idb.update({ id: item.id }, { $set: { hasRead: true } })
|
||||
db.idb.update({ _id: item._id }, { $set: { hasRead: true } })
|
||||
dispatch(markReadDone(item))
|
||||
}
|
||||
}
|
||||
|
||||
export function markUnread(item: RSSItem): AppThunk {
|
||||
return (dispatch) => {
|
||||
db.idb.update({ id: item.id }, { $set: { hasRead: false } })
|
||||
db.idb.update({ _id: item._id }, { $set: { hasRead: false } })
|
||||
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(
|
||||
state: ItemState = {},
|
||||
action: ItemActionTypes | FeedActionTypes
|
||||
@ -193,7 +235,7 @@ export function itemReducer(
|
||||
case ActionStatus.Success: {
|
||||
let newMap = {}
|
||||
for (let i of action.items) {
|
||||
newMap[i.id] = i
|
||||
newMap[i._id] = i
|
||||
}
|
||||
return {...newMap, ...state}
|
||||
}
|
||||
@ -202,18 +244,36 @@ export function itemReducer(
|
||||
case MARK_UNREAD:
|
||||
case MARK_READ: return {
|
||||
...state,
|
||||
[action.item.id] : {
|
||||
[action.item._id] : {
|
||||
...action.item,
|
||||
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 INIT_FEED: {
|
||||
switch (action.status) {
|
||||
case ActionStatus.Success: {
|
||||
let nextState = { ...state }
|
||||
for (let i of action.items) {
|
||||
nextState[i.id] = i
|
||||
nextState[i._id] = i
|
||||
}
|
||||
return nextState
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ALL, SOURCE, FeedIdType, loadMore } from "./feed"
|
||||
import { ALL, SOURCE, loadMore } from "./feed"
|
||||
import { getWindowBreakpoint, AppThunk, getDefaultView } from "../utils"
|
||||
import { RSSItem, markRead } from "./item"
|
||||
import { SourceActionTypes, DELETE_SOURCE } from "./source"
|
||||
@ -34,7 +34,7 @@ interface SwitchViewAction {
|
||||
|
||||
interface ShowItemAction {
|
||||
type: typeof SHOW_ITEM
|
||||
feedId: FeedIdType
|
||||
feedId: string
|
||||
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 {
|
||||
type: SHOW_ITEM,
|
||||
feedId: feedId,
|
||||
@ -109,8 +109,8 @@ export function showOffsetItem(offset: number): AppThunk {
|
||||
|
||||
export class PageState {
|
||||
viewType = getDefaultView()
|
||||
feedId = ALL as FeedIdType
|
||||
itemId = -1
|
||||
feedId = ALL
|
||||
itemId = null as string
|
||||
}
|
||||
|
||||
export function pageReducer(
|
||||
@ -133,16 +133,16 @@ export function pageReducer(
|
||||
case SWITCH_VIEW: return {
|
||||
...state,
|
||||
viewType: action.viewType,
|
||||
itemId: action.viewType === ViewType.List ? state.itemId : -1
|
||||
itemId: action.viewType === ViewType.List ? state.itemId : null
|
||||
}
|
||||
case SHOW_ITEM: return {
|
||||
...state,
|
||||
itemId: action.item.id
|
||||
itemId: action.item._id
|
||||
}
|
||||
case DELETE_SOURCE:
|
||||
case DISMISS_ITEM: return {
|
||||
...state,
|
||||
itemId: -1
|
||||
itemId: null
|
||||
}
|
||||
default: return state
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { shell, remote } from "electron"
|
||||
import { ThunkAction, ThunkDispatch } from "redux-thunk"
|
||||
import { AnyAction } from "redux"
|
||||
import { RootState } from "./reducer"
|
||||
import URL = require("url")
|
||||
|
||||
export enum ActionStatus {
|
||||
Request, Success, Failure, Intermediate
|
||||
@ -49,6 +50,7 @@ export function setProxy(address = null) {
|
||||
|
||||
import ElectronProxyAgent = require("@yang991178/electron-proxy-agent")
|
||||
import { ViewType } from "./models/page"
|
||||
import { RSSSource } from "./models/source"
|
||||
let agent = new ElectronProxyAgent(remote.getCurrentWebContents().session)
|
||||
export const rssParser = new Parser({
|
||||
customFields: customFields,
|
||||
|
Loading…
x
Reference in New Issue
Block a user