data backup

This commit is contained in:
刘浩远 2020-06-14 18:59:56 +08:00
parent 7385592ba7
commit 31c5cfc5a1
11 changed files with 107 additions and 33 deletions

View File

@ -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<AppTabProps> {
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 = () => (
<div className="tab-body">
<Label>{intl.get("app.language")}</Label>
@ -96,6 +109,16 @@ class AppTab extends React.Component<AppTabProps> {
</Stack.Item>
</Stack>
</form>}
<Label>{intl.get("app.data")}</Label>
<Stack horizontal>
<Stack.Item>
<PrimaryButton onClick={this.exportAll} text={intl.get("app.backup")} />
</Stack.Item>
<Stack.Item>
<DefaultButton text={intl.get("app.restore")} />
</Stack.Item>
</Stack>
</div>
)
}

View File

@ -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<SearchProps> {
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<ISearchBox>()
}

View File

@ -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 => {

View File

@ -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",

View File

@ -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": "浅色模式",

View File

@ -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)
}
}
}

View File

@ -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))
})
}

View File

@ -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<Promise<void>> {
})
insertItems(items)
.then(inserted => {
dispatch(fetchItemsSuccess(inserted.reverse()))
dispatch(fetchItemsSuccess(inserted.reverse(), getState().items))
resolve()
})
.catch(err => {

View File

@ -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)
})

View File

@ -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<schemaTypes>()
@ -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))
})
}
}
}
}

View File

@ -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<T>(a: T[], b: T[], cmp: ((x: T, y: T) => number)): T[] {
let merged = new Array<T>()
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
}