storage clean up & import settings
This commit is contained in:
parent
31c5cfc5a1
commit
8261fbf5b4
|
@ -75,6 +75,10 @@ body {
|
|||
background: #a4262c;
|
||||
border-color: #a4262c;
|
||||
}
|
||||
.ms-Button--primary.danger.is-disabled {
|
||||
background: var(--neutralLighter);
|
||||
border-color: var(--neutralLighter);
|
||||
}
|
||||
.ms-Button--commandBar.active {
|
||||
background-color: var(--neutralLight);
|
||||
color: var(--neutralDark);
|
||||
|
@ -326,6 +330,11 @@ img.favicon {
|
|||
font-size: 12px;
|
||||
color: var(--neutralSecondary);
|
||||
}
|
||||
.settings-hint.up {
|
||||
position: relative;
|
||||
top: -12px;
|
||||
line-height: unset;
|
||||
}
|
||||
.settings-about {
|
||||
margin: 72px 0;
|
||||
color: var(--black);
|
||||
|
@ -543,6 +552,15 @@ img.favicon {
|
|||
.flex-fix {
|
||||
min-width: 280px;
|
||||
}
|
||||
.cards-feed-container > .empty, .list-feed > .empty {
|
||||
width: 100%;
|
||||
height: calc(100vh - 64px);
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
color: var(--neutralSecondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info {
|
||||
position: relative;
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{"$$indexCreated":{"fieldName":"source","unique":false,"sparse":false}}
|
|
@ -22,8 +22,7 @@
|
|||
"output": "./bin/${platform}/${arch}/"
|
||||
},
|
||||
"win": {
|
||||
"target": [ "nsis", "appx" ],
|
||||
"certificateFile": "./bin/key.pfx"
|
||||
"target": [ "nsis", "appx" ]
|
||||
},
|
||||
"appx": {
|
||||
"applicationId": "FluentReader",
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
{"$$indexCreated":{"fieldName":"sid","unique":true,"sparse":false}}
|
||||
{"$$indexCreated":{"fieldName":"url","unique":true,"sparse":false}}
|
|
@ -53,6 +53,9 @@ class CardsFeed extends React.Component<FeedProps> {
|
|||
onClick={() => this.props.loadMore(this.props.feed)} /></div>
|
||||
: null
|
||||
}
|
||||
{ this.props.items.length === 0 && (
|
||||
<div className="empty">{intl.get("article.empty")}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -28,6 +28,9 @@ class ListFeed extends React.Component<FeedProps> {
|
|||
onClick={() => this.props.loadMore(this.props.feed)} /></div>
|
||||
: null
|
||||
}
|
||||
{ this.props.items.length === 0 && (
|
||||
<div className="empty">{intl.get("article.empty")}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,19 +1,56 @@
|
|||
import * as React from "react"
|
||||
import intl = require("react-intl-universal")
|
||||
import { urlTest } from "../../scripts/utils"
|
||||
import { urlTest, byteToMB, calculateItemSize } from "../../scripts/utils"
|
||||
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"
|
||||
import DangerButton from "../utils/danger-button"
|
||||
|
||||
type AppTabProps = {
|
||||
setLanguage: (option: string) => void
|
||||
deleteArticles: (days: number) => Promise<void>
|
||||
importAll: () => void
|
||||
}
|
||||
|
||||
class AppTab extends React.Component<AppTabProps> {
|
||||
state = {
|
||||
type AppTabState = {
|
||||
pacStatus: boolean
|
||||
pacUrl: string
|
||||
themeSettings: ThemeSettings
|
||||
itemSize: string
|
||||
cacheSize: string
|
||||
deleteIndex: string
|
||||
}
|
||||
|
||||
class AppTab extends React.Component<AppTabProps, AppTabState> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
pacStatus: getProxyStatus(),
|
||||
pacUrl: getProxy(),
|
||||
themeSettings: getThemeSettings()
|
||||
themeSettings: getThemeSettings(),
|
||||
itemSize: null,
|
||||
cacheSize: null,
|
||||
deleteIndex: null
|
||||
}
|
||||
this.getItemSize()
|
||||
this.getCacheSize()
|
||||
}
|
||||
|
||||
getCacheSize = () => {
|
||||
remote.session.defaultSession.getCacheSize().then(size => {
|
||||
this.setState({ cacheSize: byteToMB(size) })
|
||||
})
|
||||
}
|
||||
getItemSize = () => {
|
||||
calculateItemSize().then((size) => {
|
||||
this.setState({ itemSize: byteToMB(size) })
|
||||
})
|
||||
}
|
||||
|
||||
clearCache = () => {
|
||||
remote.session.defaultSession.clearCache().then(() => {
|
||||
this.getCacheSize()
|
||||
})
|
||||
}
|
||||
|
||||
themeChoices = (): IChoiceGroupOption[] => [
|
||||
|
@ -22,6 +59,24 @@ class AppTab extends React.Component<AppTabProps> {
|
|||
{ key: ThemeSettings.Dark, text: intl.get("app.darkTheme") }
|
||||
]
|
||||
|
||||
deleteOptions = (): IDropdownOption[] => [
|
||||
{ key: "7", text: intl.get("app.daysAgo", { days: 7 }) },
|
||||
{ key: "14", text: intl.get("app.daysAgo", { days: 14 }) },
|
||||
{ key: "21", text: intl.get("app.daysAgo", { days: 21 }) },
|
||||
{ key: "28", text: intl.get("app.daysAgo", { days: 28 }) },
|
||||
{ key: "0", text: intl.get("app.deleteAll") },
|
||||
]
|
||||
|
||||
deleteChange = (_, item: IDropdownOption) => {
|
||||
this.setState({ deleteIndex: item ? String(item.key) : null })
|
||||
}
|
||||
|
||||
confirmDelete = () => {
|
||||
this.setState({ itemSize: null })
|
||||
this.props.deleteArticles(parseInt(this.state.deleteIndex))
|
||||
.then(() => this.getItemSize())
|
||||
}
|
||||
|
||||
languageOptions = (): IDropdownOption[] => [
|
||||
{ key: "default", text: intl.get("followSystem") },
|
||||
{ key: "en-US", text: "English" },
|
||||
|
@ -38,6 +93,7 @@ class AppTab extends React.Component<AppTabProps> {
|
|||
|
||||
handleInputChange = (event) => {
|
||||
const name: string = event.target.name
|
||||
// @ts-ignore
|
||||
this.setState({[name]: event.target.value.trim()})
|
||||
}
|
||||
|
||||
|
@ -48,7 +104,7 @@ class AppTab extends React.Component<AppTabProps> {
|
|||
|
||||
onThemeChange = (_, option: IChoiceGroupOption) => {
|
||||
setThemeSettings(option.key as ThemeSettings)
|
||||
this.setState({ themeSettings: option.key })
|
||||
this.setState({ themeSettings: option.key as ThemeSettings })
|
||||
}
|
||||
|
||||
exportAll = () => {
|
||||
|
@ -110,13 +166,44 @@ class AppTab extends React.Component<AppTabProps> {
|
|||
</Stack>
|
||||
</form>}
|
||||
|
||||
<Label>{intl.get("app.cleanup")}</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item grow>
|
||||
<Dropdown
|
||||
placeholder={intl.get("app.deleteChoices")}
|
||||
options={this.deleteOptions()}
|
||||
selectedKey={this.state.deleteIndex}
|
||||
onChange={this.deleteChange} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DangerButton
|
||||
disabled={this.state.itemSize === null || this.state.deleteIndex === null}
|
||||
text={intl.get("app.confirmDelete")}
|
||||
onClick={this.confirmDelete} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<span className="settings-hint up">
|
||||
{this.state.itemSize ? intl.get("app.itemSize", {size: this.state.itemSize}) : intl.get("app.calculatingSize")}
|
||||
</span>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<DefaultButton
|
||||
text={intl.get("app.cache")}
|
||||
disabled={this.state.cacheSize === null || this.state.cacheSize === "0MB"}
|
||||
onClick={this.clearCache} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<span className="settings-hint up">
|
||||
{this.state.cacheSize ? intl.get("app.cacheSize", {size: this.state.cacheSize}) : intl.get("app.calculatingSize")}
|
||||
</span>
|
||||
|
||||
<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")} />
|
||||
<DefaultButton onClick={this.props.importAll} text={intl.get("app.restore")} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</div>
|
||||
|
|
|
@ -15,6 +15,7 @@ class DangerButton extends PrimaryButton {
|
|||
}
|
||||
|
||||
onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (!this.props.disabled) {
|
||||
if (this.state.confirming) {
|
||||
if (this.props.onClick) this.props.onClick(event)
|
||||
clearTimeout(this.timerID)
|
||||
|
@ -26,6 +27,7 @@ class DangerButton extends PrimaryButton {
|
|||
}, 5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.timerID) clearTimeout(this.timerID)
|
||||
|
|
|
@ -1,12 +1,51 @@
|
|||
import intl = require("react-intl-universal")
|
||||
import { connect } from "react-redux"
|
||||
import { setLocaleSettings } from "../../scripts/settings"
|
||||
import { initIntl } from "../../scripts/models/app"
|
||||
import { setLocaleSettings, importAll } from "../../scripts/settings"
|
||||
import { initIntl, saveSettings } from "../../scripts/models/app"
|
||||
import * as db from "../../scripts/db"
|
||||
import AppTab from "../../components/settings/app"
|
||||
import { initFeeds } from "../../scripts/models/feed"
|
||||
import { remote } from "electron"
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
setLanguage: (option: string) => {
|
||||
setLocaleSettings(option)
|
||||
dispatch(initIntl())
|
||||
},
|
||||
deleteArticles: (days: number) => new Promise((resolve) => {
|
||||
dispatch(saveSettings())
|
||||
let date = new Date()
|
||||
date.setTime(date.getTime() - days * 86400000)
|
||||
db.idb.remove({ date: { $lt: date } }, { multi: true }, () => {
|
||||
dispatch(initFeeds(true)).then(() => dispatch(saveSettings()))
|
||||
db.idb.prependOnceListener("compaction.done", () => {
|
||||
resolve()
|
||||
})
|
||||
db.idb.persistence.compactDatafile()
|
||||
})
|
||||
}),
|
||||
importAll: () => {
|
||||
let window = remote.getCurrentWindow()
|
||||
remote.dialog.showOpenDialog(window, {
|
||||
filters: [{ name: intl.get("app.frData"), extensions: ["frdata"] }],
|
||||
properties: ["openFile"]
|
||||
}).then(result => {
|
||||
if (!result.canceled) {
|
||||
remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: "warning",
|
||||
title: intl.get("app.restore"),
|
||||
message: intl.get("app.confirmImport"),
|
||||
buttons: process.platform === "win32" ? ["Yes", "No"] : [intl.get("confirm"), intl.get("cancel")],
|
||||
defaultId: 1,
|
||||
cancelId: 1
|
||||
}).then(response => {
|
||||
if (response.response === 0) {
|
||||
dispatch(saveSettings())
|
||||
importAll(result.filePaths[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -3,7 +3,8 @@ import windowStateKeeper = require("electron-window-state")
|
|||
import Store = require("electron-store")
|
||||
|
||||
let mainWindow: BrowserWindow
|
||||
const store = new Store()
|
||||
let store = new Store()
|
||||
let restarting = false
|
||||
nativeTheme.themeSource = store.get("theme", "system")
|
||||
|
||||
function createWindow() {
|
||||
|
@ -31,28 +32,38 @@ function createWindow() {
|
|||
}
|
||||
})
|
||||
mainWindowState.manage(mainWindow)
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow.on("ready-to-show", () => {
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
mainWindow.webContents.openDevTools()
|
||||
});
|
||||
// and load the index.html of the app.
|
||||
mainWindow.loadFile((app.isPackaged ? "dist/" : "") + 'index.html')
|
||||
mainWindow.loadFile((app.isPackaged ? "dist/" : "") + "index.html")
|
||||
}
|
||||
|
||||
Menu.setApplicationMenu(null)
|
||||
|
||||
app.on('ready', createWindow)
|
||||
app.on("ready", createWindow)
|
||||
|
||||
app.on('window-all-closed', function () {
|
||||
app.on("window-all-closed", function () {
|
||||
mainWindow = null
|
||||
if (process.platform !== 'darwin') {
|
||||
if (restarting) {
|
||||
restarting = false
|
||||
store = new Store()
|
||||
nativeTheme.themeSource = store.get("theme", "system")
|
||||
createWindow()
|
||||
} else if (process.platform !== "darwin") {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('activate', function () {
|
||||
app.on("activate", function () {
|
||||
if (mainWindow === null) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on("restart", () => {
|
||||
restarting = true
|
||||
mainWindow.close()
|
||||
})
|
||||
|
|
|
@ -12,6 +12,7 @@ import { SourceGroup } from "./models/group"
|
|||
} */
|
||||
|
||||
export type schemaTypes = {
|
||||
version: string
|
||||
theme: ThemeSettings
|
||||
pac: string
|
||||
pacOn: boolean
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
"subscriptions": "Subscriptions"
|
||||
},
|
||||
"article": {
|
||||
"empty": "No articles",
|
||||
"untitled": "(Untitled)",
|
||||
"hide": "Hide article",
|
||||
"unhide": "Unhide article",
|
||||
|
@ -112,6 +113,16 @@
|
|||
"groupHint": "Double click on group to edit sources. Drag and drop to reorder."
|
||||
},
|
||||
"app": {
|
||||
"cleanup": "Clean up",
|
||||
"cache": "Clear cache",
|
||||
"cacheSize": "Cached {size} of data",
|
||||
"deleteChoices": "Delete articles from ... days ago",
|
||||
"confirmDelete": "Delete",
|
||||
"daysAgo": "{days} days ago",
|
||||
"deleteAll": "Delete all articles",
|
||||
"calculatingSize": "Calculating size...",
|
||||
"itemSize": "Around {size} of local storage is occupied by articles",
|
||||
"confirmImport": "Do you really want to import data from the backup file? All current data will be wiped.",
|
||||
"data": "Application Data",
|
||||
"backup": "Backup",
|
||||
"restore": "Restore",
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
"subscriptions": "订阅源"
|
||||
},
|
||||
"article": {
|
||||
"empty": "无文章",
|
||||
"untitled": "(无标题)",
|
||||
"hide": "隐藏文章",
|
||||
"unhide": "取消隐藏",
|
||||
|
@ -112,6 +113,16 @@
|
|||
"groupHint": "双击分组以修改订阅源,可通过拖拽排序"
|
||||
},
|
||||
"app": {
|
||||
"cleanup": "清理",
|
||||
"cache": "清空缓存",
|
||||
"cacheSize": "已缓存{size}数据",
|
||||
"deleteChoices": "删除 … 天前的文章",
|
||||
"confirmDelete": "删除文章",
|
||||
"daysAgo": "{days}天前",
|
||||
"deleteAll": "删除全部文章",
|
||||
"calculatingSize": "正在计算占用空间…",
|
||||
"itemSize": "本地文章约占用{size}空间",
|
||||
"confirmImport": "确认要从备份文件导入数据吗?这将清除所有应用数据。",
|
||||
"data": "应用数据",
|
||||
"backup": "备份",
|
||||
"restore": "还原",
|
||||
|
|
|
@ -7,6 +7,7 @@ import { SourceGroupActionTypes, UPDATE_SOURCE_GROUP, ADD_SOURCE_TO_GROUP, DELET
|
|||
import { PageActionTypes, SELECT_PAGE, PageType, selectAllArticles } from "./page"
|
||||
import { getCurrentLocale } from "../settings"
|
||||
import locales from "../i18n/_locales"
|
||||
import * as db from "../db"
|
||||
|
||||
export enum ContextMenuType {
|
||||
Hidden, Item, Text, View, Group
|
||||
|
@ -198,7 +199,10 @@ export function initApp(): AppThunk {
|
|||
dispatch(initFeeds())
|
||||
).then(() => {
|
||||
dispatch(selectAllArticles())
|
||||
dispatch(fetchItems())
|
||||
return dispatch(fetchItems())
|
||||
}).then(() => {
|
||||
db.sdb.persistence.compactDatafile()
|
||||
db.idb.persistence.compactDatafile()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,16 +26,6 @@ export class RSSItem {
|
|||
this.link = item.link || ""
|
||||
this.fetchedDate = new Date()
|
||||
this.date = item.isoDate ? new Date(item.isoDate) : this.fetchedDate
|
||||
if (item.thumb) this.thumb = item.thumb
|
||||
else if (item.image) this.thumb = item.image
|
||||
else {
|
||||
let dom = domParser.parseFromString(item.content, "text/html")
|
||||
let baseEl = dom.createElement('base')
|
||||
baseEl.setAttribute('href', this.link.split("/").slice(0, 3).join("/"))
|
||||
dom.head.append(baseEl)
|
||||
let img = dom.querySelector("img")
|
||||
if (img && img.src) this.thumb = img.src
|
||||
}
|
||||
if (item.fullContent) {
|
||||
this.content = item.fullContent
|
||||
this.snippet = htmlDecode(item.fullContent)
|
||||
|
@ -43,6 +33,16 @@ export class RSSItem {
|
|||
this.content = item.content || ""
|
||||
this.snippet = htmlDecode(item.contentSnippet || "")
|
||||
}
|
||||
if (item.thumb) this.thumb = item.thumb
|
||||
else if (item.image) this.thumb = item.image
|
||||
else {
|
||||
let dom = domParser.parseFromString(this.content, "text/html")
|
||||
let baseEl = dom.createElement('base')
|
||||
baseEl.setAttribute('href', this.link.split("/").slice(0, 3).join("/"))
|
||||
dom.head.append(baseEl)
|
||||
let img = dom.querySelector("img")
|
||||
if (img && img.src) this.thumb = img.src
|
||||
}
|
||||
this.creator = item.creator
|
||||
this.hasRead = false
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { remote, ipcRenderer } from "electron"
|
||||
import { ViewType } from "./models/page"
|
||||
import { IPartialTheme, loadTheme } from "@fluentui/react"
|
||||
import { IPartialTheme, loadTheme, values } from "@fluentui/react"
|
||||
import locales from "./i18n/_locales"
|
||||
import Store = require("electron-store")
|
||||
import { schemaTypes } from "./config-schema"
|
||||
|
@ -109,7 +109,7 @@ export function getCurrentLocale() {
|
|||
return (locale in locales) ? locale : "en-US"
|
||||
}
|
||||
|
||||
export function exportAll(path) {
|
||||
export function exportAll(path: string) {
|
||||
let output = {}
|
||||
for (let [key, value] of store) {
|
||||
output[key] = value
|
||||
|
@ -133,3 +133,34 @@ export function exportAll(path) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function importAll(path) {
|
||||
fs.readFile(path, "utf-8", async (err, data) => {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
} else {
|
||||
let configs = JSON.parse(data)
|
||||
let openRequest = window.indexedDB.open("NeDB")
|
||||
openRequest.onsuccess = () => {
|
||||
let db = openRequest.result
|
||||
let objectStore = db.transaction("nedbdata", "readwrite").objectStore("nedbdata")
|
||||
let requests = Object.entries(configs.nedb).map(([key, value]) => {
|
||||
return objectStore.put(value, key)
|
||||
})
|
||||
let promises = requests.map(req => new Promise((resolve, reject) => {
|
||||
req.onsuccess = () => resolve()
|
||||
req.onerror = () => reject()
|
||||
}))
|
||||
Promise.all(promises).then(() => {
|
||||
delete configs.nedb
|
||||
store.clear()
|
||||
for (let [key, value] of Object.entries(configs)) {
|
||||
// @ts-ignore
|
||||
store.set(key, value)
|
||||
}
|
||||
ipcRenderer.send("restart")
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { app } from "electron"
|
||||
import Store = require("electron-store")
|
||||
|
||||
export default function performUpdate(store: Store) {
|
||||
let version = store.get("version", null)
|
||||
let currentVersion = app.getVersion()
|
||||
|
||||
if (version != currentVersion) {
|
||||
store.set("version", currentVersion)
|
||||
}
|
||||
}
|
|
@ -85,3 +85,25 @@ export function mergeSortedArrays<T>(a: T[], b: T[], cmp: ((x: T, y: T) => numbe
|
|||
while (j < b.length) merged.push(b[j++])
|
||||
return merged
|
||||
}
|
||||
|
||||
export function byteToMB(B: number) {
|
||||
let MB = Math.round(B / 1048576)
|
||||
return MB + "MB"
|
||||
}
|
||||
|
||||
export function calculateItemSize(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let openRequest = window.indexedDB.open("NeDB")
|
||||
openRequest.onsuccess = () => {
|
||||
let db = openRequest.result
|
||||
let objectStore = db.transaction("nedbdata").objectStore("nedbdata")
|
||||
let getRequest = objectStore.get("items")
|
||||
getRequest.onsuccess = () => {
|
||||
let resultBuffer = Buffer.from(getRequest.result)
|
||||
resolve(resultBuffer.length)
|
||||
}
|
||||
getRequest.onerror = () => reject()
|
||||
}
|
||||
openRequest.onerror = () => reject()
|
||||
})
|
||||
}
|
|
@ -2,7 +2,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|||
|
||||
module.exports = [
|
||||
{
|
||||
mode: 'development',
|
||||
mode: 'production',
|
||||
entry: './src/electron.ts',
|
||||
target: 'electron-main',
|
||||
module: {
|
||||
|
|
Loading…
Reference in New Issue