mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-04-27 16:38:42 +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;
|
||||
width: 120%;
|
||||
height: 120%;
|
||||
box-shadow: inset 5px 0 20px #0004;
|
||||
box-shadow: inset 5px 0 25px #0004;
|
||||
}
|
||||
.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 {
|
||||
@ -403,7 +403,7 @@ img.favicon {
|
||||
flex-wrap: wrap;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
top: -32px;
|
||||
margin-top: -32px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
@ -422,7 +422,7 @@ img.favicon {
|
||||
right: 0;
|
||||
width: 120%;
|
||||
height: 120%;
|
||||
box-shadow: inset 5px 0 20px #0004;
|
||||
box-shadow: inset 5px 0 25px #0004;
|
||||
}
|
||||
.list-feed {
|
||||
margin-top: 32px;
|
||||
@ -476,11 +476,12 @@ img.favicon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.read-indicator {
|
||||
.read-indicator, .starred-indicator {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
float: right;
|
||||
text-align: center;
|
||||
}
|
||||
.read-indicator::after {
|
||||
content: "";
|
||||
@ -494,6 +495,13 @@ img.favicon {
|
||||
font-size: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.starred-indicator::after {
|
||||
content: "★";
|
||||
vertical-align: top;
|
||||
color: #ffaa44;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: inline-block;
|
||||
@ -574,6 +582,16 @@ img.favicon {
|
||||
.card p.snippet.show {
|
||||
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 {
|
||||
display: flex;
|
||||
|
@ -1,12 +1,19 @@
|
||||
import * as React from "react"
|
||||
import { Card } from "./card"
|
||||
import Time from "../utils/time"
|
||||
import { AnimationClassNames } from "@fluentui/react"
|
||||
import CardInfo from "./info"
|
||||
|
||||
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() {
|
||||
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} >
|
||||
{this.props.item.thumb ? (
|
||||
<img className="bg" src={this.props.item.thumb} />
|
||||
@ -15,12 +22,7 @@ class DefaultCard extends Card {
|
||||
{this.props.item.thumb ? (
|
||||
<img className="head" src={this.props.item.thumb} />
|
||||
) : null}
|
||||
<p className="info">
|
||||
{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>
|
||||
<CardInfo source={this.props.source} item={this.props.item} />
|
||||
<h3 className="title">{this.props.item.title}</h3>
|
||||
<p className={"snippet"+(this.props.item.thumb?"":" show")}>{this.props.item.snippet}</p>
|
||||
</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 { Card } from "./card"
|
||||
import Time from "../utils/time"
|
||||
import { AnimationClassNames } from "@fluentui/react"
|
||||
import CardInfo from "./info"
|
||||
|
||||
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() {
|
||||
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} >
|
||||
{this.props.item.thumb ? (
|
||||
<div className="head"><img src={this.props.item.thumb} /></div>
|
||||
) : null}
|
||||
<div className="data">
|
||||
<p className="info">
|
||||
{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>
|
||||
<CardInfo source={this.props.source} item={this.props.item} />
|
||||
<h3 className="title">{this.props.item.title}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -6,6 +6,7 @@ import { ContextMenuType } from "../scripts/models/app"
|
||||
import { RSSItem } from "../scripts/models/item"
|
||||
import { ContextReduxProps } from "../containers/context-menu-container"
|
||||
import { ViewType } from "../scripts/models/page"
|
||||
import { FeedFilter } from "../scripts/models/feed"
|
||||
|
||||
export type ContextMenuProps = ContextReduxProps & {
|
||||
type: ContextMenuType
|
||||
@ -14,13 +15,16 @@ export type ContextMenuProps = ContextReduxProps & {
|
||||
item?: RSSItem
|
||||
feedId?: string
|
||||
text?: string
|
||||
viewType: ViewType
|
||||
viewType?: ViewType
|
||||
filter?: FeedFilter
|
||||
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
|
||||
switchFilter: (filter: FeedFilter) => void
|
||||
toggleFilter: (filter: FeedFilter) => void
|
||||
close: () => void
|
||||
}
|
||||
|
||||
@ -101,20 +105,71 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
|
||||
]
|
||||
case ContextMenuType.View: return [
|
||||
{
|
||||
key: "cardView",
|
||||
text: "卡片视图",
|
||||
iconProps: { iconName: "GridViewMedium" },
|
||||
canCheck: true,
|
||||
checked: this.props.viewType === ViewType.Cards,
|
||||
onClick: () => this.props.switchView(ViewType.Cards)
|
||||
key: "section_1",
|
||||
itemType: ContextualMenuItemType.Section,
|
||||
sectionProps: {
|
||||
title: "视图",
|
||||
bottomDivider: true,
|
||||
items: [
|
||||
{
|
||||
key: "cardView",
|
||||
text: "卡片视图",
|
||||
iconProps: { iconName: "GridViewMedium" },
|
||||
canCheck: true,
|
||||
checked: this.props.viewType === ViewType.Cards,
|
||||
onClick: () => this.props.switchView(ViewType.Cards)
|
||||
},
|
||||
{
|
||||
key: "listView",
|
||||
text: "列表视图",
|
||||
iconProps: { iconName: "BacklogList" },
|
||||
canCheck: true,
|
||||
checked: this.props.viewType === ViewType.List,
|
||||
onClick: () => this.props.switchView(ViewType.List)
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "listView",
|
||||
text: "列表视图",
|
||||
iconProps: { iconName: "BacklogList" },
|
||||
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: this.props.viewType === ViewType.List,
|
||||
onClick: () => this.props.switchView(ViewType.List)
|
||||
checked: Boolean(this.props.filter & FeedFilter.ShowHidden),
|
||||
onClick: () => this.props.toggleFilter(FeedFilter.ShowHidden)
|
||||
}
|
||||
]
|
||||
default: return []
|
||||
|
@ -4,15 +4,17 @@ import { RootState } from "../scripts/reducer"
|
||||
import { ContextMenuType, closeContextMenu } from "../scripts/models/app"
|
||||
import { ContextMenu } from "../components/context-menu"
|
||||
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 { FeedFilter } from "../scripts/models/feed"
|
||||
|
||||
const getContext = (state: RootState) => state.app.contextMenu
|
||||
const getViewType = (state: RootState) => state.page.viewType
|
||||
const getFilter = (state: RootState) => state.page.filter
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
[getContext, getViewType],
|
||||
(context, viewType) => {
|
||||
[getContext, getViewType, getFilter],
|
||||
(context, viewType, filter) => {
|
||||
switch (context.type) {
|
||||
case ContextMenuType.Item: return {
|
||||
type: context.type,
|
||||
@ -28,7 +30,8 @@ const mapStateToProps = createSelector(
|
||||
case ContextMenuType.View: return {
|
||||
type: context.type,
|
||||
event: context.event,
|
||||
viewType: viewType
|
||||
viewType: viewType,
|
||||
filter: filter
|
||||
}
|
||||
default: return { type: ContextMenuType.Hidden }
|
||||
}
|
||||
@ -46,6 +49,8 @@ const mapDispatchToProps = dispatch => {
|
||||
setDefaultView(viewType)
|
||||
dispatch(switchView(viewType))
|
||||
},
|
||||
switchFilter: (filter: FeedFilter) => dispatch(switchFilter(filter)),
|
||||
toggleFilter: (filter: FeedFilter) => dispatch(toggleFilter(filter)),
|
||||
close: () => dispatch(closeContextMenu())
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,6 @@ export const idb = new Datastore<RSSItem>({
|
||||
if (err) window.console.log(err)
|
||||
}
|
||||
})
|
||||
idb.removeIndex("id")
|
||||
idb.update({}, {$unset: {id: true}}, {multi: true})
|
||||
//idb.removeIndex("id")
|
||||
//idb.update({}, {$unset: {id: true}}, {multi: true})
|
||||
//idb.remove({}, { multi: true })
|
@ -1,8 +1,40 @@
|
||||
import * as db from "../db"
|
||||
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 { 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 SOURCE = "SOURCE"
|
||||
@ -16,18 +48,24 @@ export class RSSFeed {
|
||||
allLoaded: boolean
|
||||
sids: number[]
|
||||
iids: string[]
|
||||
filter: FeedFilter
|
||||
|
||||
constructor (id: string = null, sids=[]) {
|
||||
constructor (id: string = null, sids=[], filter=FeedFilter.Default) {
|
||||
this._id = id
|
||||
this.sids = sids
|
||||
this.iids = []
|
||||
this.loaded = false
|
||||
this.allLoaded = false
|
||||
this.filter = filter
|
||||
}
|
||||
|
||||
static loadFeed(feed: RSSFeed, init = false): Promise<RSSItem[]> {
|
||||
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 })
|
||||
.skip(init ? 0 : feed.iids.length)
|
||||
.limit(LOAD_QUANTITY)
|
||||
@ -182,14 +220,24 @@ export function feedReducer(
|
||||
switch (action.status) {
|
||||
case ActionStatus.Success: return {
|
||||
...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
|
||||
}
|
||||
case DELETE_SOURCE: {
|
||||
let nextState = {}
|
||||
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
|
||||
}
|
||||
@ -197,13 +245,15 @@ export function feedReducer(
|
||||
switch (action.status) {
|
||||
case ActionStatus.Success: {
|
||||
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)
|
||||
for (let feed of Object.values(state)) {
|
||||
if (feed.loaded) {
|
||||
let iids = action.items
|
||||
.filter(i => feed.sids.includes(i.source) && FeedFilter.testItem(feed.filter, i))
|
||||
.map(i => i._id)
|
||||
if (iids.length > 0) {
|
||||
nextState[k] = {
|
||||
...nextState[k],
|
||||
iids: [...iids, ...nextState[k].iids]
|
||||
nextState[feed._id] = {
|
||||
...feed,
|
||||
iids: [...iids, ...feed.iids]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -252,17 +302,37 @@ export function feedReducer(
|
||||
}
|
||||
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:
|
||||
switch (action.pageType) {
|
||||
case PageType.Sources: return {
|
||||
...state,
|
||||
[SOURCE]: new RSSFeed(SOURCE, action.sids)
|
||||
[SOURCE]: new RSSFeed(SOURCE, action.sids, action.filter)
|
||||
}
|
||||
case PageType.AllArticles: return action.init ? {
|
||||
...state,
|
||||
[ALL]: {
|
||||
...state[ALL],
|
||||
loaded: false
|
||||
loaded: false,
|
||||
filter: action.filter
|
||||
}
|
||||
} : 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(
|
||||
state: ItemState = {},
|
||||
action: ItemActionTypes | FeedActionTypes
|
||||
@ -242,29 +264,12 @@ export function itemReducer(
|
||||
default: return state
|
||||
}
|
||||
case MARK_UNREAD:
|
||||
case MARK_READ: return {
|
||||
...state,
|
||||
[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 MARK_READ:
|
||||
case TOGGLE_STARRED:
|
||||
case TOGGLE_HIDDEN: {
|
||||
let newItem = { ...action.item }
|
||||
if (newItem.hidden === true) delete newItem.hidden
|
||||
else newItem.hidden = true
|
||||
return {
|
||||
...state,
|
||||
[newItem._id]: newItem
|
||||
[action.item._id]: applyItemReduction(action.item, action.type)
|
||||
}
|
||||
}
|
||||
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 { RSSItem, markRead } from "./item"
|
||||
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_OFFSET_ITEM = "SHOW_OFFSET_ITEM"
|
||||
export const DISMISS_ITEM = "DISMISS_ITEM"
|
||||
export const APPLY_FILTER = "APPLY_FILTER"
|
||||
|
||||
export enum PageType {
|
||||
AllArticles, Sources, Page
|
||||
@ -22,6 +23,7 @@ interface SelectPageAction {
|
||||
pageType: PageType
|
||||
init: boolean
|
||||
keepMenu: boolean
|
||||
filter: FeedFilter
|
||||
sids?: number[]
|
||||
menuKey?: string
|
||||
title?: string
|
||||
@ -38,28 +40,39 @@ interface ShowItemAction {
|
||||
item: RSSItem
|
||||
}
|
||||
|
||||
interface ApplyFilterAction {
|
||||
type: typeof APPLY_FILTER
|
||||
filter: FeedFilter
|
||||
}
|
||||
|
||||
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 {
|
||||
return {
|
||||
type: SELECT_PAGE,
|
||||
keepMenu: getWindowBreakpoint(),
|
||||
pageType: PageType.AllArticles,
|
||||
init: init
|
||||
export function selectAllArticles(init = false): AppThunk {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: SELECT_PAGE,
|
||||
keepMenu: getWindowBreakpoint(),
|
||||
filter: getState().page.filter,
|
||||
pageType: PageType.AllArticles,
|
||||
init: init
|
||||
} as PageActionTypes)
|
||||
}
|
||||
}
|
||||
|
||||
export function selectSources(sids: number[], menuKey: string, title: string): PageActionTypes {
|
||||
return {
|
||||
type: SELECT_PAGE,
|
||||
pageType: PageType.Sources,
|
||||
keepMenu: getWindowBreakpoint(),
|
||||
sids: sids,
|
||||
menuKey: menuKey,
|
||||
title: title,
|
||||
init: true
|
||||
export function selectSources(sids: number[], menuKey: string, title: string): AppThunk {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: SELECT_PAGE,
|
||||
pageType: PageType.Sources,
|
||||
keepMenu: getWindowBreakpoint(),
|
||||
filter: getState().page.filter,
|
||||
sids: sids,
|
||||
menuKey: menuKey,
|
||||
title: title,
|
||||
init: true
|
||||
} as PageActionTypes)
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,7 +101,22 @@ export function showOffsetItem(offset: number): AppThunk {
|
||||
let iids = feed.iids
|
||||
let itemIndex = iids.indexOf(itemId)
|
||||
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) {
|
||||
let item = state.items[iids[newIndex]]
|
||||
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 {
|
||||
viewType = getDefaultView()
|
||||
filter = FeedFilter.Default
|
||||
feedId = ALL
|
||||
itemId = null as string
|
||||
}
|
||||
@ -135,6 +193,10 @@ export function pageReducer(
|
||||
viewType: action.viewType,
|
||||
itemId: action.viewType === ViewType.List ? state.itemId : null
|
||||
}
|
||||
case APPLY_FILTER: return {
|
||||
...state,
|
||||
filter: action.filter
|
||||
}
|
||||
case SHOW_ITEM: return {
|
||||
...state,
|
||||
itemId: action.item._id
|
||||
|
@ -2,7 +2,6 @@ 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
|
||||
@ -50,7 +49,6 @@ 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