article filtering

This commit is contained in:
刘浩远 2020-06-10 17:06:10 +08:00
parent 2ff5e13219
commit a9c64cbe78
11 changed files with 331 additions and 93 deletions

28
dist/styles.css vendored
View File

@ -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;

View File

@ -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>

View 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

View File

@ -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>

View File

@ -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 []

View File

@ -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())
} }
} }

View File

@ -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 })

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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,