add sources through UI

This commit is contained in:
Bruce Liu 2020-05-31 18:24:43 +08:00
parent b308df349c
commit 324aaee5f2
6 changed files with 107 additions and 66 deletions

View File

@ -5,7 +5,8 @@ import { SourceState, RSSSource } from "../../scripts/models/source"
import { urlTest } from "../../scripts/utils" import { urlTest } from "../../scripts/utils"
type SourcesTabProps = SourcesTabReduxProps & { type SourcesTabProps = SourcesTabReduxProps & {
sources: SourceState sources: SourceState,
addSource: (url: string) => void
} }
type SourcesTabState = { type SourcesTabState = {
@ -79,7 +80,10 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
onChange={this.handleInputChange} /> onChange={this.handleInputChange} />
</Stack.Item> </Stack.Item>
<Stack.Item> <Stack.Item>
<PrimaryButton disabled={!urlTest(this.state.newUrl)} text="添加" /> <PrimaryButton
disabled={!urlTest(this.state.newUrl)}
onClick={() => this.props.addSource(this.state.newUrl)}
text="添加" />
</Stack.Item> </Stack.Item>
</Stack> </Stack>

View File

@ -2,6 +2,7 @@ import { connect } from "react-redux"
import { createSelector } from "reselect" import { createSelector } from "reselect"
import { RootState } from "../../scripts/reducer" import { RootState } from "../../scripts/reducer"
import SourcesTab from "../../components/settings/sources" import SourcesTab from "../../components/settings/sources"
import { addSource } from "../../scripts/models/source"
const getSources = (state: RootState) => state.sources const getSources = (state: RootState) => state.sources
@ -14,7 +15,7 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
addSource: (url: string) => dispatch(addSource(url))
} }
} }

View File

@ -1,4 +1,4 @@
import { RSSSource, INIT_SOURCES, SourceActionTypes } from "./source" import { RSSSource, INIT_SOURCES, SourceActionTypes, ADD_SOURCE } from "./source"
import { RSSItem, ItemActionTypes, FETCH_ITEMS, fetchItems } from "./item" import { RSSItem, ItemActionTypes, FETCH_ITEMS, fetchItems } from "./item"
import { ActionStatus, AppThunk } from "../utils" import { ActionStatus, AppThunk } from "../utils"
import { INIT_FEEDS, FeedActionTypes, ALL, initFeeds } from "./feed" import { INIT_FEEDS, FeedActionTypes, ALL, initFeeds } from "./feed"
@ -117,11 +117,9 @@ export function exitSettings(): AppThunk {
if (getState().app.settings.changed) { if (getState().app.settings.changed) {
dispatch(saveSettings()) dispatch(saveSettings())
dispatch(selectAllArticles(true)) dispatch(selectAllArticles(true))
dispatch(fetchItems()) dispatch(initFeeds(true)).then(() =>
.then(() =>{
dispatch(toggleSettings()) dispatch(toggleSettings())
dispatch(initFeeds(true)) )
})
} else { } else {
dispatch(toggleSettings()) dispatch(toggleSettings())
} }
@ -143,6 +141,26 @@ export function appReducer(
} }
default: return state default: return state
} }
case ADD_SOURCE:
switch (action.status) {
case ActionStatus.Request: return {
...state,
fetchingItems: true,
settings: {
...state.settings,
changed: true,
saving: true
}
}
default: return {
...state,
fetchingItems: false,
settings: {
...state.settings,
saving: false
}
}
}
case INIT_FEEDS: case INIT_FEEDS:
switch (action.status) { switch (action.status) {
case ActionStatus.Request: return { case ActionStatus.Request: return {

View File

@ -96,6 +96,25 @@ export function fetchItemsFailure(source: RSSSource, err): ItemActionTypes {
} }
} }
export function insertItems(items: RSSItem[]): Promise<RSSItem[]> {
return new Promise<RSSItem[]>((resolve, reject) => {
db.idb.count({}, (err, count) => {
if (err) {
reject(err)
}
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)
}
})
})
})
}
export function fetchItems(): AppThunk<Promise<void>> { export function fetchItems(): AppThunk<Promise<void>> {
return (dispatch, getState) => { return (dispatch, getState) => {
let p = new Array<Promise<RSSItem[]>>() let p = new Array<Promise<RSSItem[]>>()
@ -105,11 +124,6 @@ export function fetchItems(): AppThunk<Promise<void>> {
} }
dispatch(fetchItemsRequest()) dispatch(fetchItemsRequest())
return Promise.allSettled(p).then(results => new Promise<void>((resolve, reject) => { return Promise.allSettled(p).then(results => new Promise<void>((resolve, reject) => {
db.idb.count({}, (err, count) => {
if (err) {
console.log(err)
reject(err)
}
let items = new Array<RSSItem>() let items = new Array<RSSItem>()
results.map((r, i) => { results.map((r, i) => {
if (r.status === "fulfilled") items.push(...r.value) if (r.status === "fulfilled") items.push(...r.value)
@ -118,17 +132,14 @@ export function fetchItems(): AppThunk<Promise<void>> {
dispatch(fetchItemsFailure(getState().sources[i], r.reason)) dispatch(fetchItemsFailure(getState().sources[i], r.reason))
} }
}) })
items.sort((a, b) => a.date.getTime() - b.date.getTime()) insertItems(items)
for (let i of items) i.id = count++ .then(() => {
db.idb.insert(items, (err) => {
if (err) {
console.log(err)
reject(err)
} else {
dispatch(fetchItemsSuccess(items.reverse())) dispatch(fetchItemsSuccess(items.reverse()))
resolve() resolve()
}
}) })
.catch(err => {
console.log(err)
reject(err)
}) })
})) }))
} }

View File

@ -66,7 +66,7 @@ export function selectSources(sids: number[], menuKey: string, title: string) {
export class PageState { export class PageState {
feedId = ALL feedId = ALL
sourceGroups = new Array<SourceGroup>() sourceGroups = SourceGroup.load()
} }
@ -75,14 +75,6 @@ export function pageReducer(
action: PageActionTypes | SourceActionTypes action: PageActionTypes | SourceActionTypes
): PageState { ): PageState {
switch(action.type) { switch(action.type) {
case INIT_SOURCES:
switch (action.status) {
case ActionStatus.Success: return {
...state,
sourceGroups: [new SourceGroup(action.sources, "中文")]
}
default: return state
}
case ADD_SOURCE: case ADD_SOURCE:
switch (action.status) { switch (action.status) {
case ActionStatus.Success: return { case ActionStatus.Success: return {

View File

@ -1,7 +1,8 @@
import Parser = require("rss-parser") import Parser = require("rss-parser")
import * as db from "../db" import * as db from "../db"
import { rssParser, faviconPromise, ActionStatus, AppThunk } from "../utils" import { rssParser, faviconPromise, ActionStatus, AppThunk } from "../utils"
import { RSSItem } from "./item" import { RSSItem, fetchItemsSuccess, insertItems } from "./item"
import { SourceGroup } from "./page"
export class RSSSource { export class RSSSource {
sid: number sid: number
@ -25,11 +26,15 @@ export class RSSSource {
if (this.iconurl === null) { if (this.iconurl === null) {
let f = domain + "/favicon.ico" let f = domain + "/favicon.ico"
let result = await fetch(f) let result = await fetch(f)
if (result.status == 200) this.iconurl = f if (result.status == 200 && result.headers.has("Content-Type")
&& result.headers.get("Content-Type") === "image/x-icon") {
this.iconurl = f
} }
} }
return feed
}
private static checkItem(source:RSSSource, item: Parser.Item, db: Nedb<RSSItem>): Promise<RSSItem> { private static checkItem(source: RSSSource, item: Parser.Item, db: Nedb<RSSItem>): Promise<RSSItem> {
return new Promise<RSSItem>((resolve, reject) => { return new Promise<RSSItem>((resolve, reject) => {
let i = new RSSItem(item, source) let i = new RSSItem(item, source)
db.findOne({ db.findOne({
@ -49,20 +54,21 @@ export class RSSSource {
}) })
} }
static fetchItems(source:RSSSource, parser: Parser, db: Nedb<RSSItem>): Promise<RSSItem[]> { static checkItems(source: RSSSource, items: Parser.Item[], db: Nedb<RSSItem>): Promise<RSSItem[]> {
return new Promise<RSSItem[]>((resolve, reject) => { return new Promise<RSSItem[]>((resolve, reject) => {
parser.parseURL(source.url)
.then(feed => {
let p = new Array<Promise<RSSItem>>() let p = new Array<Promise<RSSItem>>()
for (let item of feed.items) { for (let item of items) {
p.push(this.checkItem(source, item, db)) p.push(this.checkItem(source, item, db))
} }
Promise.all(p).then(values => { Promise.all(p).then(values => {
resolve(values.filter(v => v != null)) resolve(values.filter(v => v != null))
}).catch(e => { reject(e) }) }).catch(e => { reject(e) })
}) })
.catch(e => { reject(e) }) }
})
static async fetchItems(source: RSSSource, parser: Parser, db: Nedb<RSSItem>) {
let feed = await parser.parseURL(source.url)
return await this.checkItems(source, feed.items, db)
} }
} }
@ -70,9 +76,10 @@ export type SourceState = {
[sid: number]: RSSSource [sid: number]: RSSSource
} }
export const INIT_SOURCES = 'INIT_SOURCES' export const INIT_SOURCES = "INIT_SOURCES"
export const ADD_SOURCE = 'ADD_SOURCE' export const ADD_SOURCE = "ADD_SOURCE"
export const UPDATE_SOURCE = 'UPDATE_SOURCE' export const UPDATE_SOURCE = "UPDATE_SOURCE"
export const DELETE_SOURCE = "DELETE_SOURCE"
interface InitSourcesAction { interface InitSourcesAction {
type: typeof INIT_SOURCES type: typeof INIT_SOURCES
@ -162,11 +169,12 @@ export function addSourceFailure(err): SourceActionTypes {
export function addSource(url: string): AppThunk<Promise<void>> { export function addSource(url: string): AppThunk<Promise<void>> {
return (dispatch, getState) => { return (dispatch, getState) => {
if (getState().app.sourceInit) { let app = getState().app
if (app.sourceInit && !app.fetchingItems) {
dispatch(addSourceRequest()) dispatch(addSourceRequest())
let source = new RSSSource(url) let source = new RSSSource(url)
return source.fetchMetaData(rssParser) return source.fetchMetaData(rssParser)
.then(() => { .then(feed => {
let sids = Object.values(getState().sources).map(s=>s.sid) let sids = Object.values(getState().sources).map(s=>s.sid)
source.sid = Math.max(...sids, -1) + 1 source.sid = Math.max(...sids, -1) + 1
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
@ -177,10 +185,17 @@ export function addSource(url: string): AppThunk<Promise<void>> {
reject(err) reject(err)
} else { } else {
dispatch(addSourceSuccess(source)) dispatch(addSourceSuccess(source))
/* dispatch(fetchItems()).then(() => { RSSSource.checkItems(source, feed.items, db.idb)
dispatch(initFeeds()) .then(items => insertItems(items))
}) */ .then(items => {
dispatch(fetchItemsSuccess(items))
SourceGroup.save(getState().page.sourceGroups)
resolve() resolve()
})
.catch(err => {
console.log(err)
reject(err)
})
} }
}) })
}) })
@ -190,7 +205,7 @@ export function addSource(url: string): AppThunk<Promise<void>> {
dispatch(addSourceFailure(e)) dispatch(addSourceFailure(e))
}) })
} }
return new Promise((_, reject) => { reject("Need to init sources before adding.") }) return new Promise((_, reject) => { reject("Sources not initialized or fetching items.") })
} }
} }