From 2c0533a16fb5cc36d8414a875c8fa3455d8c9e12 Mon Sep 17 00:00:00 2001 From: srgooglo Date: Tue, 21 Nov 2023 19:12:56 +0100 Subject: [PATCH] added settings --- src/main/index.js | 86 ++++----- src/preload/index.js | 19 +- src/renderer/src/App.jsx | 101 +++++++---- src/renderer/src/components/Icons/index.jsx | 19 ++ .../src/layout/components/Header/index.jsx | 19 +- src/renderer/src/layout/index.jsx | 0 src/renderer/src/pages/settings/index.jsx | 165 +++++++++++++++++- src/renderer/src/pages/settings/index.less | 100 +++++++++++ src/renderer/src/router.jsx | 61 ++++++- src/renderer/src/style/index.less | 6 +- src/renderer/src/style/vars.less | 5 + 11 files changed, 486 insertions(+), 95 deletions(-) create mode 100644 src/renderer/src/components/Icons/index.jsx delete mode 100644 src/renderer/src/layout/index.jsx create mode 100644 src/renderer/src/pages/settings/index.less create mode 100644 src/renderer/src/style/vars.less diff --git a/src/main/index.js b/src/main/index.js index b784f05..c262cb7 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1,32 +1,16 @@ -import lodash from "lodash" +import sendToRender from "./utils/sendToRender" -global.sendToRenderer = (event, data) => { - function serializeIpc(data) { - const copy = lodash.cloneDeep(data) - - // remove fns - if (!Array.isArray(copy)) { - Object.keys(copy).forEach((key) => { - if (typeof copy[key] === "function") { - delete copy[key] - } - }) - } - - return copy - } - - global.win.webContents.send(event, serializeIpc(data)) -} - -const { autoUpdater } = require("electron-differential-updater") -const ProtocolRegistry = require("protocol-registry") +global.SettingsStore = new Store({ + name: "settings", + watch: true, +}) import path from "node:path" import { app, shell, BrowserWindow, ipcMain } from "electron" import { electronApp, optimizer, is } from "@electron-toolkit/utils" import isDev from "electron-is-dev" +import Store from "electron-store" import open from "open" @@ -37,6 +21,11 @@ import setup from "./setup" import PkgManager from "./pkg_mng" import { readManifest } from "./utils/readManifest" +import GoogleDriveAPI from "./lib/google_drive" + +const { autoUpdater } = require("electron-differential-updater") +const ProtocolRegistry = require("protocol-registry") + const protocolRegistryNamespace = "rsbundle" class ElectronApp { @@ -46,9 +35,6 @@ class ElectronApp { } handlers = { - pkg: () => { - return pkg - }, "get:installations": async () => { return await this.pkgManager.getInstallations() }, @@ -73,9 +59,6 @@ class ElectronApp { "pkg:apply_changes": (event, manifest_id, changes) => { this.pkgManager.applyChanges(manifest_id, changes) }, - "check:setup": async () => { - return await setup() - }, "updater:check": () => { autoUpdater.checkForUpdates() }, @@ -83,7 +66,32 @@ class ElectronApp { setTimeout(() => { autoUpdater.quitAndInstall() }, 3000) - } + }, + "settings:get": (e, key) => { + return global.SettingsStore.get(key) + }, + "settings:set": (e, key, value) => { + return global.SettingsStore.set(key, value) + }, + "settings:delete": (e, key) => { + return global.SettingsStore.delete(key) + }, + "settings:has": (e, key) => { + return global.SettingsStore.has(key) + }, + "app:init": async (event, data) => { + await setup() + + // check if can decode google drive token + const googleDrive_enabled = !!await GoogleDriveAPI.readCredentials() + + return { + pkg: pkg, + authorizedServices: { + drive: googleDrive_enabled, + }, + } + }, } events = { @@ -92,11 +100,6 @@ class ElectronApp { }, } - sendToRender(event, ...args) { - console.log(`[sendToRender][${event}]`, ...args) - this.win.webContents.send(event, ...args) - } - createWindow() { this.win = global.win = new BrowserWindow({ width: 450, @@ -129,8 +132,6 @@ class ElectronApp { } handleURLProtocol(url) { - console.log(url) - const urlStarter = `${protocolRegistryNamespace}://` if (url.startsWith(urlStarter)) { @@ -143,18 +144,17 @@ class ElectronApp { switch (action) { case "install": { - return this.sendToRender("installation:invoked", value) + return sendToRender("installation:invoked", value) } - default: { - return this.sendToRender("new:message", { + return sendToRender("new:message", { message: "Unrecognized URL action", }) } } } else { // by default if no action is specified, assume is a install action - return this.sendToRender("installation:invoked", urlValue) + return sendToRender("installation:invoked", urlValue) } } } @@ -217,7 +217,7 @@ class ElectronApp { autoUpdater.on("update-downloaded", (ev, info) => { console.log(info) - this.sendToRender("update-available", info) + sendToRender("update-available", info) }) if (isDev) { @@ -240,7 +240,9 @@ class ElectronApp { } } - this.createWindow() + await GoogleDriveAPI.init() + + await this.createWindow() app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { diff --git a/src/preload/index.js b/src/preload/index.js index 7a5b60a..3926649 100644 --- a/src/preload/index.js +++ b/src/preload/index.js @@ -1,7 +1,22 @@ import { contextBridge, ipcRenderer } from "electron" import { electronAPI } from "@electron-toolkit/preload" -const api = {} +const api = { + settings: { + get: (key) => { + return ipcRenderer.invoke("settings:get", key) + }, + set: (key, value) => { + return ipcRenderer.invoke("settings:set", key, value) + }, + delete: (key) => { + return ipcRenderer.invoke("settings:delete", key) + }, + has: (key) => { + return ipcRenderer.invoke("settings:has", key) + }, + }, +} if (process.contextIsolated) { try { @@ -21,8 +36,8 @@ if (process.contextIsolated) { ipcRenderer.removeListener(channel, listener) } }, - ) + contextBridge.exposeInMainWorld("electron", electronAPI) contextBridge.exposeInMainWorld("api", api) diff --git a/src/renderer/src/App.jsx b/src/renderer/src/App.jsx index b100168..060eca3 100644 --- a/src/renderer/src/App.jsx +++ b/src/renderer/src/App.jsx @@ -10,7 +10,7 @@ import ManifestInfo from "components/ManifestInfo" import AppHeader from "layout/components/Header" import AppModalDialog from "layout/components/ModalDialog" -import { PageRender } from "./router.jsx" +import { InternalRouter, PageRender } from "./router.jsx" globalThis.getRootCssVar = getRootCssVar globalThis.notification = antd.notification @@ -32,7 +32,10 @@ window.app = { app.modal.close() } }) - } + }, + checkUpdates: () => { + ipc.exec("updater:check") + }, } class App extends React.Component { @@ -41,6 +44,10 @@ class App extends React.Component { pkg: null, initializing: false, updateAvailable: false, + + authorizedServices: { + drive: false, + }, } ipcEvents = { @@ -50,14 +57,6 @@ class App extends React.Component { "runtime:info": (event, data) => { antd.message.info(data) }, - "initializing_text": (event, data) => { - this.setState({ - initializing_text: data, - }) - }, - "installation:invoked": (event, manifest) => { - app.invokeInstall(manifest) - }, "new:notification": (event, data) => { antd.notification[data.type || "info"]({ message: data.message, @@ -71,26 +70,47 @@ class App extends React.Component { antd.message[data.type || "info"](data.message) }, "update-available": (event, data) => { + this.onUpdateAvailable(data) + }, + "initializing_text": (event, data) => { this.setState({ - updateAvailable: true, + initializing_text: data, + }) + }, + "installation:invoked": (event, manifest) => { + app.invokeInstall(manifest) + }, + "drive:authorized": (event, data) => { + this.setState({ + authorizedServices: { + drive: true, + }, }) - console.log(data) + message.success("Google Drive API authorized") + }, + } - antd.Modal.confirm({ - title: "Update Available", - content: <> -

- A new version of the application is available. -

- , - okText: "Update", - cancelText: "Later", - onOk: () => { - app.applyUpdate() - } - }) - } + onUpdateAvailable = () => { + this.setState({ + updateAvailable: true, + }) + + console.log(data) + + antd.Modal.confirm({ + title: "Update Available", + content: <> +

+ A new version of the application is available. +

+ , + okText: "Update", + cancelText: "Later", + onOk: () => { + app.applyUpdate() + } + }) } componentDidMount = async () => { @@ -98,13 +118,16 @@ class App extends React.Component { ipc.on(event, this.ipcEvents[event]) } - const pkg = await ipc.exec("pkg") + const initResult = await ipc.exec("app:init") - await ipc.exec("check:setup") + console.log(`[INIT] >`, initResult) this.setState({ - pkg: pkg, loading: false, + pkg: initResult.pkg, + authorizedServices: { + drive: initResult.authorizedServices?.drive ?? false + }, }) } @@ -125,17 +148,19 @@ class App extends React.Component { algorithm: antd.theme.darkAlgorithm }} > - - + + + - - + + - - - - - + + + + + + } } diff --git a/src/renderer/src/components/Icons/index.jsx b/src/renderer/src/components/Icons/index.jsx new file mode 100644 index 0000000..02faa09 --- /dev/null +++ b/src/renderer/src/components/Icons/index.jsx @@ -0,0 +1,19 @@ +import React from "react" + +import * as MDIcons from "react-icons/md" +import * as SIIcons from "react-icons/si" + +export const Icons = { + ...MDIcons, + ...SIIcons, +} + +export const Icon = ({ icon }) => { + if (icon && Icons[icon]) { + return React.createElement(Icons[icon]) + } + + return <> +} + +export default Icons \ No newline at end of file diff --git a/src/renderer/src/layout/components/Header/index.jsx b/src/renderer/src/layout/components/Header/index.jsx index de6bd74..c2b1ed7 100644 --- a/src/renderer/src/layout/components/Header/index.jsx +++ b/src/renderer/src/layout/components/Header/index.jsx @@ -1,6 +1,6 @@ import React from "react" import * as antd from "antd" -import { MdFolder, MdSettings, MdDownload } from "react-icons/md" +import { Icons } from "components/Icons" import GlobalStateContext from "contexts/global" @@ -10,16 +10,24 @@ const Header = (props) => { const ctx = React.useContext(GlobalStateContext) return -
+
app.location.push("/")}>
{ !ctx.loading &&
+ { + ctx.authorizedServices?.drive && + } + { ctx.updateAvailable && } + icon={} onClick={app.applyUpdate} > Update now @@ -28,12 +36,13 @@ const Header = (props) => { } + icon={} + onClick={() => app.location.push("/settings")} /> } + icon={} onClick={() => ipc.send("open-runtime-path")} /> diff --git a/src/renderer/src/layout/index.jsx b/src/renderer/src/layout/index.jsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/renderer/src/pages/settings/index.jsx b/src/renderer/src/pages/settings/index.jsx index c559f3e..78023d3 100644 --- a/src/renderer/src/pages/settings/index.jsx +++ b/src/renderer/src/pages/settings/index.jsx @@ -1,15 +1,178 @@ import React from "react" +import * as antd from "antd" +import { Icons, Icon } from "components/Icons" + +import "./index.less" const settingsList = [ { id: "drive_auth", name: "Google Drive Authorize", description: "Authorize your Google Drive account to be used for bundles installation.", + icon: "SiGoogledrive", + type: "button", + value: async () => { + return api.settings.get("drive_auth") + }, + render: (props) => { + return { + if (!props.value) { + message.info("Authorizing...") + return ipc.exec("drive:authorize") + } + + return api.settings.delete("drive_auth") + }} + > + { + props.value ? "Deauthorize" : "Authorize" + } + + } + }, + { + id: "check_update", + name: "Check for updates", + description: "Check for updates to the app.", + icon: "MdUpdate", + type: "button", + props: { + children: "Check", + onClick: () => { + message.info("Checking for updates...") + app.checkUpdates() + } + } } ] -const Settings = () => { +const SettingTypeToComponent = { + switch: antd.Switch, + button: antd.Button, +} +const SettingItem = (props) => { + const { + id, + name, + description, + type, + icon, + props: _props, + render, + } = props.setting + + const [loading, setLoading] = React.useState(false) + const [value, setValue] = React.useState(null) + + React.useEffect(() => { + if (typeof props.setting.value === "function") { + setLoading(true) + + props.setting.value().then((value) => { + setValue(value) + setLoading(false) + }) + } else { + setLoading(false) + } + }, [props.setting.value]) + + let componentProps = { + value: value, + ..._props, + } + + async function handleChange(value) { + console.log(`Setting [${id}] set to >`, value) + setValue(value) + api.settings.set(id, value) + } + + switch (type) { + case "switch": { + componentProps.defaultChecked = defaultProps.defaultChecked ?? false + componentProps.onChange = (e) => { + handleChange(e) + } + break + } + } + + const Component = SettingTypeToComponent[type.toLowerCase()] + const Render = () => { + if (typeof render === "function") { + return render(componentProps) + } + + return React.createElement(Component, componentProps) + } + + return
+
+
+ + +

+ {name} +

+
+ +
+

+ {description} +

+
+
+ +
+ { + loading && + } + { + !loading && + } +
+
+} + +const Settings = () => { + return
+
+
+ { + app.location.push("/") + }} + /> + Back +
+ +
+ +

Settings

+
+
+ +
+ { + settingsList.map((setting, index) => { + return + }) + } +
+ +
+ +
+
} export default Settings \ No newline at end of file diff --git a/src/renderer/src/pages/settings/index.less b/src/renderer/src/pages/settings/index.less new file mode 100644 index 0000000..2d1f8a1 --- /dev/null +++ b/src/renderer/src/pages/settings/index.less @@ -0,0 +1,100 @@ +@import "style/vars.less"; + +.app_settings { + display: flex; + flex-direction: column; + + gap: 10px; + + .app_settings-header { + display: flex; + flex-direction: column; + + gap: 20px; + + .app_settings-header-back { + display: flex; + flex-direction: row; + + align-items: center; + + gap: 10px; + + svg { + color: var(--primary-color); + border: 1px solid var(--primary-color); + + border-radius: 100%; + + cursor: pointer; + + font-size: 1.5rem; + } + } + + .app_settings-header-title { + display: flex; + flex-direction: row; + + align-items: center; + + gap: 10px; + + font-size: 1.5rem; + } + } + + .app_settings-list { + display: flex; + flex-direction: column; + + background-color: var(--background-color-secondary); + + border-radius: 12px; + + .app_settings-list-item { + display: flex; + flex-direction: row; + + align-items: center; + + &:nth-child(odd) { + background-color: mix(#fff, @var-background-color-secondary, 5%); + } + + border-radius: 12px; + + padding: 10px; + + .app_settings-list-item-info { + display: flex; + flex-direction: column; + + gap: 10px; + + width: 100%; + + .app_settings-list-item-label { + display: flex; + flex-direction: row; + + gap: 6px; + } + + .app_settings-list-item-description { + display: inline-flex; + flex-direction: row; + + font-size: 0.7rem; + } + } + + .app_settings-list-item-component { + display: flex; + flex-direction: column; + + justify-content: flex-end; + } + } + } +} \ No newline at end of file diff --git a/src/renderer/src/router.jsx b/src/renderer/src/router.jsx index 00e82a9..0f9b141 100644 --- a/src/renderer/src/router.jsx +++ b/src/renderer/src/router.jsx @@ -1,9 +1,63 @@ import React from "react" import BarLoader from "react-spinners/BarLoader" +import { BrowserRouter, Route, Routes, useNavigate } from "react-router-dom" import GlobalStateContext from "contexts/global" -import InstallationsManager from "pages/manager" +import PackagesMangerPage from "pages/manager" +import SettingsPage from "pages/settings" + +const NavigationController = (props) => { + if (!app.location) { + app.location = Object() + } + + const navigate = useNavigate() + + async function setLocation(to, state = {}) { + // clean double slashes + to = to.replace(/\/{2,}/g, "/") + + // if state is a number, it's a delay + if (typeof state !== "object") { + state = {} + } + + app.location.last = window.location + + return navigate(to, { + state + }) + } + + async function backLocation() { + app.location.last = window.location + + if (transitionDuration >= 100) { + await new Promise((resolve) => setTimeout(resolve, transitionDuration)) + } + + return window.history.back() + } + + React.useEffect(() => { + app.location = { + last: window.location, + push: setLocation, + back: backLocation, + } + }, []) + + return props.children +} + +export const InternalRouter = (props) => { + return + + {props.children} + + +} export const PageRender = () => { const globalState = React.useContext(GlobalStateContext) @@ -23,5 +77,8 @@ export const PageRender = () => {
} - return + return + } /> + } /> + } \ No newline at end of file diff --git a/src/renderer/src/style/index.less b/src/renderer/src/style/index.less index 7bac6b2..5f5b0cc 100644 --- a/src/renderer/src/style/index.less +++ b/src/renderer/src/style/index.less @@ -1,11 +1,7 @@ @import "style/reset.css"; @import "style/fix.less"; -@var-text-color: #fff; -@var-background-color-primary: #424549; -@var-background-color-secondary: #1e2124; -@var-primary-color: #36d7b7; //#F3B61F; -@var-border-color: #a1a2a2; +@import "style/vars.less"; :root { --background-color-primary: @var-background-color-primary; diff --git a/src/renderer/src/style/vars.less b/src/renderer/src/style/vars.less new file mode 100644 index 0000000..7392455 --- /dev/null +++ b/src/renderer/src/style/vars.less @@ -0,0 +1,5 @@ +@var-text-color: #fff; +@var-background-color-primary: #424549; +@var-background-color-secondary: #1e2124; +@var-primary-color: #36d7b7; //#F3B61F; +@var-border-color: #a1a2a2; \ No newline at end of file