mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-02-27 00:47:58 +01:00
data backup
This commit is contained in:
parent
7385592ba7
commit
31c5cfc5a1
@ -1,8 +1,9 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import intl = require("react-intl-universal")
|
import intl = require("react-intl-universal")
|
||||||
import { urlTest } from "../../scripts/utils"
|
import { urlTest } from "../../scripts/utils"
|
||||||
import { getProxy, getProxyStatus, toggleProxyStatus, setProxy, getThemeSettings, setThemeSettings, ThemeSettings, getLocaleSettings } from "../../scripts/settings"
|
import { getProxy, getProxyStatus, toggleProxyStatus, setProxy, getThemeSettings, setThemeSettings, ThemeSettings, getLocaleSettings, exportAll } from "../../scripts/settings"
|
||||||
import { Stack, Label, Toggle, TextField, DefaultButton, ChoiceGroup, IChoiceGroupOption, loadTheme, Dropdown, IDropdownOption } from "@fluentui/react"
|
import { Stack, Label, Toggle, TextField, DefaultButton, ChoiceGroup, IChoiceGroupOption, loadTheme, Dropdown, IDropdownOption, PrimaryButton } from "@fluentui/react"
|
||||||
|
import { remote } from "electron"
|
||||||
|
|
||||||
type AppTabProps = {
|
type AppTabProps = {
|
||||||
setLanguage: (option: string) => void
|
setLanguage: (option: string) => void
|
||||||
@ -50,6 +51,18 @@ class AppTab extends React.Component<AppTabProps> {
|
|||||||
this.setState({ themeSettings: option.key })
|
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 = () => (
|
render = () => (
|
||||||
<div className="tab-body">
|
<div className="tab-body">
|
||||||
<Label>{intl.get("app.language")}</Label>
|
<Label>{intl.get("app.language")}</Label>
|
||||||
@ -96,6 +109,16 @@ class AppTab extends React.Component<AppTabProps> {
|
|||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>}
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,22 +2,10 @@ import * as React from "react"
|
|||||||
import intl = require("react-intl-universal")
|
import intl = require("react-intl-universal")
|
||||||
import { connect } from "react-redux"
|
import { connect } from "react-redux"
|
||||||
import { RootState } from "../../scripts/reducer"
|
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 { AppDispatch } from "../../scripts/utils"
|
||||||
import { performSearch } from "../../scripts/models/page"
|
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 = {
|
type SearchProps = {
|
||||||
searchOn: boolean
|
searchOn: boolean
|
||||||
initQuery: string
|
initQuery: string
|
||||||
@ -30,7 +18,14 @@ class ArticleSearch extends React.Component<SearchProps> {
|
|||||||
|
|
||||||
constructor(props: SearchProps) {
|
constructor(props: SearchProps) {
|
||||||
super(props)
|
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>()
|
this.inputRef = React.createRef<ISearchBox>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ const mapDispatchToProps = (dispatch: AppDispatch) => {
|
|||||||
remote.dialog.showSaveDialog(
|
remote.dialog.showSaveDialog(
|
||||||
remote.getCurrentWindow(),
|
remote.getCurrentWindow(),
|
||||||
{
|
{
|
||||||
|
defaultPath: "*/Fluent_Reader_Export.opml",
|
||||||
filters: [{ name: intl.get("sources.opmlFile"), extensions: ["opml"] }]
|
filters: [{ name: intl.get("sources.opmlFile"), extensions: ["opml"] }]
|
||||||
}
|
}
|
||||||
).then(result => {
|
).then(result => {
|
||||||
|
@ -62,6 +62,7 @@
|
|||||||
"manageSources": "Manage sources"
|
"manageSources": "Manage sources"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"writeError": "An error has occured while writing the file.",
|
||||||
"name": "Settings",
|
"name": "Settings",
|
||||||
"fetching": "Updating sources, please wait …",
|
"fetching": "Updating sources, please wait …",
|
||||||
"exit": "Exit settings",
|
"exit": "Exit settings",
|
||||||
@ -111,6 +112,10 @@
|
|||||||
"groupHint": "Double click on group to edit sources. Drag and drop to reorder."
|
"groupHint": "Double click on group to edit sources. Drag and drop to reorder."
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
|
"data": "Application Data",
|
||||||
|
"backup": "Backup",
|
||||||
|
"restore": "Restore",
|
||||||
|
"frData": "Fluent Reader Data",
|
||||||
"language": "Display language",
|
"language": "Display language",
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
"lightTheme": "Light mode",
|
"lightTheme": "Light mode",
|
||||||
|
@ -62,6 +62,7 @@
|
|||||||
"manageSources": "管理订阅源"
|
"manageSources": "管理订阅源"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"writeError": "写入文件时发生错误",
|
||||||
"name": "选项",
|
"name": "选项",
|
||||||
"fetching": "正在更新订阅源,请稍候…",
|
"fetching": "正在更新订阅源,请稍候…",
|
||||||
"exit": "退出选项",
|
"exit": "退出选项",
|
||||||
@ -111,6 +112,10 @@
|
|||||||
"groupHint": "双击分组以修改订阅源,可通过拖拽排序"
|
"groupHint": "双击分组以修改订阅源,可通过拖拽排序"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
|
"data": "应用数据",
|
||||||
|
"backup": "备份",
|
||||||
|
"restore": "还原",
|
||||||
|
"frData": "Fluent Reader数据",
|
||||||
"language": "界面语言",
|
"language": "界面语言",
|
||||||
"theme": "应用主题",
|
"theme": "应用主题",
|
||||||
"lightTheme": "浅色模式",
|
"lightTheme": "浅色模式",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import * as db from "../db"
|
import * as db from "../db"
|
||||||
import { SourceActionTypes, INIT_SOURCES, ADD_SOURCE, DELETE_SOURCE } from "./source"
|
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 { ItemActionTypes, FETCH_ITEMS, RSSItem, MARK_READ, MARK_UNREAD, TOGGLE_STARRED, TOGGLE_HIDDEN, applyItemReduction, ItemState } from "./item"
|
||||||
import { ActionStatus, AppThunk } from "../utils"
|
import { ActionStatus, AppThunk, mergeSortedArrays } from "../utils"
|
||||||
import { PageActionTypes, SELECT_PAGE, PageType, APPLY_FILTER } from "./page"
|
import { PageActionTypes, SELECT_PAGE, PageType, APPLY_FILTER } from "./page"
|
||||||
|
|
||||||
export enum FilterType {
|
export enum FilterType {
|
||||||
@ -115,9 +115,9 @@ export type FeedState = {
|
|||||||
[_id: string]: RSSFeed
|
[_id: string]: RSSFeed
|
||||||
}
|
}
|
||||||
|
|
||||||
export const INIT_FEEDS = 'INIT_FEEDS'
|
export const INIT_FEEDS = "INIT_FEEDS"
|
||||||
export const INIT_FEED = 'INIT_FEED'
|
export const INIT_FEED = "INIT_FEED"
|
||||||
export const LOAD_MORE = 'LOAD_MORE'
|
export const LOAD_MORE = "LOAD_MORE"
|
||||||
|
|
||||||
interface initFeedsAction {
|
interface initFeedsAction {
|
||||||
type: typeof INIT_FEEDS
|
type: typeof INIT_FEEDS
|
||||||
@ -278,13 +278,14 @@ export function feedReducer(
|
|||||||
let nextState = { ...state }
|
let nextState = { ...state }
|
||||||
for (let feed of Object.values(state)) {
|
for (let feed of Object.values(state)) {
|
||||||
if (feed.loaded) {
|
if (feed.loaded) {
|
||||||
let iids = action.items
|
let items = action.items
|
||||||
.filter(i => feed.sids.includes(i.source) && FeedFilter.testItem(feed.filter, i))
|
.filter(i => feed.sids.includes(i.source) && FeedFilter.testItem(feed.filter, i))
|
||||||
.map(i => i._id)
|
if (items.length > 0) {
|
||||||
if (iids.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] = {
|
nextState[feed._id] = {
|
||||||
...feed,
|
...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(() => {
|
Promise.allSettled(promises).then(() => {
|
||||||
dispatch(fetchItemsSuccess([]))
|
dispatch(fetchItemsSuccess([], {}))
|
||||||
dispatch(saveSettings())
|
dispatch(saveSettings())
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
remote.dialog.showErrorBox(
|
remote.dialog.showErrorBox(
|
||||||
@ -284,7 +284,7 @@ export function exportOPML(path: string): AppThunk {
|
|||||||
}
|
}
|
||||||
let serializer = new XMLSerializer()
|
let serializer = new XMLSerializer()
|
||||||
fs.writeFile(path, serializer.serializeToString(xml), (err) => {
|
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 intl = require("react-intl-universal")
|
||||||
import { rssParser, domParser, htmlDecode, ActionStatus, AppThunk } from "../utils"
|
import { rssParser, domParser, htmlDecode, ActionStatus, AppThunk } from "../utils"
|
||||||
import { RSSSource } from "./source"
|
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")
|
import Parser = require("@yang991178/rss-parser")
|
||||||
|
|
||||||
export class RSSItem {
|
export class RSSItem {
|
||||||
@ -64,6 +64,7 @@ interface FetchItemsAction {
|
|||||||
status: ActionStatus
|
status: ActionStatus
|
||||||
fetchCount?: number
|
fetchCount?: number
|
||||||
items?: RSSItem[]
|
items?: RSSItem[]
|
||||||
|
itemState?: ItemState
|
||||||
errSource?: RSSSource
|
errSource?: RSSSource
|
||||||
err?
|
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 {
|
return {
|
||||||
type: FETCH_ITEMS,
|
type: FETCH_ITEMS,
|
||||||
status: ActionStatus.Success,
|
status: ActionStatus.Success,
|
||||||
items: items
|
items: items,
|
||||||
|
itemState: itemState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,7 +164,7 @@ export function fetchItems(): AppThunk<Promise<void>> {
|
|||||||
})
|
})
|
||||||
insertItems(items)
|
insertItems(items)
|
||||||
.then(inserted => {
|
.then(inserted => {
|
||||||
dispatch(fetchItemsSuccess(inserted.reverse()))
|
dispatch(fetchItemsSuccess(inserted.reverse(), getState().items))
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
@ -223,8 +223,7 @@ export function addSource(url: string, name: string = null, batch = false): AppT
|
|||||||
dispatch(addSourceSuccess(source, batch))
|
dispatch(addSourceSuccess(source, batch))
|
||||||
RSSSource.checkItems(source, feed.items, db.idb)
|
RSSSource.checkItems(source, feed.items, db.idb)
|
||||||
.then(items => insertItems(items))
|
.then(items => insertItems(items))
|
||||||
.then(items => {
|
.then(() => {
|
||||||
//dispatch(fetchItemsSuccess(items))
|
|
||||||
SourceGroup.save(getState().groups)
|
SourceGroup.save(getState().groups)
|
||||||
resolve(source.sid)
|
resolve(source.sid)
|
||||||
})
|
})
|
||||||
|
@ -4,6 +4,8 @@ import { IPartialTheme, loadTheme } from "@fluentui/react"
|
|||||||
import locales from "./i18n/_locales"
|
import locales from "./i18n/_locales"
|
||||||
import Store = require("electron-store")
|
import Store = require("electron-store")
|
||||||
import { schemaTypes } from "./config-schema"
|
import { schemaTypes } from "./config-schema"
|
||||||
|
import fs = require("fs")
|
||||||
|
import intl = require("react-intl-universal")
|
||||||
|
|
||||||
export const store = new Store<schemaTypes>()
|
export const store = new Store<schemaTypes>()
|
||||||
|
|
||||||
@ -106,3 +108,28 @@ export function getCurrentLocale() {
|
|||||||
let locale = set === "default" ? remote.app.getLocale() : set
|
let locale = set === "default" ? remote.app.getLocale() : set
|
||||||
return (locale in locales) ? locale : "en-US"
|
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 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…
x
Reference in New Issue
Block a user