mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-04-28 17:08:44 +02:00
article filtering
This commit is contained in:
parent
2ff5e13219
commit
a9c64cbe78
28
dist/styles.css
vendored
28
dist/styles.css
vendored
@ -311,10 +311,10 @@ img.favicon {
|
|||||||
right: 0;
|
right: 0;
|
||||||
width: 120%;
|
width: 120%;
|
||||||
height: 120%;
|
height: 120%;
|
||||||
box-shadow: inset 5px 0 20px #0004;
|
box-shadow: inset 5px 0 25px #0004;
|
||||||
}
|
}
|
||||||
.main.menu-on, .list-main.menu-on {
|
.main.menu-on, .list-main.menu-on {
|
||||||
padding-left: 280px;
|
margin-left: 280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav.hide-btns .btn-group .btn, nav.menu-on .btn-group .btn.hide-wide, .menu .btn-group .btn.hide-wide {
|
nav.hide-btns .btn-group .btn, nav.menu-on .btn-group .btn.hide-wide, .menu .btn-group .btn.hide-wide {
|
||||||
@ -403,7 +403,7 @@ img.favicon {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -32px;
|
margin-top: -32px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
@ -422,7 +422,7 @@ img.favicon {
|
|||||||
right: 0;
|
right: 0;
|
||||||
width: 120%;
|
width: 120%;
|
||||||
height: 120%;
|
height: 120%;
|
||||||
box-shadow: inset 5px 0 20px #0004;
|
box-shadow: inset 5px 0 25px #0004;
|
||||||
}
|
}
|
||||||
.list-feed {
|
.list-feed {
|
||||||
margin-top: 32px;
|
margin-top: 32px;
|
||||||
@ -476,11 +476,12 @@ img.favicon {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.read-indicator {
|
.read-indicator, .starred-indicator {
|
||||||
display: block;
|
display: block;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
float: right;
|
float: right;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
.read-indicator::after {
|
.read-indicator::after {
|
||||||
content: "";
|
content: "";
|
||||||
@ -494,6 +495,13 @@ img.favicon {
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
.starred-indicator::after {
|
||||||
|
content: "★";
|
||||||
|
vertical-align: top;
|
||||||
|
color: #ffaa44;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -574,6 +582,16 @@ img.favicon {
|
|||||||
.card p.snippet.show {
|
.card p.snippet.show {
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
.card.hidden::after, .list-card.hidden::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background: #0004;
|
||||||
|
}
|
||||||
|
|
||||||
.list-card {
|
.list-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Card } from "./card"
|
import { Card } from "./card"
|
||||||
import Time from "../utils/time"
|
|
||||||
import { AnimationClassNames } from "@fluentui/react"
|
import { AnimationClassNames } from "@fluentui/react"
|
||||||
|
import CardInfo from "./info"
|
||||||
|
|
||||||
class DefaultCard extends Card {
|
class DefaultCard extends Card {
|
||||||
|
className = () => {
|
||||||
|
let cn = ["card", AnimationClassNames.slideUpIn10]
|
||||||
|
if (this.props.item.snippet && this.props.item.thumb) cn.push("transform")
|
||||||
|
if (this.props.item.hidden) cn.push("hidden")
|
||||||
|
return cn.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className={"card "+AnimationClassNames.slideUpIn10+(this.props.item.snippet&&this.props.item.thumb?" transform":"")}
|
<div className={this.className()}
|
||||||
onClick={this.onClick} onMouseUp={this.onMouseUp} >
|
onClick={this.onClick} onMouseUp={this.onMouseUp} >
|
||||||
{this.props.item.thumb ? (
|
{this.props.item.thumb ? (
|
||||||
<img className="bg" src={this.props.item.thumb} />
|
<img className="bg" src={this.props.item.thumb} />
|
||||||
@ -15,12 +22,7 @@ class DefaultCard extends Card {
|
|||||||
{this.props.item.thumb ? (
|
{this.props.item.thumb ? (
|
||||||
<img className="head" src={this.props.item.thumb} />
|
<img className="head" src={this.props.item.thumb} />
|
||||||
) : null}
|
) : null}
|
||||||
<p className="info">
|
<CardInfo source={this.props.source} item={this.props.item} />
|
||||||
{this.props.source.iconurl ? <img src={this.props.source.iconurl} /> : null}
|
|
||||||
<span className="name">{this.props.source.name}</span>
|
|
||||||
<Time date={this.props.item.date} />
|
|
||||||
{this.props.item.hasRead ? null : <span className="read-indicator"></span>}
|
|
||||||
</p>
|
|
||||||
<h3 className="title">{this.props.item.title}</h3>
|
<h3 className="title">{this.props.item.title}</h3>
|
||||||
<p className={"snippet"+(this.props.item.thumb?"":" show")}>{this.props.item.snippet}</p>
|
<p className={"snippet"+(this.props.item.thumb?"":" show")}>{this.props.item.snippet}</p>
|
||||||
</div>
|
</div>
|
||||||
|
21
src/components/cards/info.tsx
Normal file
21
src/components/cards/info.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import Time from "../utils/time"
|
||||||
|
import { RSSSource } from "../../scripts/models/source"
|
||||||
|
import { RSSItem } from "../../scripts/models/item"
|
||||||
|
|
||||||
|
type CardInfoProps = {
|
||||||
|
source: RSSSource
|
||||||
|
item: RSSItem
|
||||||
|
}
|
||||||
|
|
||||||
|
const CardInfo = (props: CardInfoProps) => (
|
||||||
|
<p className="info">
|
||||||
|
{props.source.iconurl ? <img src={props.source.iconurl} /> : null}
|
||||||
|
<span className="name">{props.source.name}</span>
|
||||||
|
<Time date={props.item.date} />
|
||||||
|
{props.item.hasRead ? null : <span className="read-indicator"></span>}
|
||||||
|
{props.item.starred ? <span className="starred-indicator"></span> : null}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default CardInfo
|
@ -1,23 +1,25 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Card } from "./card"
|
import { Card } from "./card"
|
||||||
import Time from "../utils/time"
|
|
||||||
import { AnimationClassNames } from "@fluentui/react"
|
import { AnimationClassNames } from "@fluentui/react"
|
||||||
|
import CardInfo from "./info"
|
||||||
|
|
||||||
class ListCard extends Card {
|
class ListCard extends Card {
|
||||||
|
className = () => {
|
||||||
|
let cn = ["list-card", AnimationClassNames.slideUpIn10]
|
||||||
|
if (this.props.item.snippet && this.props.item.thumb) cn.push("transform")
|
||||||
|
if (this.props.item.hidden) cn.push("hidden")
|
||||||
|
return cn.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className={"list-card "+AnimationClassNames.slideUpIn10+(this.props.item.snippet&&this.props.item.thumb?" transform":"")}
|
<div className={this.className()}
|
||||||
onClick={this.onClick} onMouseUp={this.onMouseUp} >
|
onClick={this.onClick} onMouseUp={this.onMouseUp} >
|
||||||
{this.props.item.thumb ? (
|
{this.props.item.thumb ? (
|
||||||
<div className="head"><img src={this.props.item.thumb} /></div>
|
<div className="head"><img src={this.props.item.thumb} /></div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="data">
|
<div className="data">
|
||||||
<p className="info">
|
<CardInfo source={this.props.source} item={this.props.item} />
|
||||||
{this.props.source.iconurl ? <img src={this.props.source.iconurl} /> : null}
|
|
||||||
<span className="name">{this.props.source.name}</span>
|
|
||||||
<Time date={this.props.item.date} />
|
|
||||||
{this.props.item.hasRead ? null : <span className="read-indicator"></span>}
|
|
||||||
</p>
|
|
||||||
<h3 className="title">{this.props.item.title}</h3>
|
<h3 className="title">{this.props.item.title}</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,6 +6,7 @@ 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 { ViewType } from "../scripts/models/page"
|
import { ViewType } from "../scripts/models/page"
|
||||||
|
import { FeedFilter } from "../scripts/models/feed"
|
||||||
|
|
||||||
export type ContextMenuProps = ContextReduxProps & {
|
export type ContextMenuProps = ContextReduxProps & {
|
||||||
type: ContextMenuType
|
type: ContextMenuType
|
||||||
@ -14,13 +15,16 @@ export type ContextMenuProps = ContextReduxProps & {
|
|||||||
item?: RSSItem
|
item?: RSSItem
|
||||||
feedId?: string
|
feedId?: string
|
||||||
text?: string
|
text?: string
|
||||||
viewType: ViewType
|
viewType?: ViewType
|
||||||
|
filter?: FeedFilter
|
||||||
showItem: (feedId: string, 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
|
toggleStarred: (item: RSSItem) => void
|
||||||
toggleHidden: (item: RSSItem) => void
|
toggleHidden: (item: RSSItem) => void
|
||||||
switchView: (viewType: ViewType) => void
|
switchView: (viewType: ViewType) => void
|
||||||
|
switchFilter: (filter: FeedFilter) => void
|
||||||
|
toggleFilter: (filter: FeedFilter) => void
|
||||||
close: () => void
|
close: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,6 +104,13 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
case ContextMenuType.View: return [
|
case ContextMenuType.View: return [
|
||||||
|
{
|
||||||
|
key: "section_1",
|
||||||
|
itemType: ContextualMenuItemType.Section,
|
||||||
|
sectionProps: {
|
||||||
|
title: "视图",
|
||||||
|
bottomDivider: true,
|
||||||
|
items: [
|
||||||
{
|
{
|
||||||
key: "cardView",
|
key: "cardView",
|
||||||
text: "卡片视图",
|
text: "卡片视图",
|
||||||
@ -115,6 +126,50 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
|
|||||||
canCheck: true,
|
canCheck: true,
|
||||||
checked: this.props.viewType === ViewType.List,
|
checked: this.props.viewType === ViewType.List,
|
||||||
onClick: () => this.props.switchView(ViewType.List)
|
onClick: () => this.props.switchView(ViewType.List)
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "section_2",
|
||||||
|
itemType: ContextualMenuItemType.Section,
|
||||||
|
sectionProps: {
|
||||||
|
title: "筛选",
|
||||||
|
bottomDivider: true,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: "allArticles",
|
||||||
|
text: "全部文章",
|
||||||
|
iconProps: { iconName: "ClearFilter" },
|
||||||
|
canCheck: true,
|
||||||
|
checked: (this.props.filter & ~FeedFilter.ShowHidden) == FeedFilter.Default,
|
||||||
|
onClick: () => this.props.switchFilter(FeedFilter.Default)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "unreadOnly",
|
||||||
|
text: "仅未读文章",
|
||||||
|
iconProps: { iconName: "RadioBtnOn", style: { fontSize: 14, textAlign: "center" } },
|
||||||
|
canCheck: true,
|
||||||
|
checked: (this.props.filter & ~FeedFilter.ShowHidden) == FeedFilter.UnreadOnly,
|
||||||
|
onClick: () => this.props.switchFilter(FeedFilter.UnreadOnly)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "starredOnly",
|
||||||
|
text: "仅星标文章",
|
||||||
|
iconProps: { iconName: "FavoriteStarFill" },
|
||||||
|
canCheck: true,
|
||||||
|
checked: (this.props.filter & ~FeedFilter.ShowHidden) == FeedFilter.StarredOnly,
|
||||||
|
onClick: () => this.props.switchFilter(FeedFilter.StarredOnly)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "showHidden",
|
||||||
|
text: "显示隐藏文章",
|
||||||
|
canCheck: true,
|
||||||
|
checked: Boolean(this.props.filter & FeedFilter.ShowHidden),
|
||||||
|
onClick: () => this.props.toggleFilter(FeedFilter.ShowHidden)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
default: return []
|
default: return []
|
||||||
|
@ -4,15 +4,17 @@ 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, toggleStarred, toggleHidden } 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, switchFilter, toggleFilter } from "../scripts/models/page"
|
||||||
import { setDefaultView } from "../scripts/utils"
|
import { setDefaultView } from "../scripts/utils"
|
||||||
|
import { FeedFilter } from "../scripts/models/feed"
|
||||||
|
|
||||||
const getContext = (state: RootState) => state.app.contextMenu
|
const getContext = (state: RootState) => state.app.contextMenu
|
||||||
const getViewType = (state: RootState) => state.page.viewType
|
const getViewType = (state: RootState) => state.page.viewType
|
||||||
|
const getFilter = (state: RootState) => state.page.filter
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
[getContext, getViewType],
|
[getContext, getViewType, getFilter],
|
||||||
(context, viewType) => {
|
(context, viewType, filter) => {
|
||||||
switch (context.type) {
|
switch (context.type) {
|
||||||
case ContextMenuType.Item: return {
|
case ContextMenuType.Item: return {
|
||||||
type: context.type,
|
type: context.type,
|
||||||
@ -28,7 +30,8 @@ const mapStateToProps = createSelector(
|
|||||||
case ContextMenuType.View: return {
|
case ContextMenuType.View: return {
|
||||||
type: context.type,
|
type: context.type,
|
||||||
event: context.event,
|
event: context.event,
|
||||||
viewType: viewType
|
viewType: viewType,
|
||||||
|
filter: filter
|
||||||
}
|
}
|
||||||
default: return { type: ContextMenuType.Hidden }
|
default: return { type: ContextMenuType.Hidden }
|
||||||
}
|
}
|
||||||
@ -46,6 +49,8 @@ const mapDispatchToProps = dispatch => {
|
|||||||
setDefaultView(viewType)
|
setDefaultView(viewType)
|
||||||
dispatch(switchView(viewType))
|
dispatch(switchView(viewType))
|
||||||
},
|
},
|
||||||
|
switchFilter: (filter: FeedFilter) => dispatch(switchFilter(filter)),
|
||||||
|
toggleFilter: (filter: FeedFilter) => dispatch(toggleFilter(filter)),
|
||||||
close: () => dispatch(closeContextMenu())
|
close: () => dispatch(closeContextMenu())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,6 @@ export const idb = new Datastore<RSSItem>({
|
|||||||
if (err) window.console.log(err)
|
if (err) window.console.log(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
idb.removeIndex("id")
|
//idb.removeIndex("id")
|
||||||
idb.update({}, {$unset: {id: true}}, {multi: true})
|
//idb.update({}, {$unset: {id: true}}, {multi: true})
|
||||||
//idb.remove({}, { multi: true })
|
//idb.remove({}, { multi: true })
|
@ -1,8 +1,40 @@
|
|||||||
import * as db from "../db"
|
import * as db from "../db"
|
||||||
import { SourceActionTypes, INIT_SOURCES, ADD_SOURCE, DELETE_SOURCE } from "./source"
|
import { SourceActionTypes, INIT_SOURCES, ADD_SOURCE, DELETE_SOURCE } from "./source"
|
||||||
import { ItemActionTypes, FETCH_ITEMS, RSSItem } from "./item"
|
import { ItemActionTypes, FETCH_ITEMS, RSSItem, MARK_READ, MARK_UNREAD, TOGGLE_STARRED, TOGGLE_HIDDEN, applyItemReduction } from "./item"
|
||||||
import { ActionStatus, AppThunk } from "../utils"
|
import { ActionStatus, AppThunk } from "../utils"
|
||||||
import { PageActionTypes, SELECT_PAGE, PageType } from "./page"
|
import { PageActionTypes, SELECT_PAGE, PageType, APPLY_FILTER } from "./page"
|
||||||
|
|
||||||
|
export enum FeedFilter {
|
||||||
|
None,
|
||||||
|
ShowRead = 1 << 0,
|
||||||
|
ShowNotStarred = 1 << 1,
|
||||||
|
ShowHidden = 1 << 2,
|
||||||
|
|
||||||
|
Default = ShowRead | ShowNotStarred,
|
||||||
|
UnreadOnly = ShowNotStarred,
|
||||||
|
StarredOnly = ShowRead
|
||||||
|
}
|
||||||
|
export namespace FeedFilter {
|
||||||
|
export function toQueryObject(filter: FeedFilter) {
|
||||||
|
let query = {
|
||||||
|
hasRead: false,
|
||||||
|
starred: true,
|
||||||
|
hidden: { $exists: false }
|
||||||
|
}
|
||||||
|
if (filter & FeedFilter.ShowRead) delete query.hasRead
|
||||||
|
if (filter & FeedFilter.ShowNotStarred) delete query.starred
|
||||||
|
if (filter & FeedFilter.ShowHidden) delete query.hidden
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
export function testItem(filter: FeedFilter, item: RSSItem) {
|
||||||
|
let flag = true
|
||||||
|
if (!(filter & FeedFilter.ShowRead)) flag = flag && !item.hasRead
|
||||||
|
if (!(filter & FeedFilter.ShowNotStarred)) flag = flag && item.starred
|
||||||
|
if (!(filter & FeedFilter.ShowHidden)) flag = flag && !item.hidden
|
||||||
|
return Boolean(flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const ALL = "ALL"
|
export const ALL = "ALL"
|
||||||
export const SOURCE = "SOURCE"
|
export const SOURCE = "SOURCE"
|
||||||
@ -16,18 +48,24 @@ export class RSSFeed {
|
|||||||
allLoaded: boolean
|
allLoaded: boolean
|
||||||
sids: number[]
|
sids: number[]
|
||||||
iids: string[]
|
iids: string[]
|
||||||
|
filter: FeedFilter
|
||||||
|
|
||||||
constructor (id: string = null, sids=[]) {
|
constructor (id: string = null, sids=[], filter=FeedFilter.Default) {
|
||||||
this._id = id
|
this._id = id
|
||||||
this.sids = sids
|
this.sids = sids
|
||||||
this.iids = []
|
this.iids = []
|
||||||
this.loaded = false
|
this.loaded = false
|
||||||
this.allLoaded = false
|
this.allLoaded = false
|
||||||
|
this.filter = filter
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadFeed(feed: RSSFeed, init = false): Promise<RSSItem[]> {
|
static loadFeed(feed: RSSFeed, init = false): Promise<RSSItem[]> {
|
||||||
return new Promise<RSSItem[]>((resolve, reject) => {
|
return new Promise<RSSItem[]>((resolve, reject) => {
|
||||||
db.idb.find({ source: { $in: feed.sids } })
|
let query = {
|
||||||
|
source: { $in: feed.sids },
|
||||||
|
...FeedFilter.toQueryObject(feed.filter)
|
||||||
|
}
|
||||||
|
db.idb.find(query)
|
||||||
.sort({ date: -1 })
|
.sort({ date: -1 })
|
||||||
.skip(init ? 0 : feed.iids.length)
|
.skip(init ? 0 : feed.iids.length)
|
||||||
.limit(LOAD_QUANTITY)
|
.limit(LOAD_QUANTITY)
|
||||||
@ -182,14 +220,24 @@ export function feedReducer(
|
|||||||
switch (action.status) {
|
switch (action.status) {
|
||||||
case ActionStatus.Success: return {
|
case ActionStatus.Success: return {
|
||||||
...state,
|
...state,
|
||||||
[ALL]: new RSSFeed(ALL, [...state[ALL].sids, action.source.sid])
|
[ALL]: new RSSFeed(ALL, [...state[ALL].sids, action.source.sid], state[ALL].filter)
|
||||||
}
|
}
|
||||||
default: return state
|
default: return state
|
||||||
}
|
}
|
||||||
case DELETE_SOURCE: {
|
case DELETE_SOURCE: {
|
||||||
let nextState = {}
|
let nextState = {}
|
||||||
for (let [id, feed] of Object.entries(state)) {
|
for (let [id, feed] of Object.entries(state)) {
|
||||||
nextState[id] = new RSSFeed(id, feed.sids.filter(sid => sid != action.source.sid))
|
nextState[id] = new RSSFeed(id, feed.sids.filter(sid => sid != action.source.sid), feed.filter)
|
||||||
|
}
|
||||||
|
return nextState
|
||||||
|
}
|
||||||
|
case APPLY_FILTER: {
|
||||||
|
let nextState = {}
|
||||||
|
for (let [id, feed] of Object.entries(state)) {
|
||||||
|
nextState[id] = {
|
||||||
|
...feed,
|
||||||
|
filter: action.filter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nextState
|
return nextState
|
||||||
}
|
}
|
||||||
@ -197,13 +245,15 @@ export function feedReducer(
|
|||||||
switch (action.status) {
|
switch (action.status) {
|
||||||
case ActionStatus.Success: {
|
case ActionStatus.Success: {
|
||||||
let nextState = { ...state }
|
let nextState = { ...state }
|
||||||
for (let k of Object.keys(state)) {
|
for (let feed of Object.values(state)) {
|
||||||
if (state[k].loaded) {
|
if (feed.loaded) {
|
||||||
let iids = action.items.filter(i => state[k].sids.includes(i.source)).map(i => i._id)
|
let iids = action.items
|
||||||
|
.filter(i => feed.sids.includes(i.source) && FeedFilter.testItem(feed.filter, i))
|
||||||
|
.map(i => i._id)
|
||||||
if (iids.length > 0) {
|
if (iids.length > 0) {
|
||||||
nextState[k] = {
|
nextState[feed._id] = {
|
||||||
...nextState[k],
|
...feed,
|
||||||
iids: [...iids, ...nextState[k].iids]
|
iids: [...iids, ...feed.iids]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,17 +302,37 @@ export function feedReducer(
|
|||||||
}
|
}
|
||||||
default: return state
|
default: return state
|
||||||
}
|
}
|
||||||
|
case MARK_READ:
|
||||||
|
case MARK_UNREAD:
|
||||||
|
case TOGGLE_STARRED:
|
||||||
|
case TOGGLE_HIDDEN: {
|
||||||
|
let nextItem = applyItemReduction(action.item, action.type)
|
||||||
|
let filteredFeeds = Object.values(state).filter(feed => feed.loaded && !FeedFilter.testItem(feed.filter, nextItem))
|
||||||
|
if (filteredFeeds.length > 0) {
|
||||||
|
let nextState = { ...state }
|
||||||
|
for (let feed of filteredFeeds) {
|
||||||
|
nextState[feed._id] = {
|
||||||
|
...feed,
|
||||||
|
iids: feed.iids.filter(id => id != nextItem._id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nextState
|
||||||
|
} else {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
case SELECT_PAGE:
|
case SELECT_PAGE:
|
||||||
switch (action.pageType) {
|
switch (action.pageType) {
|
||||||
case PageType.Sources: return {
|
case PageType.Sources: return {
|
||||||
...state,
|
...state,
|
||||||
[SOURCE]: new RSSFeed(SOURCE, action.sids)
|
[SOURCE]: new RSSFeed(SOURCE, action.sids, action.filter)
|
||||||
}
|
}
|
||||||
case PageType.AllArticles: return action.init ? {
|
case PageType.AllArticles: return action.init ? {
|
||||||
...state,
|
...state,
|
||||||
[ALL]: {
|
[ALL]: {
|
||||||
...state[ALL],
|
...state[ALL],
|
||||||
loaded: false
|
loaded: false,
|
||||||
|
filter: action.filter
|
||||||
}
|
}
|
||||||
} : state
|
} : state
|
||||||
default: return state
|
default: return state
|
||||||
|
@ -225,6 +225,28 @@ export function toggleHidden(item: RSSItem): AppThunk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function applyItemReduction(item: RSSItem, type: string) {
|
||||||
|
let nextItem = { ...item }
|
||||||
|
switch (type) {
|
||||||
|
case MARK_READ:
|
||||||
|
case MARK_UNREAD: {
|
||||||
|
nextItem.hasRead = type === MARK_READ
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case TOGGLE_STARRED: {
|
||||||
|
if (item.starred === true) delete nextItem.starred
|
||||||
|
else nextItem.starred = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case TOGGLE_HIDDEN: {
|
||||||
|
if (item.hidden === true) delete nextItem.hidden
|
||||||
|
else nextItem.hidden = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nextItem
|
||||||
|
}
|
||||||
|
|
||||||
export function itemReducer(
|
export function itemReducer(
|
||||||
state: ItemState = {},
|
state: ItemState = {},
|
||||||
action: ItemActionTypes | FeedActionTypes
|
action: ItemActionTypes | FeedActionTypes
|
||||||
@ -242,29 +264,12 @@ export function itemReducer(
|
|||||||
default: return state
|
default: return state
|
||||||
}
|
}
|
||||||
case MARK_UNREAD:
|
case MARK_UNREAD:
|
||||||
case MARK_READ: return {
|
case MARK_READ:
|
||||||
...state,
|
case TOGGLE_STARRED:
|
||||||
[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: {
|
case TOGGLE_HIDDEN: {
|
||||||
let newItem = { ...action.item }
|
|
||||||
if (newItem.hidden === true) delete newItem.hidden
|
|
||||||
else newItem.hidden = true
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[newItem._id]: newItem
|
[action.item._id]: applyItemReduction(action.item, action.type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case LOAD_MORE:
|
case LOAD_MORE:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ALL, SOURCE, loadMore } from "./feed"
|
import { ALL, SOURCE, loadMore, FeedFilter, initFeeds } 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"
|
||||||
@ -8,6 +8,7 @@ export const SWITCH_VIEW = "SWITCH_VIEW"
|
|||||||
export const SHOW_ITEM = "SHOW_ITEM"
|
export const SHOW_ITEM = "SHOW_ITEM"
|
||||||
export const SHOW_OFFSET_ITEM = "SHOW_OFFSET_ITEM"
|
export const SHOW_OFFSET_ITEM = "SHOW_OFFSET_ITEM"
|
||||||
export const DISMISS_ITEM = "DISMISS_ITEM"
|
export const DISMISS_ITEM = "DISMISS_ITEM"
|
||||||
|
export const APPLY_FILTER = "APPLY_FILTER"
|
||||||
|
|
||||||
export enum PageType {
|
export enum PageType {
|
||||||
AllArticles, Sources, Page
|
AllArticles, Sources, Page
|
||||||
@ -22,6 +23,7 @@ interface SelectPageAction {
|
|||||||
pageType: PageType
|
pageType: PageType
|
||||||
init: boolean
|
init: boolean
|
||||||
keepMenu: boolean
|
keepMenu: boolean
|
||||||
|
filter: FeedFilter
|
||||||
sids?: number[]
|
sids?: number[]
|
||||||
menuKey?: string
|
menuKey?: string
|
||||||
title?: string
|
title?: string
|
||||||
@ -38,28 +40,39 @@ interface ShowItemAction {
|
|||||||
item: RSSItem
|
item: RSSItem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ApplyFilterAction {
|
||||||
|
type: typeof APPLY_FILTER
|
||||||
|
filter: FeedFilter
|
||||||
|
}
|
||||||
|
|
||||||
interface DismissItemAction { type: typeof DISMISS_ITEM }
|
interface DismissItemAction { type: typeof DISMISS_ITEM }
|
||||||
|
|
||||||
export type PageActionTypes = SelectPageAction | SwitchViewAction | ShowItemAction | DismissItemAction
|
export type PageActionTypes = SelectPageAction | SwitchViewAction | ShowItemAction | DismissItemAction | ApplyFilterAction
|
||||||
|
|
||||||
export function selectAllArticles(init = false): PageActionTypes {
|
export function selectAllArticles(init = false): AppThunk {
|
||||||
return {
|
return (dispatch, getState) => {
|
||||||
|
dispatch({
|
||||||
type: SELECT_PAGE,
|
type: SELECT_PAGE,
|
||||||
keepMenu: getWindowBreakpoint(),
|
keepMenu: getWindowBreakpoint(),
|
||||||
|
filter: getState().page.filter,
|
||||||
pageType: PageType.AllArticles,
|
pageType: PageType.AllArticles,
|
||||||
init: init
|
init: init
|
||||||
|
} as PageActionTypes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectSources(sids: number[], menuKey: string, title: string): PageActionTypes {
|
export function selectSources(sids: number[], menuKey: string, title: string): AppThunk {
|
||||||
return {
|
return (dispatch, getState) => {
|
||||||
|
dispatch({
|
||||||
type: SELECT_PAGE,
|
type: SELECT_PAGE,
|
||||||
pageType: PageType.Sources,
|
pageType: PageType.Sources,
|
||||||
keepMenu: getWindowBreakpoint(),
|
keepMenu: getWindowBreakpoint(),
|
||||||
|
filter: getState().page.filter,
|
||||||
sids: sids,
|
sids: sids,
|
||||||
menuKey: menuKey,
|
menuKey: menuKey,
|
||||||
title: title,
|
title: title,
|
||||||
init: true
|
init: true
|
||||||
|
} as PageActionTypes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +101,22 @@ export function showOffsetItem(offset: number): AppThunk {
|
|||||||
let iids = feed.iids
|
let iids = feed.iids
|
||||||
let itemIndex = iids.indexOf(itemId)
|
let itemIndex = iids.indexOf(itemId)
|
||||||
let newIndex = itemIndex + offset
|
let newIndex = itemIndex + offset
|
||||||
if (itemIndex >= 0 && newIndex >= 0) {
|
if (itemIndex < 0) {
|
||||||
|
let item = state.items[itemId]
|
||||||
|
let prevs = feed.iids
|
||||||
|
.map((id, index) => [state.items[id], index] as [RSSItem, number])
|
||||||
|
.filter(([i, _]) => i.date > item.date)
|
||||||
|
if (prevs.length > 0) {
|
||||||
|
let prev = prevs[0]
|
||||||
|
for (let j = 1; j < prevs.length; j += 1) {
|
||||||
|
if (prevs[j][0].date < prev[0].date) prev = prevs[j]
|
||||||
|
}
|
||||||
|
newIndex = prev[1] + offset + (offset < 0 ? 1 : 0)
|
||||||
|
} else {
|
||||||
|
newIndex = offset - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newIndex >= 0) {
|
||||||
if (newIndex < iids.length) {
|
if (newIndex < iids.length) {
|
||||||
let item = state.items[iids[newIndex]]
|
let item = state.items[iids[newIndex]]
|
||||||
dispatch(markRead(item))
|
dispatch(markRead(item))
|
||||||
@ -107,8 +135,38 @@ export function showOffsetItem(offset: number): AppThunk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const applyFilterDone = (filter: FeedFilter): PageActionTypes => ({
|
||||||
|
type: APPLY_FILTER,
|
||||||
|
filter: filter
|
||||||
|
})
|
||||||
|
|
||||||
|
function applyFilter(filter: FeedFilter): AppThunk {
|
||||||
|
return (dispatch) => {
|
||||||
|
dispatch(applyFilterDone(filter))
|
||||||
|
dispatch(initFeeds(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function switchFilter(filter: FeedFilter): AppThunk {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
let oldFilter = getState().page.filter
|
||||||
|
let newFilter = filter | (oldFilter & FeedFilter.ShowHidden)
|
||||||
|
if (newFilter != oldFilter) {
|
||||||
|
dispatch(applyFilter(newFilter))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleFilter(filter: FeedFilter): AppThunk {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
let oldFilter = getState().page.filter
|
||||||
|
dispatch(applyFilter(oldFilter ^ filter))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class PageState {
|
export class PageState {
|
||||||
viewType = getDefaultView()
|
viewType = getDefaultView()
|
||||||
|
filter = FeedFilter.Default
|
||||||
feedId = ALL
|
feedId = ALL
|
||||||
itemId = null as string
|
itemId = null as string
|
||||||
}
|
}
|
||||||
@ -135,6 +193,10 @@ export function pageReducer(
|
|||||||
viewType: action.viewType,
|
viewType: action.viewType,
|
||||||
itemId: action.viewType === ViewType.List ? state.itemId : null
|
itemId: action.viewType === ViewType.List ? state.itemId : null
|
||||||
}
|
}
|
||||||
|
case APPLY_FILTER: return {
|
||||||
|
...state,
|
||||||
|
filter: action.filter
|
||||||
|
}
|
||||||
case SHOW_ITEM: return {
|
case SHOW_ITEM: return {
|
||||||
...state,
|
...state,
|
||||||
itemId: action.item._id
|
itemId: action.item._id
|
||||||
|
@ -2,7 +2,6 @@ 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
|
||||||
@ -50,7 +49,6 @@ 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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user