diff --git a/src/components/settings/app.tsx b/src/components/settings/app.tsx index 6b8d0da..24ccfe0 100644 --- a/src/components/settings/app.tsx +++ b/src/components/settings/app.tsx @@ -1,8 +1,9 @@ import * as React from "react" import intl = require("react-intl-universal") import { urlTest } from "../../scripts/utils" -import { getProxy, getProxyStatus, toggleProxyStatus, setProxy, getThemeSettings, setThemeSettings, ThemeSettings, getLocaleSettings } from "../../scripts/settings" -import { Stack, Label, Toggle, TextField, DefaultButton, ChoiceGroup, IChoiceGroupOption, loadTheme, Dropdown, IDropdownOption } from "@fluentui/react" +import { getProxy, getProxyStatus, toggleProxyStatus, setProxy, getThemeSettings, setThemeSettings, ThemeSettings, getLocaleSettings, exportAll } from "../../scripts/settings" +import { Stack, Label, Toggle, TextField, DefaultButton, ChoiceGroup, IChoiceGroupOption, loadTheme, Dropdown, IDropdownOption, PrimaryButton } from "@fluentui/react" +import { remote } from "electron" type AppTabProps = { setLanguage: (option: string) => void @@ -50,6 +51,18 @@ class AppTab extends React.Component { this.setState({ themeSettings: option.key }) } + exportAll = () => { + remote.dialog.showSaveDialog( + remote.getCurrentWindow(), + { + defaultPath: "*/Fluent_Reader_Backup.frdata", + filters: [{ name: intl.get("app.frData"), extensions: ["frdata"] }] + } + ).then(result => { + if (!result.canceled) exportAll(result.filePath) + }) + } + render = () => (
@@ -96,6 +109,16 @@ class AppTab extends React.Component { } + + + + + + + + + +
) } diff --git a/src/components/utils/article-search.tsx b/src/components/utils/article-search.tsx index f9eacee..26a6d3a 100644 --- a/src/components/utils/article-search.tsx +++ b/src/components/utils/article-search.tsx @@ -2,22 +2,10 @@ import * as React from "react" import intl = require("react-intl-universal") import { connect } from "react-redux" import { RootState } from "../../scripts/reducer" -import { SearchBox, IRefObject, ISearchBox } from "@fluentui/react" +import { SearchBox, ISearchBox, Async } from "@fluentui/react" import { AppDispatch } from "../../scripts/utils" import { performSearch } from "../../scripts/models/page" -class Debounced { - public use = (func: (...args: any[]) => any, delay: number): ((...args: any[]) => void) => { - let timer: NodeJS.Timeout - return (...args: any[]) => { - clearTimeout(timer) - timer = setTimeout(() => { - func.apply(this, args) - }, delay) - } - } -} - type SearchProps = { searchOn: boolean initQuery: string @@ -30,7 +18,14 @@ class ArticleSearch extends React.Component { constructor(props: SearchProps) { super(props) - this.debouncedSearch = new Debounced().use((query: string) => props.dispatch(performSearch(query)), 750) + this.debouncedSearch = new Async().debounce((query: string) => { + try { + RegExp(query) + props.dispatch(performSearch(query)) + } catch { + // console.log("Invalid regex") + } + }, 750) this.inputRef = React.createRef() } diff --git a/src/containers/settings/sources-container.tsx b/src/containers/settings/sources-container.tsx index 8ad7115..60f3201 100644 --- a/src/containers/settings/sources-container.tsx +++ b/src/containers/settings/sources-container.tsx @@ -42,6 +42,7 @@ const mapDispatchToProps = (dispatch: AppDispatch) => { remote.dialog.showSaveDialog( remote.getCurrentWindow(), { + defaultPath: "*/Fluent_Reader_Export.opml", filters: [{ name: intl.get("sources.opmlFile"), extensions: ["opml"] }] } ).then(result => { diff --git a/src/scripts/i18n/en-US.json b/src/scripts/i18n/en-US.json index 1f5bf82..16b3484 100644 --- a/src/scripts/i18n/en-US.json +++ b/src/scripts/i18n/en-US.json @@ -62,6 +62,7 @@ "manageSources": "Manage sources" }, "settings": { + "writeError": "An error has occured while writing the file.", "name": "Settings", "fetching": "Updating sources, please wait …", "exit": "Exit settings", @@ -111,6 +112,10 @@ "groupHint": "Double click on group to edit sources. Drag and drop to reorder." }, "app": { + "data": "Application Data", + "backup": "Backup", + "restore": "Restore", + "frData": "Fluent Reader Data", "language": "Display language", "theme": "Theme", "lightTheme": "Light mode", diff --git a/src/scripts/i18n/zh-CN.json b/src/scripts/i18n/zh-CN.json index e27d14f..5db08fe 100644 --- a/src/scripts/i18n/zh-CN.json +++ b/src/scripts/i18n/zh-CN.json @@ -62,6 +62,7 @@ "manageSources": "管理订阅源" }, "settings": { + "writeError": "写入文件时发生错误", "name": "选项", "fetching": "正在更新订阅源,请稍候…", "exit": "退出选项", @@ -111,6 +112,10 @@ "groupHint": "双击分组以修改订阅源,可通过拖拽排序" }, "app": { + "data": "应用数据", + "backup": "备份", + "restore": "还原", + "frData": "Fluent Reader数据", "language": "界面语言", "theme": "应用主题", "lightTheme": "浅色模式", diff --git a/src/scripts/models/feed.ts b/src/scripts/models/feed.ts index dd22e46..5c7fccd 100644 --- a/src/scripts/models/feed.ts +++ b/src/scripts/models/feed.ts @@ -1,7 +1,7 @@ import * as db from "../db" import { SourceActionTypes, INIT_SOURCES, ADD_SOURCE, DELETE_SOURCE } from "./source" -import { ItemActionTypes, FETCH_ITEMS, RSSItem, MARK_READ, MARK_UNREAD, TOGGLE_STARRED, TOGGLE_HIDDEN, applyItemReduction } from "./item" -import { ActionStatus, AppThunk } from "../utils" +import { ItemActionTypes, FETCH_ITEMS, RSSItem, MARK_READ, MARK_UNREAD, TOGGLE_STARRED, TOGGLE_HIDDEN, applyItemReduction, ItemState } from "./item" +import { ActionStatus, AppThunk, mergeSortedArrays } from "../utils" import { PageActionTypes, SELECT_PAGE, PageType, APPLY_FILTER } from "./page" export enum FilterType { @@ -115,9 +115,9 @@ export type FeedState = { [_id: string]: RSSFeed } -export const INIT_FEEDS = 'INIT_FEEDS' -export const INIT_FEED = 'INIT_FEED' -export const LOAD_MORE = 'LOAD_MORE' +export const INIT_FEEDS = "INIT_FEEDS" +export const INIT_FEED = "INIT_FEED" +export const LOAD_MORE = "LOAD_MORE" interface initFeedsAction { type: typeof INIT_FEEDS @@ -278,13 +278,14 @@ export function feedReducer( let nextState = { ...state } for (let feed of Object.values(state)) { if (feed.loaded) { - let iids = action.items + let items = action.items .filter(i => feed.sids.includes(i.source) && FeedFilter.testItem(feed.filter, i)) - .map(i => i._id) - if (iids.length > 0) { + if (items.length > 0) { + let oldItems = feed.iids.map(id => action.itemState[id]) + let nextItems = mergeSortedArrays(oldItems, items, (a, b) => b.date.getTime() - a.date.getTime()) nextState[feed._id] = { ...feed, - iids: [...iids, ...feed.iids] + iids: nextItems.map(i => i._id) } } } diff --git a/src/scripts/models/group.ts b/src/scripts/models/group.ts index 1dce9ab..b07946d 100644 --- a/src/scripts/models/group.ts +++ b/src/scripts/models/group.ts @@ -236,7 +236,7 @@ export function importOPML(path: string): AppThunk { }) }) Promise.allSettled(promises).then(() => { - dispatch(fetchItemsSuccess([])) + dispatch(fetchItemsSuccess([], {})) dispatch(saveSettings()) if (errors.length > 0) { remote.dialog.showErrorBox( @@ -284,7 +284,7 @@ export function exportOPML(path: string): AppThunk { } let serializer = new XMLSerializer() fs.writeFile(path, serializer.serializeToString(xml), (err) => { - if (err) console.log(err) + if (err) remote.dialog.showErrorBox(intl.get("settings.writeError"), String(err)) }) } diff --git a/src/scripts/models/item.ts b/src/scripts/models/item.ts index 4a6c006..9af9e2e 100644 --- a/src/scripts/models/item.ts +++ b/src/scripts/models/item.ts @@ -2,7 +2,7 @@ import * as db from "../db" import intl = require("react-intl-universal") import { rssParser, domParser, htmlDecode, ActionStatus, AppThunk } from "../utils" import { RSSSource } from "./source" -import { FeedActionTypes, INIT_FEED, LOAD_MORE, FeedFilter } from "./feed" +import { FeedActionTypes, INIT_FEED, LOAD_MORE } from "./feed" import Parser = require("@yang991178/rss-parser") export class RSSItem { @@ -64,6 +64,7 @@ interface FetchItemsAction { status: ActionStatus fetchCount?: number items?: RSSItem[] + itemState?: ItemState errSource?: RSSSource err? } @@ -104,11 +105,12 @@ export function fetchItemsRequest(fetchCount = 0): ItemActionTypes { } } -export function fetchItemsSuccess(items: RSSItem[]): ItemActionTypes { +export function fetchItemsSuccess(items: RSSItem[], itemState: ItemState): ItemActionTypes { return { type: FETCH_ITEMS, status: ActionStatus.Success, - items: items + items: items, + itemState: itemState } } @@ -162,7 +164,7 @@ export function fetchItems(): AppThunk> { }) insertItems(items) .then(inserted => { - dispatch(fetchItemsSuccess(inserted.reverse())) + dispatch(fetchItemsSuccess(inserted.reverse(), getState().items)) resolve() }) .catch(err => { diff --git a/src/scripts/models/source.ts b/src/scripts/models/source.ts index d680203..c67eee6 100644 --- a/src/scripts/models/source.ts +++ b/src/scripts/models/source.ts @@ -223,8 +223,7 @@ export function addSource(url: string, name: string = null, batch = false): AppT dispatch(addSourceSuccess(source, batch)) RSSSource.checkItems(source, feed.items, db.idb) .then(items => insertItems(items)) - .then(items => { - //dispatch(fetchItemsSuccess(items)) + .then(() => { SourceGroup.save(getState().groups) resolve(source.sid) }) diff --git a/src/scripts/settings.ts b/src/scripts/settings.ts index 76b4009..faea277 100644 --- a/src/scripts/settings.ts +++ b/src/scripts/settings.ts @@ -4,6 +4,8 @@ import { IPartialTheme, loadTheme } from "@fluentui/react" import locales from "./i18n/_locales" import Store = require("electron-store") import { schemaTypes } from "./config-schema" +import fs = require("fs") +import intl = require("react-intl-universal") export const store = new Store() @@ -106,3 +108,28 @@ export function getCurrentLocale() { let locale = set === "default" ? remote.app.getLocale() : set return (locale in locales) ? locale : "en-US" } + +export function exportAll(path) { + let output = {} + for (let [key, value] of store) { + output[key] = value + } + output["nedb"] = {} + let openRequest = window.indexedDB.open("NeDB") + openRequest.onsuccess = () => { + let db = openRequest.result + let objectStore = db.transaction("nedbdata").objectStore("nedbdata") + let cursorRequest = objectStore.openCursor() + cursorRequest.onsuccess = () => { + let cursor = cursorRequest.result + if (cursor) { + output["nedb"][cursor.key] = cursor.value + cursor.continue() + } else { + fs.writeFile(path, JSON.stringify(output), (err) => { + if (err) remote.dialog.showErrorBox(intl.get("settings.writeError"), String(err)) + }) + } + } + } +} \ No newline at end of file diff --git a/src/scripts/utils.ts b/src/scripts/utils.ts index 455150c..2694a56 100644 --- a/src/scripts/utils.ts +++ b/src/scripts/utils.ts @@ -69,3 +69,19 @@ export const cutText = (s: string, length: number) => { } export const googleSearch = (text: string) => openExternal("https://www.google.com/search?q=" + encodeURIComponent(text)) + +export function mergeSortedArrays(a: T[], b: T[], cmp: ((x: T, y: T) => number)): T[] { + let merged = new Array() + let i = 0 + let j = 0 + while (i < a.length && j < b.length) { + if (cmp(a[i], b[j]) <= 0) { + merged.push(a[i++]) + } else { + merged.push(b[j++]) + } + } + while (i < a.length) merged.push(a[i++]) + while (j < b.length) merged.push(b[j++]) + return merged +} \ No newline at end of file