diff --git a/src/components/settings/sources.tsx b/src/components/settings/sources.tsx index de84dd4..76a11e9 100644 --- a/src/components/settings/sources.tsx +++ b/src/components/settings/sources.tsx @@ -1,7 +1,7 @@ import * as React from "react" import intl = require("react-intl-universal") import { Label, DefaultButton, TextField, Stack, PrimaryButton, DetailsList, - IColumn, SelectionMode, Selection, IChoiceGroupOption, ChoiceGroup } from "@fluentui/react" + IColumn, SelectionMode, Selection, IChoiceGroupOption, ChoiceGroup, IDropdownOption, Dropdown } from "@fluentui/react" import { SourceState, RSSSource, SourceOpenTarget } from "../../scripts/models/source" import { urlTest } from "../../scripts/utils" import DangerButton from "../utils/danger-button" @@ -11,6 +11,7 @@ type SourcesTabProps = { addSource: (url: string) => void updateSourceName: (source: RSSSource, name: string) => void updateSourceOpenTarget: (source: RSSSource, target: SourceOpenTarget) => void + updateFetchFrequency: (source: RSSSource, frequency: number) => void deleteSource: (source: RSSSource) => void importOPML: () => void exportOPML: () => void @@ -74,6 +75,24 @@ class SourcesTab extends React.Component { } ] + fetchFrequencyOptions = (): IDropdownOption[] => [ + { key: "0", text: intl.get("sources.unlimited") }, + { key: "15", text: intl.get("time.m", { m: 15 }) }, + { key: "30", text: intl.get("time.m", { m: 30 }) }, + { key: "60", text: intl.get("time.h", { h: 1 }) }, + { key: "120", text: intl.get("time.h", { h: 2 }) }, + { key: "180", text: intl.get("time.h", { h: 3 }) }, + { key: "360", text: intl.get("time.h", { h: 6 }) }, + { key: "720", text: intl.get("time.h", { h: 12 }) }, + { key: "1440", text: intl.get("time.d", { d: 1 }) } + ] + + onFetchFrequencyChange = (_, option: IDropdownOption) => { + let frequency = parseInt(option.key as string) + this.props.updateFetchFrequency(this.state.selectedSource, frequency) + this.setState({selectedSource: {...this.state.selectedSource, fetchFrequency: frequency} as RSSSource}) + } + sourceOpenTargetChoices = (): IChoiceGroupOption[] => [ { key: String(SourceOpenTarget.Local), text: intl.get("sources.rssText") }, { key: String(SourceOpenTarget.Webpage), text: intl.get("sources.loadWebpage") }, @@ -157,6 +176,16 @@ class SourcesTab extends React.Component { text={intl.get("sources.editName")} /> + + + + + + { updateSourceOpenTarget: (source: RSSSource, target: SourceOpenTarget) => { dispatch(updateSource({ ...source, openTarget: target } as RSSSource)) }, + updateFetchFrequency: (source: RSSSource, frequency: number) => { + dispatch(updateSource({ ...source, fetchFrequency: frequency } as RSSSource)) + }, deleteSource: (source: RSSSource) => dispatch(deleteSource(source)), importOPML: () => { remote.dialog.showOpenDialog( diff --git a/src/electron.ts b/src/electron.ts index 830f4d3..fd4768a 100644 --- a/src/electron.ts +++ b/src/electron.ts @@ -9,10 +9,17 @@ if (!locked) { } let mainWindow: BrowserWindow -let store = new Store() -let restarting = false -performUpdate(store) -nativeTheme.themeSource = store.get("theme", "system") +let store: Store +let restarting: boolean + +function init() { + restarting = false + store = new Store() + performUpdate(store) + nativeTheme.themeSource = store.get("theme", "system") +} + +init() function createWindow() { let mainWindowState = windowStateKeeper({ @@ -61,9 +68,7 @@ app.on("second-instance", () => { app.on("window-all-closed", function () { mainWindow = null if (restarting) { - restarting = false - store = new Store() - nativeTheme.themeSource = store.get("theme", "system") + init() createWindow() } else if (process.platform !== "darwin") { app.quit() diff --git a/src/scripts/i18n/en-US.json b/src/scripts/i18n/en-US.json index 1fc7658..4b46fc1 100644 --- a/src/scripts/i18n/en-US.json +++ b/src/scripts/i18n/en-US.json @@ -15,6 +15,11 @@ "confirmMarkAll": "Do you really want to mark all articles on this page as read?", "confirm": "Confirm", "cancel": "Cancel", + "time": { + "m": "{m, plural, =1 {# minute} other {# minutes}}", + "h": "{h, plural, =1 {# hour} other {# hours}}", + "d": "{d, plural, =1 {# day} other {# days}}" + }, "log": { "empty": "No notifications", "fetchFailure": "Failed to load source \"{name}\".", @@ -82,6 +87,8 @@ "opmlFile": "OPML File", "name": "Source name", "editName": "Edit name", + "fetchFrequency": "Fetch frequency limit", + "unlimited": "Unlimited", "openTarget": "Default open target for articles", "delete": "Delete source", "add": "Add source", diff --git a/src/scripts/i18n/zh-CN.json b/src/scripts/i18n/zh-CN.json index 8fd44b7..e01b052 100644 --- a/src/scripts/i18n/zh-CN.json +++ b/src/scripts/i18n/zh-CN.json @@ -15,6 +15,11 @@ "confirmMarkAll": "确认将本页所有文章标为已读?", "confirm": "确认", "cancel": "取消", + "time": { + "m": "{m}分钟", + "h": "{h}小时", + "d": "{d}天" + }, "log": { "empty": "无消息", "fetchFailure": "无法加载订阅源“{name}”", @@ -82,6 +87,8 @@ "opmlFile": "OPML文件", "name": "订阅源名称", "editName": "修改名称", + "fetchFrequency": "抓取频率限制", + "unlimited": "无限制", "openTarget": "订阅源文章打开方式", "delete": "删除订阅源", "add": "添加订阅源", diff --git a/src/scripts/models/item.ts b/src/scripts/models/item.ts index 3c312e8..33d80d9 100644 --- a/src/scripts/models/item.ts +++ b/src/scripts/models/item.ts @@ -147,8 +147,12 @@ export function fetchItems(): AppThunk> { return (dispatch, getState) => { let promises = new Array>() if (!getState().app.fetchingItems) { - for (let source of Object.values(getState().sources)) { - let promise = RSSSource.fetchItems(source, rssParser, db.idb) + let timenow = new Date().getTime() + let sources = Object.values(getState().sources).filter(s => + (s.lastFetched.getTime() + (s.fetchFrequency || 0) * 60000) <= timenow + ) + for (let source of sources) { + let promise = RSSSource.fetchItems(source, rssParser) promise.finally(() => dispatch(fetchItemsIntermediate())) promises.push(promise) } diff --git a/src/scripts/models/source.ts b/src/scripts/models/source.ts index f092f05..af4e489 100644 --- a/src/scripts/models/source.ts +++ b/src/scripts/models/source.ts @@ -18,11 +18,14 @@ export class RSSSource { name: string openTarget: SourceOpenTarget unreadCount: number + lastFetched: Date + fetchFrequency?: number // in minutes constructor(url: string, name: string = null) { this.url = url this.name = name this.openTarget = SourceOpenTarget.Local + this.lastFetched = new Date() } async fetchMetaData(parser: Parser) { @@ -46,10 +49,10 @@ export class RSSSource { } } - private static checkItem(source: RSSSource, item: Parser.Item, db: Nedb): Promise { + private static checkItem(source: RSSSource, item: Parser.Item): Promise { return new Promise((resolve, reject) => { let i = new RSSItem(item, source) - db.findOne({ + db.idb.findOne({ source: i.source, title: i.title, date: i.date @@ -66,11 +69,11 @@ export class RSSSource { }) } - static checkItems(source: RSSSource, items: Parser.Item[], db: Nedb): Promise { + static checkItems(source: RSSSource, items: Parser.Item[]): Promise { return new Promise((resolve, reject) => { let p = new Array>() for (let item of items) { - p.push(this.checkItem(source, item, db)) + p.push(this.checkItem(source, item)) } Promise.all(p).then(values => { resolve(values.filter(v => v != null)) @@ -78,9 +81,10 @@ export class RSSSource { }) } - static async fetchItems(source: RSSSource, parser: Parser, db: Nedb) { + static async fetchItems(source: RSSSource, parser: Parser) { let feed = await parser.parseURL(source.url) - return await this.checkItems(source, feed.items, db) + db.sdb.update({ sid: source.sid }, { $set: { lastFetched: new Date() } }) + return await this.checkItems(source, feed.items) } } @@ -244,7 +248,7 @@ export function addSource(url: string, name: string = null, batch = false): AppT .then(inserted => { inserted.unreadCount = feed.items.length dispatch(addSourceSuccess(inserted, batch)) - return RSSSource.checkItems(inserted, feed.items, db.idb) + return RSSSource.checkItems(inserted, feed.items) .then(items => insertItems(items)) .then(() => { SourceGroup.save(getState().groups)