mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-03-06 12:27:49 +01:00
add utils bridge
This commit is contained in:
parent
b4c1ddd587
commit
394643b95e
@ -1,7 +1,7 @@
|
||||
import { SourceGroup, ViewType, ThemeSettings } from "../schema-types"
|
||||
import { SourceGroup, ViewType, ThemeSettings, SchemaTypes } from "../schema-types"
|
||||
import { ipcRenderer } from "electron"
|
||||
|
||||
const SettingsBridge = {
|
||||
const settingsBridge = {
|
||||
saveGroups: (groups: SourceGroup[]) => {
|
||||
ipcRenderer.invoke("set-groups", groups)
|
||||
},
|
||||
@ -59,13 +59,21 @@ const SettingsBridge = {
|
||||
},
|
||||
getCurrentLocale: (): string => {
|
||||
return ipcRenderer.sendSync("get-locale")
|
||||
}
|
||||
},
|
||||
|
||||
getAll: () => {
|
||||
return ipcRenderer.sendSync("get-all-settings") as Object
|
||||
},
|
||||
|
||||
setAll: (configs) => {
|
||||
ipcRenderer.invoke("import-all-settings", configs)
|
||||
},
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
settings: typeof SettingsBridge
|
||||
settings: typeof settingsBridge
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsBridge
|
||||
export default settingsBridge
|
85
src/bridges/utils.ts
Normal file
85
src/bridges/utils.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { ipcRenderer } from "electron"
|
||||
|
||||
const utilsBridge = {
|
||||
getVersion: (): string => {
|
||||
return ipcRenderer.sendSync("get-version")
|
||||
},
|
||||
|
||||
openExternal: (url: string) => {
|
||||
ipcRenderer.invoke("open-external", url)
|
||||
},
|
||||
|
||||
showErrorBox: (title: string, content: string) => {
|
||||
ipcRenderer.invoke("show-error-box", title, content)
|
||||
},
|
||||
|
||||
showMessageBox: async (title: string, message: string, confirm: string, cancel: string, defaultCancel=false, type="none") => {
|
||||
return await ipcRenderer.invoke("show-message-box", title, message, confirm, cancel, defaultCancel, type) as boolean
|
||||
},
|
||||
|
||||
showSaveDialog: async (filters: Electron.FileFilter[], path: string) => {
|
||||
let result = await ipcRenderer.invoke("show-save-dialog", filters, path) as boolean
|
||||
if (result) {
|
||||
return (result: string, errmsg: string) => {
|
||||
ipcRenderer.invoke("write-save-result", result, errmsg)
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
showOpenDialog: async (filters: Electron.FileFilter[]) => {
|
||||
return await ipcRenderer.invoke("show-open-dialog", filters) as string
|
||||
},
|
||||
|
||||
getCacheSize: async (): Promise<number> => {
|
||||
return await ipcRenderer.invoke("get-cache")
|
||||
},
|
||||
|
||||
clearCache: async () => {
|
||||
await ipcRenderer.invoke("clear-cache")
|
||||
},
|
||||
|
||||
addWebviewKeydownListener: (id: number, callback: (event: Electron.Input) => any) => {
|
||||
ipcRenderer.invoke("add-webview-keydown-listener", id)
|
||||
ipcRenderer.removeAllListeners("webview-keydown")
|
||||
ipcRenderer.on("webview-keydown", (_, input) => {
|
||||
callback(input)
|
||||
})
|
||||
},
|
||||
|
||||
writeClipboard: (text: string) => {
|
||||
ipcRenderer.invoke("write-clipboard", text)
|
||||
},
|
||||
|
||||
closeWindow: () => {
|
||||
ipcRenderer.invoke("close-window")
|
||||
},
|
||||
minimizeWindow: () => {
|
||||
ipcRenderer.invoke("minimize-window")
|
||||
},
|
||||
maximizeWindow: () => {
|
||||
ipcRenderer.invoke("maximize-window")
|
||||
},
|
||||
isMaximized: () => {
|
||||
return ipcRenderer.sendSync("is-maximized") as boolean
|
||||
},
|
||||
addWindowStateListener: (callback: (state: boolean) => any) => {
|
||||
ipcRenderer.removeAllListeners("maximized")
|
||||
ipcRenderer.on("maximized", () => {
|
||||
callback(true)
|
||||
})
|
||||
ipcRenderer.removeAllListeners("unmaximized")
|
||||
ipcRenderer.on("unmaximized", () => {
|
||||
callback(false)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
utils: typeof utilsBridge
|
||||
}
|
||||
}
|
||||
|
||||
export default utilsBridge
|
@ -2,11 +2,9 @@ import * as React from "react"
|
||||
import intl from "react-intl-universal"
|
||||
import { renderToString } from "react-dom/server"
|
||||
import { RSSItem } from "../scripts/models/item"
|
||||
import { openExternal } from "../scripts/utils"
|
||||
import { Stack, CommandBarButton, IContextualMenuProps, FocusZone } from "@fluentui/react"
|
||||
import { RSSSource, SourceOpenTarget } from "../scripts/models/source"
|
||||
import { store } from "../scripts/settings"
|
||||
import { clipboard, remote } from "electron"
|
||||
|
||||
const FONT_SIZE_STORE_KEY = "fontSize"
|
||||
const FONT_SIZE_OPTIONS = [12, 13, 14, 15, 16, 17, 18, 19, 20]
|
||||
@ -70,7 +68,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
||||
key: "copyURL",
|
||||
text: intl.get("context.copyURL"),
|
||||
iconProps: { iconName: "Link" },
|
||||
onClick: () => { clipboard.writeText(this.props.item.link) }
|
||||
onClick: () => { window.utils.writeClipboard(this.props.item.link) }
|
||||
},
|
||||
{
|
||||
key: "toggleHidden",
|
||||
@ -84,7 +82,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
||||
ipcHandler = event => {
|
||||
switch (event.channel) {
|
||||
case "request-navigation": {
|
||||
openExternal(event.args[0])
|
||||
window.utils.openExternal(event.args[0])
|
||||
break
|
||||
}
|
||||
case "context-menu": {
|
||||
@ -96,13 +94,13 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
||||
}
|
||||
}
|
||||
popUpHandler = event => {
|
||||
openExternal(event.url)
|
||||
window.utils.openExternal(event.url)
|
||||
}
|
||||
navigationHandler = event => {
|
||||
openExternal(event.url)
|
||||
window.utils.openExternal(event.url)
|
||||
this.props.dismiss()
|
||||
}
|
||||
keyDownHandler = (_, input) => {
|
||||
keyDownHandler = (input: Electron.Input) => {
|
||||
if (input.type === "keyDown") {
|
||||
switch (input.key) {
|
||||
case "Escape":
|
||||
@ -140,8 +138,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
||||
webview.addEventListener("new-window", this.popUpHandler)
|
||||
webview.addEventListener("will-navigate", this.navigationHandler)
|
||||
webview.addEventListener("dom-ready", () => {
|
||||
let webContents = remote.webContents.fromId(webview.getWebContentsId())
|
||||
webContents.on("before-input-event", this.keyDownHandler)
|
||||
window.utils.addWebviewKeydownListener(webview.getWebContentsId(), this.keyDownHandler)
|
||||
})
|
||||
this.webview = webview
|
||||
webview.focus()
|
||||
@ -163,7 +160,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
|
||||
}
|
||||
|
||||
openInBrowser = () => {
|
||||
openExternal(this.props.item.link)
|
||||
window.utils.openExternal(this.props.item.link)
|
||||
}
|
||||
|
||||
toggleWebpage = () => {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import * as React from "react"
|
||||
import { openExternal } from "../../scripts/utils"
|
||||
import { RSSSource, SourceOpenTarget } from "../../scripts/models/source"
|
||||
import { RSSItem } from "../../scripts/models/item"
|
||||
|
||||
@ -16,7 +15,7 @@ export interface CardProps {
|
||||
export class Card extends React.Component<CardProps> {
|
||||
openInBrowser = () => {
|
||||
this.props.markRead(this.props.item)
|
||||
openExternal(this.props.item.link)
|
||||
window.utils.openExternal(this.props.item.link)
|
||||
}
|
||||
|
||||
onClick = (e: React.MouseEvent) => {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import * as React from "react"
|
||||
import intl from "react-intl-universal"
|
||||
import { clipboard } from "electron"
|
||||
import { openExternal, cutText, googleSearch } from "../scripts/utils"
|
||||
import { cutText, googleSearch } from "../scripts/utils"
|
||||
import { ContextualMenu, IContextualMenuItem, ContextualMenuItemType, DirectionalHint } from "office-ui-fabric-react/lib/ContextualMenu"
|
||||
import { ContextMenuType } from "../scripts/models/app"
|
||||
import { RSSItem } from "../scripts/models/item"
|
||||
@ -51,7 +50,7 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
|
||||
iconProps: { iconName: "NavigateExternalInline" },
|
||||
onClick: () => {
|
||||
this.props.markRead(this.props.item)
|
||||
openExternal(this.props.item.link)
|
||||
window.utils.openExternal(this.props.item.link)
|
||||
}
|
||||
},
|
||||
this.props.item.hasRead
|
||||
@ -85,12 +84,12 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
|
||||
{
|
||||
key: "copyTitle",
|
||||
text: intl.get("context.copyTitle"),
|
||||
onClick: () => { clipboard.writeText(this.props.item.title) }
|
||||
onClick: () => { window.utils.writeClipboard(this.props.item.title) }
|
||||
},
|
||||
{
|
||||
key: "copyURL",
|
||||
text: intl.get("context.copyURL"),
|
||||
onClick: () => { clipboard.writeText(this.props.item.link) }
|
||||
onClick: () => { window.utils.writeClipboard(this.props.item.link) }
|
||||
}
|
||||
]
|
||||
case ContextMenuType.Text: return [
|
||||
@ -98,7 +97,7 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
|
||||
key: "copyText",
|
||||
text: intl.get("context.copy"),
|
||||
iconProps: { iconName: "Copy" },
|
||||
onClick: () => { clipboard.writeText(this.props.text) }
|
||||
onClick: () => { window.utils.writeClipboard(this.props.text) }
|
||||
},
|
||||
{
|
||||
key: "searchText",
|
||||
|
@ -1,6 +1,5 @@
|
||||
import * as React from "react"
|
||||
import intl from "react-intl-universal"
|
||||
import { remote } from "electron"
|
||||
import { Icon } from "@fluentui/react/lib/Icon"
|
||||
import { AppState } from "../scripts/models/app"
|
||||
import { ProgressIndicator } from "@fluentui/react"
|
||||
@ -20,25 +19,21 @@ type NavProps = {
|
||||
|
||||
type NavState = {
|
||||
maximized: boolean,
|
||||
window: Electron.BrowserWindow
|
||||
}
|
||||
|
||||
class Nav extends React.Component<NavProps, NavState> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
let window = remote.getCurrentWindow()
|
||||
window.on("maximize", () => {
|
||||
this.setState({ maximized: true })
|
||||
})
|
||||
window.on("unmaximize", () => {
|
||||
this.setState({ maximized: false })
|
||||
})
|
||||
window.utils.addWindowStateListener(this.setMaximizeState)
|
||||
this.state = {
|
||||
maximized: remote.getCurrentWindow().isMaximized(),
|
||||
window: window
|
||||
maximized: window.utils.isMaximized()
|
||||
}
|
||||
}
|
||||
|
||||
setMaximizeState = (state: boolean) => {
|
||||
this.setState({ maximized: state })
|
||||
}
|
||||
|
||||
navShortcutsHandler = (e: KeyboardEvent) => {
|
||||
if (!this.props.state.settings.display) {
|
||||
switch (e.key) {
|
||||
@ -77,18 +72,14 @@ class Nav extends React.Component<NavProps, NavState> {
|
||||
}
|
||||
|
||||
minimize = () => {
|
||||
this.state.window.minimize()
|
||||
window.utils.minimizeWindow()
|
||||
}
|
||||
maximize = () => {
|
||||
if (this.state.maximized) {
|
||||
this.state.window.unmaximize()
|
||||
} else {
|
||||
this.state.window.maximize()
|
||||
}
|
||||
window.utils.maximizeWindow()
|
||||
this.setState({ maximized: !this.state.maximized })
|
||||
}
|
||||
close = () => {
|
||||
this.state.window.close()
|
||||
window.utils.closeWindow()
|
||||
}
|
||||
|
||||
canFetch = () => this.props.state.sourceInit && this.props.state.feedInit && !this.props.state.fetchingItems
|
||||
|
@ -1,8 +1,6 @@
|
||||
import * as React from "react"
|
||||
import intl from "react-intl-universal"
|
||||
import { Stack, Link } from "@fluentui/react"
|
||||
import { openExternal } from "../../scripts/utils"
|
||||
import { remote } from "electron"
|
||||
|
||||
class AboutTab extends React.Component {
|
||||
render = () => (
|
||||
@ -10,12 +8,12 @@ class AboutTab extends React.Component {
|
||||
<Stack className="settings-about" horizontalAlign="center">
|
||||
<img src="icons/logo.svg" style={{width: 120, height: 120}} />
|
||||
<h3 style={{fontWeight: 600}}>Fluent Reader</h3>
|
||||
<small>{intl.get("settings.version")} {remote.app.getVersion()}</small>
|
||||
<small>{intl.get("settings.version")} {window.utils.getVersion()}</small>
|
||||
<p className="settings-hint">Copyright © 2020 Haoyuan Liu. All rights reserved.</p>
|
||||
<Stack horizontal horizontalAlign="center" tokens={{childrenGap: 12}}>
|
||||
<small><Link onClick={() => openExternal("https://github.com/yang991178/fluent-reader/wiki/Support#keyboard-shortcuts")}>{intl.get("settings.shortcuts")}</Link></small>
|
||||
<small><Link onClick={() => openExternal("https://github.com/yang991178/fluent-reader")}>{intl.get("settings.openSource")}</Link></small>
|
||||
<small><Link onClick={() => openExternal("https://github.com/yang991178/fluent-reader/issues")}>{intl.get("settings.feedback")}</Link></small>
|
||||
<small><Link onClick={() => window.utils.openExternal("https://github.com/yang991178/fluent-reader/wiki/Support#keyboard-shortcuts")}>{intl.get("settings.shortcuts")}</Link></small>
|
||||
<small><Link onClick={() => window.utils.openExternal("https://github.com/yang991178/fluent-reader")}>{intl.get("settings.openSource")}</Link></small>
|
||||
<small><Link onClick={() => window.utils.openExternal("https://github.com/yang991178/fluent-reader/issues")}>{intl.get("settings.feedback")}</Link></small>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
|
@ -4,13 +4,12 @@ import { urlTest, byteToMB, calculateItemSize } from "../../scripts/utils"
|
||||
import { ThemeSettings } from "../../schema-types"
|
||||
import { getThemeSettings, setThemeSettings, 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
|
||||
importAll: () => Promise<void>
|
||||
}
|
||||
|
||||
type AppTabState = {
|
||||
@ -38,7 +37,7 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
||||
}
|
||||
|
||||
getCacheSize = () => {
|
||||
remote.session.defaultSession.getCacheSize().then(size => {
|
||||
window.utils.getCacheSize().then(size => {
|
||||
this.setState({ cacheSize: byteToMB(size) })
|
||||
})
|
||||
}
|
||||
@ -49,7 +48,7 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
||||
}
|
||||
|
||||
clearCache = () => {
|
||||
remote.session.defaultSession.clearCache().then(() => {
|
||||
window.utils.clearCache().then(() => {
|
||||
this.getCacheSize()
|
||||
})
|
||||
}
|
||||
@ -110,18 +109,6 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
||||
this.setState({ themeSettings: option.key as ThemeSettings })
|
||||
}
|
||||
|
||||
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>
|
||||
@ -206,7 +193,7 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
|
||||
<Label>{intl.get("app.data")}</Label>
|
||||
<Stack horizontal>
|
||||
<Stack.Item>
|
||||
<PrimaryButton onClick={this.exportAll} text={intl.get("app.backup")} />
|
||||
<PrimaryButton onClick={exportAll} text={intl.get("app.backup")} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<DefaultButton onClick={this.props.importAll} text={intl.get("app.restore")} />
|
||||
|
@ -7,7 +7,7 @@ import { SourceRule, RuleActions } from "../../scripts/models/rule"
|
||||
import { FilterType } from "../../scripts/models/feed"
|
||||
import { validateRegex } from "../../scripts/utils"
|
||||
import { RSSItem } from "../../scripts/models/item"
|
||||
import Parser = require("@yang991178/rss-parser")
|
||||
import Parser from "@yang991178/rss-parser"
|
||||
|
||||
const actionKeyMap = {
|
||||
"r-true": "article.markRead",
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { remote } from "electron"
|
||||
import intl from "react-intl-universal"
|
||||
import { connect } from "react-redux"
|
||||
import { createSelector } from "reselect"
|
||||
@ -28,14 +27,12 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
settings: () => dispatch(toggleSettings()),
|
||||
search: () => dispatch(toggleSearch()),
|
||||
markAllRead: () => {
|
||||
remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
title: intl.get("nav.markAllRead"),
|
||||
message: intl.get("confirmMarkAll"),
|
||||
buttons: process.platform === "win32" ? ["Yes", "No"] : [intl.get("confirm"), intl.get("cancel")],
|
||||
defaultId: 0,
|
||||
cancelId: 1
|
||||
}).then(response => {
|
||||
if (response.response === 0) {
|
||||
window.utils.showMessageBox(
|
||||
intl.get("nav.markAllRead"),
|
||||
intl.get("confirmMarkAll"),
|
||||
intl.get("confirm"), intl.get("cancel")
|
||||
).then(response => {
|
||||
if (response) {
|
||||
dispatch(markAllRead())
|
||||
}
|
||||
})
|
||||
|
@ -1,11 +1,9 @@
|
||||
import intl from "react-intl-universal"
|
||||
import { connect } from "react-redux"
|
||||
import { 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"
|
||||
import { importAll } from "../../scripts/settings"
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
setLanguage: (option: string) => {
|
||||
@ -24,28 +22,10 @@ const mapDispatchToProps = dispatch => ({
|
||||
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])
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
importAll: async () => {
|
||||
dispatch(saveSettings())
|
||||
let cancelled = await importAll()
|
||||
if (cancelled) dispatch(saveSettings())
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
import intl from "react-intl-universal"
|
||||
import { remote } from "electron"
|
||||
import { connect } from "react-redux"
|
||||
import { createSelector } from "reselect"
|
||||
import { RootState } from "../../scripts/reducer"
|
||||
@ -31,28 +29,8 @@ const mapDispatchToProps = (dispatch: AppDispatch) => {
|
||||
},
|
||||
deleteSource: (source: RSSSource) => dispatch(deleteSource(source)),
|
||||
deleteSources: (sources: RSSSource[]) => dispatch(deleteSources(sources)),
|
||||
importOPML: () => {
|
||||
remote.dialog.showOpenDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
filters: [{ name: intl.get("sources.opmlFile"), extensions: ["xml", "opml"] }],
|
||||
properties: ["openFile"]
|
||||
}
|
||||
).then(result => {
|
||||
if (!result.canceled && result.filePaths.length > 0) dispatch(importOPML(result.filePaths[0]))
|
||||
})
|
||||
},
|
||||
exportOPML: () => {
|
||||
remote.dialog.showSaveDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
defaultPath: "*/Fluent_Reader_Export.opml",
|
||||
filters: [{ name: intl.get("sources.opmlFile"), extensions: ["opml"] }]
|
||||
}
|
||||
).then(result => {
|
||||
if (!result.canceled) dispatch(exportOPML(result.filePath))
|
||||
})
|
||||
}
|
||||
importOPML: () => dispatch(importOPML()),
|
||||
exportOPML: () => dispatch(exportOPML())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { app, ipcMain, BrowserWindow, Menu, nativeTheme } from "electron"
|
||||
import windowStateKeeper = require("electron-window-state")
|
||||
import { ThemeSettings } from "./schema-types"
|
||||
import { store, setThemeListener } from "./main/settings"
|
||||
import { app, ipcMain, Menu, nativeTheme } from "electron"
|
||||
import { ThemeSettings, SchemaTypes } from "./schema-types"
|
||||
import { store } from "./main/settings"
|
||||
import performUpdate from "./main/update-scripts"
|
||||
import path = require("path")
|
||||
import { WindowManager } from "./main/window"
|
||||
|
||||
if (!process.mas) {
|
||||
const locked = app.requestSingleInstanceLock()
|
||||
@ -12,55 +11,15 @@ if (!process.mas) {
|
||||
}
|
||||
}
|
||||
|
||||
let mainWindow: BrowserWindow
|
||||
let restarting: boolean
|
||||
let restarting = false
|
||||
|
||||
function init(setTheme = true) {
|
||||
restarting = false
|
||||
function init() {
|
||||
performUpdate(store)
|
||||
if (setTheme) nativeTheme.themeSource = store.get("theme", ThemeSettings.Default)
|
||||
nativeTheme.themeSource = store.get("theme", ThemeSettings.Default)
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
function createWindow() {
|
||||
let mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1200,
|
||||
defaultHeight: 700,
|
||||
})
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow({
|
||||
title: "Fluent Reader",
|
||||
backgroundColor: process.platform === "darwin" ? "#00000000" : (nativeTheme.shouldUseDarkColors ? "#282828" : "#faf9f8"),
|
||||
vibrancy: "sidebar",
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
minWidth: 992,
|
||||
minHeight: 600,
|
||||
frame: process.platform === "darwin",
|
||||
titleBarStyle: "hiddenInset",
|
||||
fullscreenable: false,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
webviewTag: true,
|
||||
enableRemoteModule: true,
|
||||
preload: path.join(app.getAppPath(), (app.isPackaged ? "dist/" : "") + "preload.js")
|
||||
}
|
||||
})
|
||||
mainWindowState.manage(mainWindow)
|
||||
mainWindow.on("ready-to-show", () => {
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
if (!app.isPackaged) mainWindow.webContents.openDevTools()
|
||||
})
|
||||
setThemeListener(mainWindow)
|
||||
// and load the index.html of the app.
|
||||
mainWindow.loadFile((app.isPackaged ? "dist/" : "") + "index.html", )
|
||||
}
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
const template = [
|
||||
{
|
||||
@ -86,34 +45,29 @@ if (process.platform === "darwin") {
|
||||
Menu.setApplicationMenu(null)
|
||||
}
|
||||
|
||||
app.on("ready", createWindow)
|
||||
const winManager = new WindowManager()
|
||||
|
||||
app.on("second-instance", () => {
|
||||
if (mainWindow !== null) {
|
||||
mainWindow.focus()
|
||||
app.on("window-all-closed", () => {
|
||||
if (winManager.hasWindow()) {
|
||||
winManager.mainWindow.webContents.session.clearStorageData({ storages: ["cookies"] })
|
||||
}
|
||||
})
|
||||
|
||||
app.on("window-all-closed", function () {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.session.clearStorageData({ storages: ["cookies"] })
|
||||
}
|
||||
mainWindow = null
|
||||
winManager.mainWindow = null
|
||||
if (restarting) {
|
||||
init(false)
|
||||
createWindow()
|
||||
restarting = false
|
||||
winManager.createWindow()
|
||||
} else if (process.platform !== "darwin") {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on("activate", function () {
|
||||
if (mainWindow === null) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on("restart", () => {
|
||||
ipcMain.handle("import-all-settings", (_, configs: SchemaTypes) => {
|
||||
restarting = true
|
||||
mainWindow.close()
|
||||
store.clear()
|
||||
for (let [key, value] of Object.entries(configs)) {
|
||||
// @ts-ignore
|
||||
store.set(key, value)
|
||||
}
|
||||
performUpdate(store)
|
||||
nativeTheme.themeSource = store.get("theme", ThemeSettings.Default)
|
||||
winManager.mainWindow.close()
|
||||
})
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Store = require("electron-store")
|
||||
import { SchemaTypes, SourceGroup, ViewType, ThemeSettings } from "../schema-types"
|
||||
import { ipcMain, session, nativeTheme, BrowserWindow, app } from "electron"
|
||||
import { WindowManager } from "./window"
|
||||
|
||||
export const store = new Store<SchemaTypes>()
|
||||
|
||||
@ -76,12 +77,14 @@ ipcMain.handle("set-theme", (_, theme: ThemeSettings) => {
|
||||
ipcMain.on("get-theme-dark-color", (event) => {
|
||||
event.returnValue = nativeTheme.shouldUseDarkColors
|
||||
})
|
||||
export function setThemeListener(window: BrowserWindow) {
|
||||
export function setThemeListener(manager: WindowManager) {
|
||||
nativeTheme.removeAllListeners()
|
||||
nativeTheme.on("updated", () => {
|
||||
let contents = window.webContents
|
||||
if (!contents.isDestroyed()) {
|
||||
contents.send("theme-updated", nativeTheme.shouldUseDarkColors)
|
||||
if (manager.hasWindow()) {
|
||||
let contents = manager.mainWindow.webContents
|
||||
if (!contents.isDestroyed()) {
|
||||
contents.send("theme-updated", nativeTheme.shouldUseDarkColors)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -101,3 +104,11 @@ ipcMain.on("get-locale", (event) => {
|
||||
let locale = setting === "default" ? app.getLocale() : setting
|
||||
event.returnValue = locale
|
||||
})
|
||||
|
||||
ipcMain.on("get-all-settings", (event) => {
|
||||
let output = {}
|
||||
for (let [key, value] of store) {
|
||||
output[key] = value
|
||||
}
|
||||
event.returnValue = output
|
||||
})
|
||||
|
116
src/main/utils.ts
Normal file
116
src/main/utils.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { ipcMain, shell, dialog, app, session, webContents, clipboard } from "electron"
|
||||
import { WindowManager } from "./window"
|
||||
import fs = require("fs")
|
||||
|
||||
export function setUtilsListeners(manager: WindowManager) {
|
||||
ipcMain.on("get-version", (event) => {
|
||||
event.returnValue = app.getVersion()
|
||||
})
|
||||
|
||||
ipcMain.handle("open-external", (_, url: string) => {
|
||||
if (url.startsWith("https://") || url.startsWith("http://"))
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
ipcMain.handle("show-error-box", (_, title, content) => {
|
||||
dialog.showErrorBox(title, content)
|
||||
})
|
||||
|
||||
ipcMain.handle("show-message-box", async (_, title, message, confirm, cancel, defaultCancel, type) => {
|
||||
if (manager.hasWindow()) {
|
||||
let response = await dialog.showMessageBox(manager.mainWindow, {
|
||||
type: type,
|
||||
title: title,
|
||||
message: message,
|
||||
buttons: process.platform === "win32" ? ["Yes", "No"] : [confirm, cancel],
|
||||
cancelId: 1,
|
||||
defaultId: defaultCancel ? 1 : 0
|
||||
})
|
||||
return response.response === 0
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle("show-save-dialog", async (_, filters: Electron.FileFilter[], path: string) => {
|
||||
ipcMain.removeAllListeners("write-save-result")
|
||||
if (manager.hasWindow()) {
|
||||
let response = await dialog.showSaveDialog(manager.mainWindow, {
|
||||
defaultPath: path,
|
||||
filters: filters
|
||||
})
|
||||
if (!response.canceled) {
|
||||
ipcMain.handleOnce("write-save-result", (_, result, errmsg) => {
|
||||
fs.writeFile(response.filePath, result, (err) => {
|
||||
if (err) dialog.showErrorBox(errmsg, String(err))
|
||||
})
|
||||
})
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
ipcMain.handle("show-open-dialog", async (_, filters: Electron.FileFilter[]) => {
|
||||
if (manager.hasWindow()) {
|
||||
let response = await dialog.showOpenDialog(manager.mainWindow, {
|
||||
filters: filters,
|
||||
properties: ["openFile"]
|
||||
})
|
||||
if (!response.canceled) {
|
||||
try {
|
||||
return await fs.promises.readFile(response.filePaths[0], "utf-8")
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
ipcMain.handle("get-cache", async () => {
|
||||
return await session.defaultSession.getCacheSize()
|
||||
})
|
||||
|
||||
ipcMain.handle("clear-cache", async () => {
|
||||
await session.defaultSession.clearCache()
|
||||
})
|
||||
|
||||
ipcMain.handle("add-webview-keydown-listener", (_, id) => {
|
||||
let contents = webContents.fromId(id)
|
||||
contents.on("before-input-event", (_, input) => {
|
||||
if (manager.hasWindow()) {
|
||||
let contents = manager.mainWindow.webContents
|
||||
if (!contents.isDestroyed()) {
|
||||
contents.send("webview-keydown", input)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle("write-clipboard", (_, text) => {
|
||||
clipboard.writeText(text)
|
||||
})
|
||||
|
||||
ipcMain.handle("close-window", () => {
|
||||
if (manager.hasWindow()) manager.mainWindow.close()
|
||||
})
|
||||
|
||||
ipcMain.handle("minimize-window", () => {
|
||||
if (manager.hasWindow()) manager.mainWindow.minimize()
|
||||
})
|
||||
|
||||
ipcMain.handle("maximize-window", () => {
|
||||
if (manager.hasWindow) {
|
||||
if (manager.mainWindow.isMaximized()) {
|
||||
manager.mainWindow.unmaximize()
|
||||
} else {
|
||||
manager.mainWindow.maximize()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on("is-maximized", (event) => {
|
||||
event.returnValue = Boolean(manager.mainWindow) && manager.mainWindow.isMaximized()
|
||||
})
|
||||
}
|
86
src/main/window.ts
Normal file
86
src/main/window.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import windowStateKeeper = require("electron-window-state")
|
||||
import { BrowserWindow, nativeTheme, app } from "electron"
|
||||
import path = require("path")
|
||||
import { setThemeListener } from "./settings"
|
||||
import { setUtilsListeners } from "./utils"
|
||||
|
||||
export class WindowManager {
|
||||
mainWindow: BrowserWindow = null
|
||||
private mainWindowState: windowStateKeeper.State
|
||||
|
||||
constructor() {
|
||||
this.init()
|
||||
}
|
||||
|
||||
private init = () => {
|
||||
app.on("ready", () => {
|
||||
this.mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1200,
|
||||
defaultHeight: 700,
|
||||
})
|
||||
this.setListeners()
|
||||
this.createWindow()
|
||||
})
|
||||
}
|
||||
|
||||
private setListeners = () => {
|
||||
setThemeListener(this)
|
||||
setUtilsListeners(this)
|
||||
|
||||
app.on("second-instance", () => {
|
||||
if (this.mainWindow !== null) {
|
||||
this.mainWindow.focus()
|
||||
}
|
||||
})
|
||||
|
||||
app.on("activate", () => {
|
||||
if (this.mainWindow === null) {
|
||||
this.createWindow()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
createWindow = () => {
|
||||
if (!this.hasWindow()) {
|
||||
this.mainWindow = new BrowserWindow({
|
||||
title: "Fluent Reader",
|
||||
backgroundColor: process.platform === "darwin" ? "#00000000" : (nativeTheme.shouldUseDarkColors ? "#282828" : "#faf9f8"),
|
||||
vibrancy: "sidebar",
|
||||
x: this.mainWindowState.x,
|
||||
y: this.mainWindowState.y,
|
||||
width: this.mainWindowState.width,
|
||||
height: this.mainWindowState.height,
|
||||
minWidth: 992,
|
||||
minHeight: 600,
|
||||
frame: process.platform === "darwin",
|
||||
titleBarStyle: "hiddenInset",
|
||||
fullscreenable: false,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
webviewTag: true,
|
||||
enableRemoteModule: true,
|
||||
preload: path.join(app.getAppPath(), (app.isPackaged ? "dist/" : "") + "preload.js")
|
||||
}
|
||||
})
|
||||
this.mainWindowState.manage(this.mainWindow)
|
||||
this.mainWindow.on("ready-to-show", () => {
|
||||
this.mainWindow.show()
|
||||
this.mainWindow.focus()
|
||||
if (!app.isPackaged) this.mainWindow.webContents.openDevTools()
|
||||
})
|
||||
this.mainWindow.loadFile((app.isPackaged ? "dist/" : "") + "index.html", )
|
||||
|
||||
this.mainWindow.on("maximize", () => {
|
||||
this.mainWindow.webContents.send("maximized")
|
||||
})
|
||||
this.mainWindow.on("unmaximize", () => {
|
||||
this.mainWindow.webContents.send("unmaximized")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
hasWindow = () => {
|
||||
return this.mainWindow !== null && !this.mainWindow.isDestroyed()
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
import { contextBridge } from "electron"
|
||||
import SettingsBridge from "./bridges/settings"
|
||||
import settingsBridge from "./bridges/settings"
|
||||
import utilsBridge from "./bridges/utils"
|
||||
|
||||
window.settings = SettingsBridge
|
||||
window.settings = settingsBridge
|
||||
window.utils = utilsBridge
|
@ -1,4 +1,4 @@
|
||||
import Datastore = require("nedb")
|
||||
import Datastore from "nedb"
|
||||
import { RSSSource } from "./models/source"
|
||||
import { RSSItem } from "./models/item"
|
||||
|
||||
|
@ -1,11 +1,9 @@
|
||||
import fs = require("fs")
|
||||
import intl from "react-intl-universal"
|
||||
import { SourceActionTypes, ADD_SOURCE, DELETE_SOURCE, addSource, RSSSource } from "./source"
|
||||
import { SourceGroup } from "../../schema-types"
|
||||
import { ActionStatus, AppThunk, domParser, AppDispatch } from "../utils"
|
||||
import { ActionStatus, AppThunk, domParser } from "../utils"
|
||||
import { saveSettings } from "./app"
|
||||
import { fetchItemsIntermediate, fetchItemsRequest, fetchItemsSuccess } from "./item"
|
||||
import { remote } from "electron"
|
||||
|
||||
export const CREATE_SOURCE_GROUP = "CREATE_SOURCE_GROUP"
|
||||
export const ADD_SOURCE_TO_GROUP = "ADD_SOURCE_TO_GROUP"
|
||||
@ -167,12 +165,11 @@ function outlineToSource(outline: Element): [ReturnType<typeof addSource>, strin
|
||||
}
|
||||
}
|
||||
|
||||
export function importOPML(path: string): AppThunk {
|
||||
export function importOPML(): AppThunk {
|
||||
return async (dispatch) => {
|
||||
fs.readFile(path, "utf-8", async (err, data) => {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
} else {
|
||||
const filters = [{ name: intl.get("sources.opmlFile"), extensions: ["xml", "opml"] }]
|
||||
window.utils.showOpenDialog(filters).then(data => {
|
||||
if (data) {
|
||||
dispatch(saveSettings())
|
||||
let doc = domParser.parseFromString(data, "text/xml").getElementsByTagName("body")
|
||||
if (doc.length == 0) {
|
||||
@ -182,7 +179,7 @@ export function importOPML(path: string): AppThunk {
|
||||
let parseError = doc[0].getElementsByTagName("parsererror")
|
||||
if (parseError.length > 0) {
|
||||
dispatch(saveSettings())
|
||||
remote.dialog.showErrorBox(intl.get("sources.errorParse"), intl.get("sources.errorParseHint"))
|
||||
window.utils.showErrorBox(intl.get("sources.errorParse"), intl.get("sources.errorParseHint"))
|
||||
return
|
||||
}
|
||||
let sources: [ReturnType<typeof addSource>, number, string][] = []
|
||||
@ -214,7 +211,7 @@ export function importOPML(path: string): AppThunk {
|
||||
dispatch(fetchItemsSuccess([], {}))
|
||||
dispatch(saveSettings())
|
||||
if (errors.length > 0) {
|
||||
remote.dialog.showErrorBox(
|
||||
window.utils.showErrorBox(
|
||||
intl.get("sources.errorImport", { count: errors.length }),
|
||||
errors.map(e => {
|
||||
return e[0] + "\n" + String(e[1])
|
||||
@ -236,33 +233,35 @@ function sourceToOutline(source: RSSSource, xml: Document) {
|
||||
return outline
|
||||
}
|
||||
|
||||
export function exportOPML(path: string): AppThunk {
|
||||
export function exportOPML(): AppThunk {
|
||||
return (_, getState) => {
|
||||
let state = getState()
|
||||
let xml = domParser.parseFromString(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><opml version=\"1.0\"><head><title>Fluent Reader Export</title></head><body></body></opml>",
|
||||
"text/xml"
|
||||
)
|
||||
let body = xml.getElementsByTagName("body")[0]
|
||||
for (let group of state.groups) {
|
||||
if (group.isMultiple) {
|
||||
let outline = xml.createElement("outline")
|
||||
outline.setAttribute("text", group.name)
|
||||
outline.setAttribute("name", group.name)
|
||||
for (let sid of group.sids) {
|
||||
outline.appendChild(sourceToOutline(state.sources[sid], xml))
|
||||
const filters = [{ name: intl.get("sources.opmlFile"), extensions: ["opml"] }]
|
||||
window.utils.showSaveDialog(filters, "*/Fluent_Reader_Export.opml").then(write => {
|
||||
if (write) {
|
||||
let state = getState()
|
||||
let xml = domParser.parseFromString(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><opml version=\"1.0\"><head><title>Fluent Reader Export</title></head><body></body></opml>",
|
||||
"text/xml"
|
||||
)
|
||||
let body = xml.getElementsByTagName("body")[0]
|
||||
for (let group of state.groups) {
|
||||
if (group.isMultiple) {
|
||||
let outline = xml.createElement("outline")
|
||||
outline.setAttribute("text", group.name)
|
||||
outline.setAttribute("name", group.name)
|
||||
for (let sid of group.sids) {
|
||||
outline.appendChild(sourceToOutline(state.sources[sid], xml))
|
||||
}
|
||||
body.appendChild(outline)
|
||||
} else {
|
||||
body.appendChild(sourceToOutline(state.sources[group.sids[0]], xml))
|
||||
}
|
||||
}
|
||||
body.appendChild(outline)
|
||||
} else {
|
||||
body.appendChild(sourceToOutline(state.sources[group.sids[0]], xml))
|
||||
let serializer = new XMLSerializer()
|
||||
write(serializer.serializeToString(xml), intl.get("settings.writeError"))
|
||||
}
|
||||
}
|
||||
let serializer = new XMLSerializer()
|
||||
fs.writeFile(path, serializer.serializeToString(xml), (err) => {
|
||||
if (err) remote.dialog.showErrorBox(intl.get("settings.writeError"), String(err))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type GroupState = SourceGroup[]
|
||||
|
@ -1,9 +1,9 @@
|
||||
import * as db from "../db"
|
||||
import intl from "react-intl-universal"
|
||||
import { domParser, htmlDecode, ActionStatus, AppThunk, openExternal } from "../utils"
|
||||
import { domParser, htmlDecode, ActionStatus, AppThunk } from "../utils"
|
||||
import { RSSSource } from "./source"
|
||||
import { FeedActionTypes, INIT_FEED, LOAD_MORE } from "./feed"
|
||||
import Parser = require("@yang991178/rss-parser")
|
||||
import Parser from "@yang991178/rss-parser"
|
||||
|
||||
export class RSSItem {
|
||||
_id: string
|
||||
@ -279,7 +279,7 @@ export function itemShortcuts(item: RSSItem, key: string): AppThunk {
|
||||
break
|
||||
case "b": case "B":
|
||||
if (!item.hasRead) dispatch(markRead(item))
|
||||
openExternal(item.link)
|
||||
window.utils.openExternal(item.link)
|
||||
break
|
||||
case "s": case "S":
|
||||
dispatch(toggleStarred(item))
|
||||
|
@ -1,10 +1,9 @@
|
||||
import Parser = require("@yang991178/rss-parser")
|
||||
import Parser from "@yang991178/rss-parser"
|
||||
import intl from "react-intl-universal"
|
||||
import * as db from "../db"
|
||||
import { fetchFavicon, ActionStatus, AppThunk, parseRSS } from "../utils"
|
||||
import { RSSItem, insertItems, ItemActionTypes, FETCH_ITEMS, MARK_READ, MARK_UNREAD, MARK_ALL_READ } from "./item"
|
||||
import { saveSettings } from "./app"
|
||||
import { remote } from "electron"
|
||||
import { SourceRule } from "./rule"
|
||||
|
||||
export enum SourceOpenTarget {
|
||||
@ -256,7 +255,7 @@ export function addSource(url: string, name: string = null, batch = false): AppT
|
||||
.catch(e => {
|
||||
dispatch(addSourceFailure(e, batch))
|
||||
if (!batch) {
|
||||
remote.dialog.showErrorBox(intl.get("sources.errorAdd"), String(e))
|
||||
window.utils.showErrorBox(intl.get("sources.errorAdd"), String(e))
|
||||
}
|
||||
return Promise.reject(e)
|
||||
})
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { remote, ipcRenderer } from "electron"
|
||||
import { IPartialTheme, loadTheme } from "@fluentui/react"
|
||||
import locales from "./i18n/_locales"
|
||||
import Store = require("electron-store")
|
||||
import { ThemeSettings, SchemaTypes } from "../schema-types"
|
||||
import fs = require("fs")
|
||||
import intl from "react-intl-universal"
|
||||
|
||||
export const store = new Store<SchemaTypes>()
|
||||
@ -61,65 +59,58 @@ export function getCurrentLocale() {
|
||||
return (locale in locales) ? locale : "en-US"
|
||||
}
|
||||
|
||||
export function exportAll(path: string) {
|
||||
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))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function importAll(path) {
|
||||
fs.readFile(path, "utf-8", async (err, data) => {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
} else {
|
||||
let configs = JSON.parse(data)
|
||||
export function exportAll() {
|
||||
const filters = [{ name: intl.get("app.frData"), extensions: ["frdata"] }]
|
||||
window.utils.showSaveDialog(filters, "*/Fluent_Reader_Backup.frdata").then(write => {
|
||||
if (write) {
|
||||
let output = window.settings.getAll()
|
||||
output["nedb"] = {}
|
||||
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()
|
||||
let hasTheme = false
|
||||
for (let [key, value] of Object.entries(configs)) {
|
||||
if (key === THEME_STORE_KEY) {
|
||||
setThemeSettings(value as ThemeSettings)
|
||||
hasTheme = true
|
||||
} else {
|
||||
// @ts-ignore
|
||||
store.set(key, value)
|
||||
}
|
||||
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 {
|
||||
write(JSON.stringify(output), intl.get("settings.writeError"))
|
||||
}
|
||||
if (!hasTheme) setThemeSettings(ThemeSettings.Default)
|
||||
ipcRenderer.send("restart")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function importAll() {
|
||||
const filters = [{ name: intl.get("app.frData"), extensions: ["frdata"] }]
|
||||
let data = await window.utils.showOpenDialog(filters)
|
||||
if (!data) return true
|
||||
let confirmed = await window.utils.showMessageBox(
|
||||
intl.get("app.restore"),
|
||||
intl.get("app.confirmImport"),
|
||||
intl.get("confirm"), intl.get("cancel"),
|
||||
true, "warning"
|
||||
)
|
||||
if (!confirmed) return true
|
||||
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
|
||||
window.settings.setAll(configs)
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { shell, remote } from "electron"
|
||||
import { ThunkAction, ThunkDispatch } from "redux-thunk"
|
||||
import { AnyAction } from "redux"
|
||||
import { RootState } from "./reducer"
|
||||
import Parser from "@yang991178/rss-parser"
|
||||
import Url from "url"
|
||||
|
||||
export enum ActionStatus {
|
||||
Request, Success, Failure, Intermediate
|
||||
@ -16,7 +17,6 @@ export type AppThunk<ReturnType = void> = ThunkAction<
|
||||
|
||||
export type AppDispatch = ThunkDispatch<RootState, undefined, AnyAction>
|
||||
|
||||
import Parser = require("@yang991178/rss-parser")
|
||||
const rssParser = new Parser({
|
||||
customFields: {
|
||||
item: [
|
||||
@ -41,7 +41,6 @@ export async function parseRSS(url: string) {
|
||||
|
||||
export const domParser = new DOMParser()
|
||||
|
||||
import Url = require("url")
|
||||
export async function fetchFavicon(url: string) {
|
||||
try {
|
||||
let result = await fetch(url, { credentials: "omit" })
|
||||
@ -77,21 +76,16 @@ export function htmlDecode(input: string) {
|
||||
return doc.documentElement.textContent
|
||||
}
|
||||
|
||||
export function openExternal(url: string) {
|
||||
if (url.startsWith("https://") || url.startsWith("http://"))
|
||||
shell.openExternal(url)
|
||||
}
|
||||
|
||||
export const urlTest = (s: string) =>
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi.test(s)
|
||||
|
||||
export const getWindowBreakpoint = () => remote.getCurrentWindow().getSize()[0] >= 1441
|
||||
export const getWindowBreakpoint = () => window.outerWidth >= 1441
|
||||
|
||||
export const cutText = (s: string, length: number) => {
|
||||
return (s.length <= length) ? s : s.slice(0, length) + "…"
|
||||
}
|
||||
|
||||
export const googleSearch = (text: string) => openExternal("https://www.google.com/search?q=" + encodeURIComponent(text))
|
||||
export const googleSearch = (text: string) => window.utils.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>()
|
||||
@ -114,6 +108,17 @@ export function byteToMB(B: number) {
|
||||
return MB + "MB"
|
||||
}
|
||||
|
||||
function byteLength(str: string) {
|
||||
var s = str.length;
|
||||
for (var i = str.length - 1; i >= 0; i--) {
|
||||
var code = str.charCodeAt(i);
|
||||
if (code > 0x7f && code <= 0x7ff) s++;
|
||||
else if (code > 0x7ff && code <= 0xffff) s += 2;
|
||||
if (code >= 0xDC00 && code <= 0xDFFF) i--; //trail surrogate
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
export function calculateItemSize(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let openRequest = window.indexedDB.open("NeDB")
|
||||
@ -122,8 +127,7 @@ export function calculateItemSize(): Promise<number> {
|
||||
let objectStore = db.transaction("nedbdata").objectStore("nedbdata")
|
||||
let getRequest = objectStore.get("items")
|
||||
getRequest.onsuccess = () => {
|
||||
let resultBuffer = Buffer.from(getRequest.result)
|
||||
resolve(resultBuffer.length)
|
||||
resolve(byteLength(getRequest.result))
|
||||
}
|
||||
getRequest.onerror = () => reject()
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user