add utils bridge

This commit is contained in:
刘浩远 2020-06-30 15:29:39 +08:00
parent b4c1ddd587
commit 394643b95e
23 changed files with 486 additions and 305 deletions

View File

@ -1,7 +1,7 @@
import { SourceGroup, ViewType, ThemeSettings } from "../schema-types" import { SourceGroup, ViewType, ThemeSettings, SchemaTypes } from "../schema-types"
import { ipcRenderer } from "electron" import { ipcRenderer } from "electron"
const SettingsBridge = { const settingsBridge = {
saveGroups: (groups: SourceGroup[]) => { saveGroups: (groups: SourceGroup[]) => {
ipcRenderer.invoke("set-groups", groups) ipcRenderer.invoke("set-groups", groups)
}, },
@ -59,13 +59,21 @@ const SettingsBridge = {
}, },
getCurrentLocale: (): string => { getCurrentLocale: (): string => {
return ipcRenderer.sendSync("get-locale") return ipcRenderer.sendSync("get-locale")
} },
getAll: () => {
return ipcRenderer.sendSync("get-all-settings") as Object
},
setAll: (configs) => {
ipcRenderer.invoke("import-all-settings", configs)
},
} }
declare global { declare global {
interface Window { interface Window {
settings: typeof SettingsBridge settings: typeof settingsBridge
} }
} }
export default SettingsBridge export default settingsBridge

85
src/bridges/utils.ts Normal file
View 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

View File

@ -2,11 +2,9 @@ import * as React from "react"
import intl from "react-intl-universal" import intl from "react-intl-universal"
import { renderToString } from "react-dom/server" import { renderToString } from "react-dom/server"
import { RSSItem } from "../scripts/models/item" import { RSSItem } from "../scripts/models/item"
import { openExternal } from "../scripts/utils"
import { Stack, CommandBarButton, IContextualMenuProps, FocusZone } from "@fluentui/react" import { Stack, CommandBarButton, IContextualMenuProps, FocusZone } from "@fluentui/react"
import { RSSSource, SourceOpenTarget } from "../scripts/models/source" import { RSSSource, SourceOpenTarget } from "../scripts/models/source"
import { store } from "../scripts/settings" import { store } from "../scripts/settings"
import { clipboard, remote } from "electron"
const FONT_SIZE_STORE_KEY = "fontSize" const FONT_SIZE_STORE_KEY = "fontSize"
const FONT_SIZE_OPTIONS = [12, 13, 14, 15, 16, 17, 18, 19, 20] 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", key: "copyURL",
text: intl.get("context.copyURL"), text: intl.get("context.copyURL"),
iconProps: { iconName: "Link" }, iconProps: { iconName: "Link" },
onClick: () => { clipboard.writeText(this.props.item.link) } onClick: () => { window.utils.writeClipboard(this.props.item.link) }
}, },
{ {
key: "toggleHidden", key: "toggleHidden",
@ -84,7 +82,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
ipcHandler = event => { ipcHandler = event => {
switch (event.channel) { switch (event.channel) {
case "request-navigation": { case "request-navigation": {
openExternal(event.args[0]) window.utils.openExternal(event.args[0])
break break
} }
case "context-menu": { case "context-menu": {
@ -96,13 +94,13 @@ class Article extends React.Component<ArticleProps, ArticleState> {
} }
} }
popUpHandler = event => { popUpHandler = event => {
openExternal(event.url) window.utils.openExternal(event.url)
} }
navigationHandler = event => { navigationHandler = event => {
openExternal(event.url) window.utils.openExternal(event.url)
this.props.dismiss() this.props.dismiss()
} }
keyDownHandler = (_, input) => { keyDownHandler = (input: Electron.Input) => {
if (input.type === "keyDown") { if (input.type === "keyDown") {
switch (input.key) { switch (input.key) {
case "Escape": case "Escape":
@ -140,8 +138,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
webview.addEventListener("new-window", this.popUpHandler) webview.addEventListener("new-window", this.popUpHandler)
webview.addEventListener("will-navigate", this.navigationHandler) webview.addEventListener("will-navigate", this.navigationHandler)
webview.addEventListener("dom-ready", () => { webview.addEventListener("dom-ready", () => {
let webContents = remote.webContents.fromId(webview.getWebContentsId()) window.utils.addWebviewKeydownListener(webview.getWebContentsId(), this.keyDownHandler)
webContents.on("before-input-event", this.keyDownHandler)
}) })
this.webview = webview this.webview = webview
webview.focus() webview.focus()
@ -163,7 +160,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
} }
openInBrowser = () => { openInBrowser = () => {
openExternal(this.props.item.link) window.utils.openExternal(this.props.item.link)
} }
toggleWebpage = () => { toggleWebpage = () => {

View File

@ -1,5 +1,4 @@
import * as React from "react" import * as React from "react"
import { openExternal } from "../../scripts/utils"
import { RSSSource, SourceOpenTarget } from "../../scripts/models/source" import { RSSSource, SourceOpenTarget } from "../../scripts/models/source"
import { RSSItem } from "../../scripts/models/item" import { RSSItem } from "../../scripts/models/item"
@ -16,7 +15,7 @@ export interface CardProps {
export class Card extends React.Component<CardProps> { export class Card extends React.Component<CardProps> {
openInBrowser = () => { openInBrowser = () => {
this.props.markRead(this.props.item) this.props.markRead(this.props.item)
openExternal(this.props.item.link) window.utils.openExternal(this.props.item.link)
} }
onClick = (e: React.MouseEvent) => { onClick = (e: React.MouseEvent) => {

View File

@ -1,7 +1,6 @@
import * as React from "react" import * as React from "react"
import intl from "react-intl-universal" import intl from "react-intl-universal"
import { clipboard } from "electron" import { cutText, googleSearch } from "../scripts/utils"
import { openExternal, cutText, googleSearch } from "../scripts/utils"
import { ContextualMenu, IContextualMenuItem, ContextualMenuItemType, DirectionalHint } from "office-ui-fabric-react/lib/ContextualMenu" import { ContextualMenu, IContextualMenuItem, ContextualMenuItemType, DirectionalHint } from "office-ui-fabric-react/lib/ContextualMenu"
import { ContextMenuType } from "../scripts/models/app" import { ContextMenuType } from "../scripts/models/app"
import { RSSItem } from "../scripts/models/item" import { RSSItem } from "../scripts/models/item"
@ -51,7 +50,7 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
iconProps: { iconName: "NavigateExternalInline" }, iconProps: { iconName: "NavigateExternalInline" },
onClick: () => { onClick: () => {
this.props.markRead(this.props.item) this.props.markRead(this.props.item)
openExternal(this.props.item.link) window.utils.openExternal(this.props.item.link)
} }
}, },
this.props.item.hasRead this.props.item.hasRead
@ -85,12 +84,12 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
{ {
key: "copyTitle", key: "copyTitle",
text: intl.get("context.copyTitle"), text: intl.get("context.copyTitle"),
onClick: () => { clipboard.writeText(this.props.item.title) } onClick: () => { window.utils.writeClipboard(this.props.item.title) }
}, },
{ {
key: "copyURL", key: "copyURL",
text: intl.get("context.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 [ case ContextMenuType.Text: return [
@ -98,7 +97,7 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
key: "copyText", key: "copyText",
text: intl.get("context.copy"), text: intl.get("context.copy"),
iconProps: { iconName: "Copy" }, iconProps: { iconName: "Copy" },
onClick: () => { clipboard.writeText(this.props.text) } onClick: () => { window.utils.writeClipboard(this.props.text) }
}, },
{ {
key: "searchText", key: "searchText",

View File

@ -1,6 +1,5 @@
import * as React from "react" import * as React from "react"
import intl from "react-intl-universal" import intl from "react-intl-universal"
import { remote } from "electron"
import { Icon } from "@fluentui/react/lib/Icon" import { Icon } from "@fluentui/react/lib/Icon"
import { AppState } from "../scripts/models/app" import { AppState } from "../scripts/models/app"
import { ProgressIndicator } from "@fluentui/react" import { ProgressIndicator } from "@fluentui/react"
@ -20,25 +19,21 @@ type NavProps = {
type NavState = { type NavState = {
maximized: boolean, maximized: boolean,
window: Electron.BrowserWindow
} }
class Nav extends React.Component<NavProps, NavState> { class Nav extends React.Component<NavProps, NavState> {
constructor(props) { constructor(props) {
super(props) super(props)
let window = remote.getCurrentWindow() window.utils.addWindowStateListener(this.setMaximizeState)
window.on("maximize", () => {
this.setState({ maximized: true })
})
window.on("unmaximize", () => {
this.setState({ maximized: false })
})
this.state = { this.state = {
maximized: remote.getCurrentWindow().isMaximized(), maximized: window.utils.isMaximized()
window: window
} }
} }
setMaximizeState = (state: boolean) => {
this.setState({ maximized: state })
}
navShortcutsHandler = (e: KeyboardEvent) => { navShortcutsHandler = (e: KeyboardEvent) => {
if (!this.props.state.settings.display) { if (!this.props.state.settings.display) {
switch (e.key) { switch (e.key) {
@ -77,18 +72,14 @@ class Nav extends React.Component<NavProps, NavState> {
} }
minimize = () => { minimize = () => {
this.state.window.minimize() window.utils.minimizeWindow()
} }
maximize = () => { maximize = () => {
if (this.state.maximized) { window.utils.maximizeWindow()
this.state.window.unmaximize()
} else {
this.state.window.maximize()
}
this.setState({ maximized: !this.state.maximized }) this.setState({ maximized: !this.state.maximized })
} }
close = () => { close = () => {
this.state.window.close() window.utils.closeWindow()
} }
canFetch = () => this.props.state.sourceInit && this.props.state.feedInit && !this.props.state.fetchingItems canFetch = () => this.props.state.sourceInit && this.props.state.feedInit && !this.props.state.fetchingItems

View File

@ -1,8 +1,6 @@
import * as React from "react" import * as React from "react"
import intl from "react-intl-universal" import intl from "react-intl-universal"
import { Stack, Link } from "@fluentui/react" import { Stack, Link } from "@fluentui/react"
import { openExternal } from "../../scripts/utils"
import { remote } from "electron"
class AboutTab extends React.Component { class AboutTab extends React.Component {
render = () => ( render = () => (
@ -10,12 +8,12 @@ class AboutTab extends React.Component {
<Stack className="settings-about" horizontalAlign="center"> <Stack className="settings-about" horizontalAlign="center">
<img src="icons/logo.svg" style={{width: 120, height: 120}} /> <img src="icons/logo.svg" style={{width: 120, height: 120}} />
<h3 style={{fontWeight: 600}}>Fluent Reader</h3> <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> <p className="settings-hint">Copyright © 2020 Haoyuan Liu. All rights reserved.</p>
<Stack horizontal horizontalAlign="center" tokens={{childrenGap: 12}}> <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={() => window.utils.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={() => window.utils.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/issues")}>{intl.get("settings.feedback")}</Link></small>
</Stack> </Stack>
</Stack> </Stack>
</div> </div>

View File

@ -4,13 +4,12 @@ import { urlTest, byteToMB, calculateItemSize } from "../../scripts/utils"
import { ThemeSettings } from "../../schema-types" import { ThemeSettings } from "../../schema-types"
import { getThemeSettings, setThemeSettings, exportAll } from "../../scripts/settings" import { getThemeSettings, setThemeSettings, exportAll } from "../../scripts/settings"
import { Stack, Label, Toggle, TextField, DefaultButton, ChoiceGroup, IChoiceGroupOption, loadTheme, Dropdown, IDropdownOption, PrimaryButton } from "@fluentui/react" 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" import DangerButton from "../utils/danger-button"
type AppTabProps = { type AppTabProps = {
setLanguage: (option: string) => void setLanguage: (option: string) => void
deleteArticles: (days: number) => Promise<void> deleteArticles: (days: number) => Promise<void>
importAll: () => void importAll: () => Promise<void>
} }
type AppTabState = { type AppTabState = {
@ -38,7 +37,7 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
} }
getCacheSize = () => { getCacheSize = () => {
remote.session.defaultSession.getCacheSize().then(size => { window.utils.getCacheSize().then(size => {
this.setState({ cacheSize: byteToMB(size) }) this.setState({ cacheSize: byteToMB(size) })
}) })
} }
@ -49,7 +48,7 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
} }
clearCache = () => { clearCache = () => {
remote.session.defaultSession.clearCache().then(() => { window.utils.clearCache().then(() => {
this.getCacheSize() this.getCacheSize()
}) })
} }
@ -110,18 +109,6 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
this.setState({ themeSettings: option.key as ThemeSettings }) 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 = () => ( render = () => (
<div className="tab-body"> <div className="tab-body">
<Label>{intl.get("app.language")}</Label> <Label>{intl.get("app.language")}</Label>
@ -206,7 +193,7 @@ class AppTab extends React.Component<AppTabProps, AppTabState> {
<Label>{intl.get("app.data")}</Label> <Label>{intl.get("app.data")}</Label>
<Stack horizontal> <Stack horizontal>
<Stack.Item> <Stack.Item>
<PrimaryButton onClick={this.exportAll} text={intl.get("app.backup")} /> <PrimaryButton onClick={exportAll} text={intl.get("app.backup")} />
</Stack.Item> </Stack.Item>
<Stack.Item> <Stack.Item>
<DefaultButton onClick={this.props.importAll} text={intl.get("app.restore")} /> <DefaultButton onClick={this.props.importAll} text={intl.get("app.restore")} />

View File

@ -7,7 +7,7 @@ import { SourceRule, RuleActions } from "../../scripts/models/rule"
import { FilterType } from "../../scripts/models/feed" import { FilterType } from "../../scripts/models/feed"
import { validateRegex } from "../../scripts/utils" import { validateRegex } from "../../scripts/utils"
import { RSSItem } from "../../scripts/models/item" import { RSSItem } from "../../scripts/models/item"
import Parser = require("@yang991178/rss-parser") import Parser from "@yang991178/rss-parser"
const actionKeyMap = { const actionKeyMap = {
"r-true": "article.markRead", "r-true": "article.markRead",

View File

@ -1,4 +1,3 @@
import { remote } from "electron"
import intl from "react-intl-universal" import intl from "react-intl-universal"
import { connect } from "react-redux" import { connect } from "react-redux"
import { createSelector } from "reselect" import { createSelector } from "reselect"
@ -28,14 +27,12 @@ const mapDispatchToProps = (dispatch) => ({
settings: () => dispatch(toggleSettings()), settings: () => dispatch(toggleSettings()),
search: () => dispatch(toggleSearch()), search: () => dispatch(toggleSearch()),
markAllRead: () => { markAllRead: () => {
remote.dialog.showMessageBox(remote.getCurrentWindow(), { window.utils.showMessageBox(
title: intl.get("nav.markAllRead"), intl.get("nav.markAllRead"),
message: intl.get("confirmMarkAll"), intl.get("confirmMarkAll"),
buttons: process.platform === "win32" ? ["Yes", "No"] : [intl.get("confirm"), intl.get("cancel")], intl.get("confirm"), intl.get("cancel")
defaultId: 0, ).then(response => {
cancelId: 1 if (response) {
}).then(response => {
if (response.response === 0) {
dispatch(markAllRead()) dispatch(markAllRead())
} }
}) })

View File

@ -1,11 +1,9 @@
import intl from "react-intl-universal"
import { connect } from "react-redux" import { connect } from "react-redux"
import { importAll } from "../../scripts/settings"
import { initIntl, saveSettings } from "../../scripts/models/app" import { initIntl, saveSettings } from "../../scripts/models/app"
import * as db from "../../scripts/db" import * as db from "../../scripts/db"
import AppTab from "../../components/settings/app" import AppTab from "../../components/settings/app"
import { initFeeds } from "../../scripts/models/feed" import { initFeeds } from "../../scripts/models/feed"
import { remote } from "electron" import { importAll } from "../../scripts/settings"
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
setLanguage: (option: string) => { setLanguage: (option: string) => {
@ -24,28 +22,10 @@ const mapDispatchToProps = dispatch => ({
db.idb.persistence.compactDatafile() db.idb.persistence.compactDatafile()
}) })
}), }),
importAll: () => { importAll: async () => {
let window = remote.getCurrentWindow() dispatch(saveSettings())
remote.dialog.showOpenDialog(window, { let cancelled = await importAll()
filters: [{ name: intl.get("app.frData"), extensions: ["frdata"] }], if (cancelled) dispatch(saveSettings())
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])
}
})
}
})
} }
}) })

View File

@ -1,5 +1,3 @@
import intl from "react-intl-universal"
import { remote } from "electron"
import { connect } from "react-redux" import { connect } from "react-redux"
import { createSelector } from "reselect" import { createSelector } from "reselect"
import { RootState } from "../../scripts/reducer" import { RootState } from "../../scripts/reducer"
@ -31,28 +29,8 @@ const mapDispatchToProps = (dispatch: AppDispatch) => {
}, },
deleteSource: (source: RSSSource) => dispatch(deleteSource(source)), deleteSource: (source: RSSSource) => dispatch(deleteSource(source)),
deleteSources: (sources: RSSSource[]) => dispatch(deleteSources(sources)), deleteSources: (sources: RSSSource[]) => dispatch(deleteSources(sources)),
importOPML: () => { importOPML: () => dispatch(importOPML()),
remote.dialog.showOpenDialog( exportOPML: () => dispatch(exportOPML())
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))
})
}
} }
} }

View File

@ -1,9 +1,8 @@
import { app, ipcMain, BrowserWindow, Menu, nativeTheme } from "electron" import { app, ipcMain, Menu, nativeTheme } from "electron"
import windowStateKeeper = require("electron-window-state") import { ThemeSettings, SchemaTypes } from "./schema-types"
import { ThemeSettings } from "./schema-types" import { store } from "./main/settings"
import { store, setThemeListener } from "./main/settings"
import performUpdate from "./main/update-scripts" import performUpdate from "./main/update-scripts"
import path = require("path") import { WindowManager } from "./main/window"
if (!process.mas) { if (!process.mas) {
const locked = app.requestSingleInstanceLock() const locked = app.requestSingleInstanceLock()
@ -12,55 +11,15 @@ if (!process.mas) {
} }
} }
let mainWindow: BrowserWindow let restarting = false
let restarting: boolean
function init(setTheme = true) { function init() {
restarting = false
performUpdate(store) performUpdate(store)
if (setTheme) nativeTheme.themeSource = store.get("theme", ThemeSettings.Default) nativeTheme.themeSource = store.get("theme", ThemeSettings.Default)
} }
init() 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") { if (process.platform === "darwin") {
const template = [ const template = [
{ {
@ -86,34 +45,29 @@ if (process.platform === "darwin") {
Menu.setApplicationMenu(null) Menu.setApplicationMenu(null)
} }
app.on("ready", createWindow) const winManager = new WindowManager()
app.on("second-instance", () => { app.on("window-all-closed", () => {
if (mainWindow !== null) { if (winManager.hasWindow()) {
mainWindow.focus() winManager.mainWindow.webContents.session.clearStorageData({ storages: ["cookies"] })
} }
}) winManager.mainWindow = null
app.on("window-all-closed", function () {
if (mainWindow) {
mainWindow.webContents.session.clearStorageData({ storages: ["cookies"] })
}
mainWindow = null
if (restarting) { if (restarting) {
init(false) restarting = false
createWindow() winManager.createWindow()
} else if (process.platform !== "darwin") { } else if (process.platform !== "darwin") {
app.quit() app.quit()
} }
}) })
app.on("activate", function () { ipcMain.handle("import-all-settings", (_, configs: SchemaTypes) => {
if (mainWindow === null) {
createWindow()
}
})
ipcMain.on("restart", () => {
restarting = true 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()
}) })

View File

@ -1,6 +1,7 @@
import Store = require("electron-store") import Store = require("electron-store")
import { SchemaTypes, SourceGroup, ViewType, ThemeSettings } from "../schema-types" import { SchemaTypes, SourceGroup, ViewType, ThemeSettings } from "../schema-types"
import { ipcMain, session, nativeTheme, BrowserWindow, app } from "electron" import { ipcMain, session, nativeTheme, BrowserWindow, app } from "electron"
import { WindowManager } from "./window"
export const store = new Store<SchemaTypes>() export const store = new Store<SchemaTypes>()
@ -76,12 +77,14 @@ ipcMain.handle("set-theme", (_, theme: ThemeSettings) => {
ipcMain.on("get-theme-dark-color", (event) => { ipcMain.on("get-theme-dark-color", (event) => {
event.returnValue = nativeTheme.shouldUseDarkColors event.returnValue = nativeTheme.shouldUseDarkColors
}) })
export function setThemeListener(window: BrowserWindow) { export function setThemeListener(manager: WindowManager) {
nativeTheme.removeAllListeners() nativeTheme.removeAllListeners()
nativeTheme.on("updated", () => { nativeTheme.on("updated", () => {
let contents = window.webContents if (manager.hasWindow()) {
if (!contents.isDestroyed()) { let contents = manager.mainWindow.webContents
contents.send("theme-updated", nativeTheme.shouldUseDarkColors) 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 let locale = setting === "default" ? app.getLocale() : setting
event.returnValue = locale 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
View 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
View 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()
}
}

View File

@ -1,4 +1,6 @@
import { contextBridge } from "electron" 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

View File

@ -1,4 +1,4 @@
import Datastore = require("nedb") import Datastore from "nedb"
import { RSSSource } from "./models/source" import { RSSSource } from "./models/source"
import { RSSItem } from "./models/item" import { RSSItem } from "./models/item"

View File

@ -1,11 +1,9 @@
import fs = require("fs")
import intl from "react-intl-universal" import intl from "react-intl-universal"
import { SourceActionTypes, ADD_SOURCE, DELETE_SOURCE, addSource, RSSSource } from "./source" import { SourceActionTypes, ADD_SOURCE, DELETE_SOURCE, addSource, RSSSource } from "./source"
import { SourceGroup } from "../../schema-types" import { SourceGroup } from "../../schema-types"
import { ActionStatus, AppThunk, domParser, AppDispatch } from "../utils" import { ActionStatus, AppThunk, domParser } from "../utils"
import { saveSettings } from "./app" import { saveSettings } from "./app"
import { fetchItemsIntermediate, fetchItemsRequest, fetchItemsSuccess } from "./item" import { fetchItemsIntermediate, fetchItemsRequest, fetchItemsSuccess } from "./item"
import { remote } from "electron"
export const CREATE_SOURCE_GROUP = "CREATE_SOURCE_GROUP" export const CREATE_SOURCE_GROUP = "CREATE_SOURCE_GROUP"
export const ADD_SOURCE_TO_GROUP = "ADD_SOURCE_TO_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) => { return async (dispatch) => {
fs.readFile(path, "utf-8", async (err, data) => { const filters = [{ name: intl.get("sources.opmlFile"), extensions: ["xml", "opml"] }]
if (err) { window.utils.showOpenDialog(filters).then(data => {
console.log(err) if (data) {
} else {
dispatch(saveSettings()) dispatch(saveSettings())
let doc = domParser.parseFromString(data, "text/xml").getElementsByTagName("body") let doc = domParser.parseFromString(data, "text/xml").getElementsByTagName("body")
if (doc.length == 0) { if (doc.length == 0) {
@ -182,7 +179,7 @@ export function importOPML(path: string): AppThunk {
let parseError = doc[0].getElementsByTagName("parsererror") let parseError = doc[0].getElementsByTagName("parsererror")
if (parseError.length > 0) { if (parseError.length > 0) {
dispatch(saveSettings()) 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 return
} }
let sources: [ReturnType<typeof addSource>, number, string][] = [] let sources: [ReturnType<typeof addSource>, number, string][] = []
@ -214,7 +211,7 @@ export function importOPML(path: string): AppThunk {
dispatch(fetchItemsSuccess([], {})) dispatch(fetchItemsSuccess([], {}))
dispatch(saveSettings()) dispatch(saveSettings())
if (errors.length > 0) { if (errors.length > 0) {
remote.dialog.showErrorBox( window.utils.showErrorBox(
intl.get("sources.errorImport", { count: errors.length }), intl.get("sources.errorImport", { count: errors.length }),
errors.map(e => { errors.map(e => {
return e[0] + "\n" + String(e[1]) return e[0] + "\n" + String(e[1])
@ -236,33 +233,35 @@ function sourceToOutline(source: RSSSource, xml: Document) {
return outline return outline
} }
export function exportOPML(path: string): AppThunk { export function exportOPML(): AppThunk {
return (_, getState) => { return (_, getState) => {
let state = getState() const filters = [{ name: intl.get("sources.opmlFile"), extensions: ["opml"] }]
let xml = domParser.parseFromString( window.utils.showSaveDialog(filters, "*/Fluent_Reader_Export.opml").then(write => {
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><opml version=\"1.0\"><head><title>Fluent Reader Export</title></head><body></body></opml>", if (write) {
"text/xml" let state = getState()
) let xml = domParser.parseFromString(
let body = xml.getElementsByTagName("body")[0] "<?xml version=\"1.0\" encoding=\"UTF-8\"?><opml version=\"1.0\"><head><title>Fluent Reader Export</title></head><body></body></opml>",
for (let group of state.groups) { "text/xml"
if (group.isMultiple) { )
let outline = xml.createElement("outline") let body = xml.getElementsByTagName("body")[0]
outline.setAttribute("text", group.name) for (let group of state.groups) {
outline.setAttribute("name", group.name) if (group.isMultiple) {
for (let sid of group.sids) { let outline = xml.createElement("outline")
outline.appendChild(sourceToOutline(state.sources[sid], xml)) 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) let serializer = new XMLSerializer()
} else { write(serializer.serializeToString(xml), intl.get("settings.writeError"))
body.appendChild(sourceToOutline(state.sources[group.sids[0]], xml))
} }
}
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[] export type GroupState = SourceGroup[]

View File

@ -1,9 +1,9 @@
import * as db from "../db" import * as db from "../db"
import intl from "react-intl-universal" 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 { RSSSource } from "./source"
import { FeedActionTypes, INIT_FEED, LOAD_MORE } from "./feed" import { FeedActionTypes, INIT_FEED, LOAD_MORE } from "./feed"
import Parser = require("@yang991178/rss-parser") import Parser from "@yang991178/rss-parser"
export class RSSItem { export class RSSItem {
_id: string _id: string
@ -279,7 +279,7 @@ export function itemShortcuts(item: RSSItem, key: string): AppThunk {
break break
case "b": case "B": case "b": case "B":
if (!item.hasRead) dispatch(markRead(item)) if (!item.hasRead) dispatch(markRead(item))
openExternal(item.link) window.utils.openExternal(item.link)
break break
case "s": case "S": case "s": case "S":
dispatch(toggleStarred(item)) dispatch(toggleStarred(item))

View File

@ -1,10 +1,9 @@
import Parser = require("@yang991178/rss-parser") import Parser from "@yang991178/rss-parser"
import intl from "react-intl-universal" import intl from "react-intl-universal"
import * as db from "../db" import * as db from "../db"
import { fetchFavicon, ActionStatus, AppThunk, parseRSS } from "../utils" import { fetchFavicon, ActionStatus, AppThunk, parseRSS } from "../utils"
import { RSSItem, insertItems, ItemActionTypes, FETCH_ITEMS, MARK_READ, MARK_UNREAD, MARK_ALL_READ } from "./item" import { RSSItem, insertItems, ItemActionTypes, FETCH_ITEMS, MARK_READ, MARK_UNREAD, MARK_ALL_READ } from "./item"
import { saveSettings } from "./app" import { saveSettings } from "./app"
import { remote } from "electron"
import { SourceRule } from "./rule" import { SourceRule } from "./rule"
export enum SourceOpenTarget { export enum SourceOpenTarget {
@ -256,7 +255,7 @@ export function addSource(url: string, name: string = null, batch = false): AppT
.catch(e => { .catch(e => {
dispatch(addSourceFailure(e, batch)) dispatch(addSourceFailure(e, batch))
if (!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) return Promise.reject(e)
}) })

View File

@ -1,9 +1,7 @@
import { remote, ipcRenderer } from "electron"
import { IPartialTheme, loadTheme } from "@fluentui/react" 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 { ThemeSettings, SchemaTypes } from "../schema-types" import { ThemeSettings, SchemaTypes } from "../schema-types"
import fs = require("fs")
import intl from "react-intl-universal" import intl from "react-intl-universal"
export const store = new Store<SchemaTypes>() export const store = new Store<SchemaTypes>()
@ -61,65 +59,58 @@ export function getCurrentLocale() {
return (locale in locales) ? locale : "en-US" return (locale in locales) ? locale : "en-US"
} }
export function exportAll(path: string) { export function exportAll() {
let output = {} const filters = [{ name: intl.get("app.frData"), extensions: ["frdata"] }]
for (let [key, value] of store) { window.utils.showSaveDialog(filters, "*/Fluent_Reader_Backup.frdata").then(write => {
output[key] = value if (write) {
} let output = window.settings.getAll()
output["nedb"] = {} 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)
let openRequest = window.indexedDB.open("NeDB") let openRequest = window.indexedDB.open("NeDB")
openRequest.onsuccess = () => { openRequest.onsuccess = () => {
let db = openRequest.result let db = openRequest.result
let objectStore = db.transaction("nedbdata", "readwrite").objectStore("nedbdata") let objectStore = db.transaction("nedbdata").objectStore("nedbdata")
let requests = Object.entries(configs.nedb).map(([key, value]) => { let cursorRequest = objectStore.openCursor()
return objectStore.put(value, key) cursorRequest.onsuccess = () => {
}) let cursor = cursorRequest.result
let promises = requests.map(req => new Promise((resolve, reject) => { if (cursor) {
req.onsuccess = () => resolve() output["nedb"][cursor.key] = cursor.value
req.onerror = () => reject() cursor.continue()
})) } else {
Promise.all(promises).then(() => { write(JSON.stringify(output), intl.get("settings.writeError"))
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)
}
} }
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
}

View File

@ -1,7 +1,8 @@
import { shell, remote } from "electron"
import { ThunkAction, ThunkDispatch } from "redux-thunk" import { ThunkAction, ThunkDispatch } from "redux-thunk"
import { AnyAction } from "redux" import { AnyAction } from "redux"
import { RootState } from "./reducer" import { RootState } from "./reducer"
import Parser from "@yang991178/rss-parser"
import Url from "url"
export enum ActionStatus { export enum ActionStatus {
Request, Success, Failure, Intermediate Request, Success, Failure, Intermediate
@ -16,7 +17,6 @@ export type AppThunk<ReturnType = void> = ThunkAction<
export type AppDispatch = ThunkDispatch<RootState, undefined, AnyAction> export type AppDispatch = ThunkDispatch<RootState, undefined, AnyAction>
import Parser = require("@yang991178/rss-parser")
const rssParser = new Parser({ const rssParser = new Parser({
customFields: { customFields: {
item: [ item: [
@ -41,7 +41,6 @@ export async function parseRSS(url: string) {
export const domParser = new DOMParser() export const domParser = new DOMParser()
import Url = require("url")
export async function fetchFavicon(url: string) { export async function fetchFavicon(url: string) {
try { try {
let result = await fetch(url, { credentials: "omit" }) let result = await fetch(url, { credentials: "omit" })
@ -77,21 +76,16 @@ export function htmlDecode(input: string) {
return doc.documentElement.textContent return doc.documentElement.textContent
} }
export function openExternal(url: string) {
if (url.startsWith("https://") || url.startsWith("http://"))
shell.openExternal(url)
}
export const urlTest = (s: string) => 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) /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) => { export const cutText = (s: string, length: number) => {
return (s.length <= length) ? s : s.slice(0, length) + "…" 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[] { export function mergeSortedArrays<T>(a: T[], b: T[], cmp: ((x: T, y: T) => number)): T[] {
let merged = new Array<T>() let merged = new Array<T>()
@ -114,6 +108,17 @@ export function byteToMB(B: number) {
return MB + "MB" 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> { export function calculateItemSize(): Promise<number> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let openRequest = window.indexedDB.open("NeDB") let openRequest = window.indexedDB.open("NeDB")
@ -122,8 +127,7 @@ export function calculateItemSize(): Promise<number> {
let objectStore = db.transaction("nedbdata").objectStore("nedbdata") let objectStore = db.transaction("nedbdata").objectStore("nedbdata")
let getRequest = objectStore.get("items") let getRequest = objectStore.get("items")
getRequest.onsuccess = () => { getRequest.onsuccess = () => {
let resultBuffer = Buffer.from(getRequest.result) resolve(byteLength(getRequest.result))
resolve(resultBuffer.length)
} }
getRequest.onerror = () => reject() getRequest.onerror = () => reject()
} }