2020-08-24 13:32:34 +08:00

302 lines
8.9 KiB
TypeScript

import { ALL, SOURCE, loadMore, FeedFilter, FilterType, initFeeds, FeedActionTypes, INIT_FEED } from "./feed"
import { getWindowBreakpoint, AppThunk, ActionStatus } from "../utils"
import { RSSItem, markRead } from "./item"
import { SourceActionTypes, DELETE_SOURCE } from "./source"
import { toggleMenu } from "./app"
import { ViewType, ViewConfigs } from "../../schema-types"
export const SELECT_PAGE = "SELECT_PAGE"
export const SWITCH_VIEW = "SWITCH_VIEW"
export const SET_VIEW_CONFIGS = "SET_VIEW_CONFIGS"
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 const TOGGLE_SEARCH = "TOGGLE_SEARCH"
export enum PageType {
AllArticles, Sources, Page
}
interface SelectPageAction {
type: typeof SELECT_PAGE
pageType: PageType
init: boolean
keepMenu: boolean
filter: FeedFilter
sids?: number[]
menuKey?: string
title?: string
}
interface SwitchViewAction {
type: typeof SWITCH_VIEW
viewType: ViewType
}
interface SetViewConfigsAction {
type: typeof SET_VIEW_CONFIGS
configs: ViewConfigs
}
interface ShowItemAction {
type: typeof SHOW_ITEM
feedId: string
item: RSSItem
}
interface ApplyFilterAction {
type: typeof APPLY_FILTER
filter: FeedFilter
}
interface DismissItemAction { type: typeof DISMISS_ITEM }
interface ToggleSearchAction { type: typeof TOGGLE_SEARCH }
export type PageActionTypes = SelectPageAction | SwitchViewAction | ShowItemAction
| DismissItemAction | ApplyFilterAction | ToggleSearchAction | SetViewConfigsAction
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): AppThunk {
return (dispatch, getState) => {
if (getState().app.menuKey !== menuKey) {
dispatch({
type: SELECT_PAGE,
pageType: PageType.Sources,
keepMenu: getWindowBreakpoint(),
filter: getState().page.filter,
sids: sids,
menuKey: menuKey,
title: title,
init: true
} as PageActionTypes)
}
}
}
export function switchView(viewType: ViewType): PageActionTypes {
return {
type: SWITCH_VIEW,
viewType: viewType
}
}
export function setViewConfigs(configs: ViewConfigs): AppThunk {
return (dispatch, getState) => {
window.settings.setViewConfigs(getState().page.viewType, configs)
dispatch({
type: "SET_VIEW_CONFIGS",
configs: configs
})
}
}
export function showItem(feedId: string, item: RSSItem): AppThunk {
return (dispatch, getState) => {
const state = getState()
if (state.items.hasOwnProperty(item._id) && state.sources.hasOwnProperty(item.source)) {
dispatch({
type: SHOW_ITEM,
feedId: feedId,
item: item
})
}
}
}
export function showItemFromId(iid: string): AppThunk {
return (dispatch, getState) => {
const state = getState()
const item = state.items[iid]
if (!item.hasRead) dispatch(markRead(item))
if (item) dispatch(showItem(null, item))
}
}
export const dismissItem = (): PageActionTypes => ({ type: DISMISS_ITEM })
export const toggleSearch = (): AppThunk => {
return (dispatch, getState) => {
let state = getState()
dispatch(({ type: TOGGLE_SEARCH }))
if (!getWindowBreakpoint() && state.app.menu) {
dispatch(toggleMenu())
}
if (state.page.searchOn) {
dispatch(applyFilter({
...state.page.filter,
search: ""
}))
}
}
}
export function showOffsetItem(offset: number): AppThunk {
return (dispatch, getState) => {
let state = getState()
if (!state.page.itemFromFeed) return
let [itemId, feedId] = [state.page.itemId, state.page.feedId]
let feed = state.feeds[feedId]
let iids = feed.iids
let itemIndex = iids.indexOf(itemId)
let newIndex = itemIndex + offset
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))
dispatch(showItem(feedId, item))
return
} else if (!feed.allLoaded){
dispatch(loadMore(feed)).then(() => {
dispatch(showOffsetItem(offset))
}).catch(() =>
dispatch(dismissItem())
)
return
}
}
dispatch(dismissItem())
}
}
const applyFilterDone = (filter: FeedFilter): PageActionTypes => ({
type: APPLY_FILTER,
filter: filter
})
function applyFilter(filter: FeedFilter): AppThunk {
return (dispatch, getState) => {
const oldFilterType = getState().page.filter.type
if (filter.type !== oldFilterType) window.settings.setFilterType(filter.type)
dispatch(applyFilterDone(filter))
dispatch(initFeeds(true))
}
}
export function switchFilter(filter: FilterType): AppThunk {
return (dispatch, getState) => {
let oldFilter = getState().page.filter
let oldType = oldFilter.type
let newType = filter | (oldType & FilterType.Toggles)
if (oldType != newType) {
dispatch(applyFilter({
...oldFilter,
type: newType
}))
}
}
}
export function toggleFilter(filter: FilterType): AppThunk {
return (dispatch, getState) => {
let nextFilter = { ...getState().page.filter }
nextFilter.type ^= filter
dispatch(applyFilter(nextFilter))
}
}
export function performSearch(query: string): AppThunk {
return (dispatch, getState) => {
let state = getState()
if (state.page.searchOn) {
dispatch(applyFilter({
...state.page.filter,
search: query
}))
}
}
}
export class PageState {
viewType = window.settings.getDefaultView()
viewConfigs = window.settings.getViewConfigs(window.settings.getDefaultView())
filter = new FeedFilter()
feedId = ALL
itemId = null as string
itemFromFeed = true
searchOn = false
}
export function pageReducer(
state = new PageState(),
action: PageActionTypes | SourceActionTypes | FeedActionTypes
): PageState {
switch (action.type) {
case SELECT_PAGE:
switch (action.pageType) {
case PageType.AllArticles: return {
...state,
feedId: ALL,
itemId: null
}
case PageType.Sources: return {
...state,
feedId: SOURCE,
itemId: null
}
default: return state
}
case SWITCH_VIEW: return {
...state,
viewType: action.viewType,
viewConfigs: window.settings.getViewConfigs(action.viewType),
itemId: null
}
case SET_VIEW_CONFIGS: return {
...state,
viewConfigs: action.configs
}
case APPLY_FILTER: return {
...state,
filter: action.filter
}
case SHOW_ITEM: return {
...state,
itemId: action.item._id,
itemFromFeed: Boolean(action.feedId)
}
case INIT_FEED: switch (action.status) {
case ActionStatus.Success: return {
...state,
itemId: (action.feed._id === state.feedId && action.items.filter(i => i._id === state.itemId).length === 0)
? null : state.itemId
}
default: return state
}
case DELETE_SOURCE:
case DISMISS_ITEM: return {
...state,
itemId: null
}
case TOGGLE_SEARCH: return {
...state,
searchOn: !state.searchOn
}
default: return state
}
}