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}
+
+
+
+
+
+
+
+ {
+ 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