data backup
This commit is contained in:
parent
7385592ba7
commit
31c5cfc5a1
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>()
|
||||
}
|
||||
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "浅色模式",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue