mirror of
https://github.com/ragestudio/relic.git
synced 2025-06-09 10:34:18 +00:00
merge from local
This commit is contained in:
parent
86a6effeb1
commit
e187a49947
@ -33,6 +33,7 @@ export default async (pkg, step) => {
|
|||||||
//`--depth ${step.depth ?? 1}`,
|
//`--depth ${step.depth ?? 1}`,
|
||||||
//"--filter=blob:none",
|
//"--filter=blob:none",
|
||||||
//"--filter=tree:0",
|
//"--filter=tree:0",
|
||||||
|
"--progress",
|
||||||
"--recurse-submodules",
|
"--recurse-submodules",
|
||||||
"--remote-submodules",
|
"--remote-submodules",
|
||||||
step.url,
|
step.url,
|
||||||
|
@ -83,6 +83,7 @@ export default async function apply(pkg_id, changes = {}) {
|
|||||||
return pkg
|
return pkg
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
global._relic_eventBus.emit(`pkg:error`, {
|
global._relic_eventBus.emit(`pkg:error`, {
|
||||||
|
event: "apply",
|
||||||
id: pkg_id,
|
id: pkg_id,
|
||||||
error
|
error
|
||||||
})
|
})
|
||||||
|
@ -19,6 +19,20 @@ export default async function execute(pkg_id, { useRemote = false, force = false
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pkg.last_status !== "installed") {
|
||||||
|
if (!force) {
|
||||||
|
BaseLog.info(`Package not installed [${pkg_id}], aborting execution`)
|
||||||
|
|
||||||
|
global._relic_eventBus.emit(`pkg:error`, {
|
||||||
|
id: pkg_id,
|
||||||
|
event: "execute",
|
||||||
|
error: new Error("Package not valid or not installed"),
|
||||||
|
})
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const manifestPath = useRemote ? pkg.remote_manifest : pkg.local_manifest
|
const manifestPath = useRemote ? pkg.remote_manifest : pkg.local_manifest
|
||||||
|
|
||||||
if (!fs.existsSync(manifestPath)) {
|
if (!fs.existsSync(manifestPath)) {
|
||||||
|
@ -164,12 +164,13 @@ export default async function install(manifest) {
|
|||||||
return pkg
|
return pkg
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
global._relic_eventBus.emit(`pkg:error`, {
|
global._relic_eventBus.emit(`pkg:error`, {
|
||||||
id: id,
|
id: id ?? manifest.constructor.id,
|
||||||
error
|
event: "install",
|
||||||
|
error,
|
||||||
})
|
})
|
||||||
|
|
||||||
global._relic_eventBus.emit(`pkg:update:state`, {
|
global._relic_eventBus.emit(`pkg:update:state`, {
|
||||||
id: id,
|
id: id ?? manifest.constructor.id,
|
||||||
last_status: "failed",
|
last_status: "failed",
|
||||||
status_text: `Installation failed`,
|
status_text: `Installation failed`,
|
||||||
})
|
})
|
||||||
|
76
packages/core/src/handlers/lastOperationRetry.js
Normal file
76
packages/core/src/handlers/lastOperationRetry.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import fs from "node:fs"
|
||||||
|
import path from "node:path"
|
||||||
|
|
||||||
|
import Logger from "../logger"
|
||||||
|
import DB from "../db"
|
||||||
|
|
||||||
|
import PackageInstall from "./install"
|
||||||
|
import PackageUpdate from "./update"
|
||||||
|
import PackageUninstall from "./uninstall"
|
||||||
|
|
||||||
|
import Vars from "../vars"
|
||||||
|
|
||||||
|
export default async function lastOperationRetry(pkg_id) {
|
||||||
|
try {
|
||||||
|
const Log = Logger.child({ service: `OPERATION_RETRY|${pkg_id}` })
|
||||||
|
const pkg = await DB.getPackages(pkg_id)
|
||||||
|
|
||||||
|
if (!pkg) {
|
||||||
|
Log.error(`This package doesn't exist`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.info(`Try performing last operation retry...`)
|
||||||
|
|
||||||
|
global._relic_eventBus.emit(`pkg:update:state`, {
|
||||||
|
id: pkg.id,
|
||||||
|
status_text: `Performing last operation retry...`,
|
||||||
|
})
|
||||||
|
|
||||||
|
switch (pkg.last_status) {
|
||||||
|
case "installing":
|
||||||
|
await PackageInstall(pkg.local_manifest)
|
||||||
|
break
|
||||||
|
case "updating":
|
||||||
|
await PackageUpdate(pkg_id)
|
||||||
|
break
|
||||||
|
case "uninstalling":
|
||||||
|
await PackageUninstall(pkg_id)
|
||||||
|
break
|
||||||
|
case "failed": {
|
||||||
|
// copy pkg.local_manifest to cache after uninstall
|
||||||
|
const cachedManifest = path.join(Vars.cache_path, path.basename(pkg.local_manifest))
|
||||||
|
|
||||||
|
await fs.promises.copyFile(pkg.local_manifest, cachedManifest)
|
||||||
|
|
||||||
|
await PackageUninstall(pkg_id)
|
||||||
|
await PackageInstall(cachedManifest)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
Log.error(`Invalid last status: ${pkg.last_status}`)
|
||||||
|
|
||||||
|
global._relic_eventBus.emit(`pkg:error`, {
|
||||||
|
id: pkg.id,
|
||||||
|
event: "retrying last operation",
|
||||||
|
status_text: `Performing last operation retry...`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkg
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`Failed to perform last operation retry of [${pkg_id}]`)
|
||||||
|
Logger.error(error)
|
||||||
|
|
||||||
|
global._relic_eventBus.emit(`pkg:error`, {
|
||||||
|
event: "retrying last operation",
|
||||||
|
id: pkg_id,
|
||||||
|
error: error,
|
||||||
|
})
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
@ -20,21 +20,33 @@ export default async function uninstall(pkg_id) {
|
|||||||
const Log = Logger.child({ service: `UNINSTALLER|${pkg.id}` })
|
const Log = Logger.child({ service: `UNINSTALLER|${pkg.id}` })
|
||||||
|
|
||||||
Log.info(`Uninstalling package...`)
|
Log.info(`Uninstalling package...`)
|
||||||
|
|
||||||
global._relic_eventBus.emit(`pkg:update:state`, {
|
global._relic_eventBus.emit(`pkg:update:state`, {
|
||||||
id: pkg.id,
|
id: pkg.id,
|
||||||
status_text: `Uninstalling package...`,
|
status_text: `Uninstalling package...`,
|
||||||
})
|
})
|
||||||
|
|
||||||
const ManifestRead = await ManifestReader(pkg.local_manifest)
|
try {
|
||||||
const manifest = await ManifestVM(ManifestRead.code)
|
const ManifestRead = await ManifestReader(pkg.local_manifest)
|
||||||
|
const manifest = await ManifestVM(ManifestRead.code)
|
||||||
|
|
||||||
if (typeof manifest.uninstall === "function") {
|
if (typeof manifest.uninstall === "function") {
|
||||||
Log.info(`Performing uninstall hook...`)
|
Log.info(`Performing uninstall hook...`)
|
||||||
global._relic_eventBus.emit(`pkg:update:state`, {
|
|
||||||
|
global._relic_eventBus.emit(`pkg:update:state`, {
|
||||||
|
id: pkg.id,
|
||||||
|
status_text: `Performing uninstall hook...`,
|
||||||
|
})
|
||||||
|
|
||||||
|
await manifest.uninstall(pkg)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Log.error(`Failed to perform uninstall hook`, error)
|
||||||
|
global._relic_eventBus.emit(`pkg:error`, {
|
||||||
|
event: "uninstall",
|
||||||
id: pkg.id,
|
id: pkg.id,
|
||||||
status_text: `Performing uninstall hook...`,
|
error
|
||||||
})
|
})
|
||||||
await manifest.uninstall(pkg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.info(`Deleting package directory...`)
|
Log.info(`Deleting package directory...`)
|
||||||
@ -62,6 +74,7 @@ export default async function uninstall(pkg_id) {
|
|||||||
return pkg
|
return pkg
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
global._relic_eventBus.emit(`pkg:error`, {
|
global._relic_eventBus.emit(`pkg:error`, {
|
||||||
|
event: "uninstall",
|
||||||
id: pkg_id,
|
id: pkg_id,
|
||||||
error
|
error
|
||||||
})
|
})
|
||||||
|
@ -116,10 +116,21 @@ export default async function update(pkg_id) {
|
|||||||
return pkg
|
return pkg
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
global._relic_eventBus.emit(`pkg:error`, {
|
global._relic_eventBus.emit(`pkg:error`, {
|
||||||
|
event: "update",
|
||||||
id: pkg_id,
|
id: pkg_id,
|
||||||
error
|
error,
|
||||||
|
last_status: "failed"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await DB.updatePackageById(pkg_id, {
|
||||||
|
last_status: "failed",
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
BaseLog.error(`Failed to update status of pkg [${pkg_id}]`)
|
||||||
|
BaseLog.error(error.stack)
|
||||||
|
}
|
||||||
|
|
||||||
BaseLog.error(`Failed to update package [${pkg_id}]`, error)
|
BaseLog.error(`Failed to update package [${pkg_id}]`, error)
|
||||||
BaseLog.error(error.stack)
|
BaseLog.error(error.stack)
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import PackageList from "./handlers/list"
|
|||||||
import PackageRead from "./handlers/read"
|
import PackageRead from "./handlers/read"
|
||||||
import PackageAuthorize from "./handlers/authorize"
|
import PackageAuthorize from "./handlers/authorize"
|
||||||
import PackageCheckUpdate from "./handlers/checkUpdate"
|
import PackageCheckUpdate from "./handlers/checkUpdate"
|
||||||
|
import PackageLastOperationRetry from "./handlers/lastOperationRetry"
|
||||||
|
|
||||||
export default class RelicCore {
|
export default class RelicCore {
|
||||||
constructor(params) {
|
constructor(params) {
|
||||||
@ -55,7 +56,8 @@ export default class RelicCore {
|
|||||||
list: PackageList,
|
list: PackageList,
|
||||||
read: PackageRead,
|
read: PackageRead,
|
||||||
authorize: PackageAuthorize,
|
authorize: PackageAuthorize,
|
||||||
checkUpdate: PackageCheckUpdate
|
checkUpdate: PackageCheckUpdate,
|
||||||
|
lastOperationRetry: PackageLastOperationRetry,
|
||||||
}
|
}
|
||||||
|
|
||||||
openPath(pkg_id) {
|
openPath(pkg_id) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import winston from "winston"
|
import winston from "winston"
|
||||||
|
import WinstonTransport from "winston-transport"
|
||||||
import colors from "cli-color"
|
import colors from "cli-color"
|
||||||
|
|
||||||
const servicesToColor = {
|
const servicesToColor = {
|
||||||
@ -6,10 +7,6 @@ const servicesToColor = {
|
|||||||
color: "whiteBright",
|
color: "whiteBright",
|
||||||
background: "bgBlackBright",
|
background: "bgBlackBright",
|
||||||
},
|
},
|
||||||
"INSTALL": {
|
|
||||||
color: "whiteBright",
|
|
||||||
background: "bgBlueBright",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const paintText = (level, service, ...args) => {
|
const paintText = (level, service, ...args) => {
|
||||||
@ -27,6 +24,13 @@ const format = winston.format.printf(({ timestamp, service = "CORE", level, mess
|
|||||||
return `${paintText(level, service, `(${level}) [${service}]`)} > ${message}`
|
return `${paintText(level, service, `(${level}) [${service}]`)} > ${message}`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
class EventBusTransport extends WinstonTransport {
|
||||||
|
log(info, next) {
|
||||||
|
global._relic_eventBus.emit(`logger:new`, info)
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default winston.createLogger({
|
export default winston.createLogger({
|
||||||
format: winston.format.combine(
|
format: winston.format.combine(
|
||||||
winston.format.timestamp(),
|
winston.format.timestamp(),
|
||||||
@ -34,6 +38,7 @@ export default winston.createLogger({
|
|||||||
),
|
),
|
||||||
transports: [
|
transports: [
|
||||||
new winston.transports.Console(),
|
new winston.transports.Console(),
|
||||||
|
new EventBusTransport(),
|
||||||
//new winston.transports.File({ filename: "error.log", level: "error" }),
|
//new winston.transports.File({ filename: "error.log", level: "error" }),
|
||||||
//new winston.transports.File({ filename: "combined.log" }),
|
//new winston.transports.File({ filename: "combined.log" }),
|
||||||
],
|
],
|
||||||
|
@ -39,6 +39,15 @@ export async function readManifest(manifest) {
|
|||||||
throw new Error(`Manifest is not a file: ${target}`)
|
throw new Error(`Manifest is not a file: ${target}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copy to cache
|
||||||
|
const cachedManifest = path.join(Vars.cache_path, path.basename(target))
|
||||||
|
|
||||||
|
await fs.promises.copyFile(target, cachedManifest)
|
||||||
|
|
||||||
|
if (!fs.existsSync(cachedManifest)) {
|
||||||
|
throw new Error(`Manifest copy failed: ${target}`)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
remote_manifest: undefined,
|
remote_manifest: undefined,
|
||||||
local_manifest: target,
|
local_manifest: target,
|
||||||
|
@ -1,14 +1,76 @@
|
|||||||
import sendToRender from "../utils/sendToRender"
|
import sendToRender from "../utils/sendToRender"
|
||||||
|
import { ipcMain } from "electron"
|
||||||
|
|
||||||
export default class CoreAdapter {
|
export default class CoreAdapter {
|
||||||
constructor(electronApp, RelicCore) {
|
constructor(electronApp, RelicCore) {
|
||||||
this.app = electronApp
|
this.app = electronApp
|
||||||
this.core = RelicCore
|
this.core = RelicCore
|
||||||
|
this.initialized = false
|
||||||
this.initialize()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
events = {
|
loggerWindow = null
|
||||||
|
|
||||||
|
ipcEvents = {
|
||||||
|
"pkg:list": async () => {
|
||||||
|
return await this.core.package.list()
|
||||||
|
},
|
||||||
|
"pkg:get": async (event, pkg_id) => {
|
||||||
|
return await this.core.db.getPackages(pkg_id)
|
||||||
|
},
|
||||||
|
"pkg:read": async (event, manifest_path, options = {}) => {
|
||||||
|
const manifest = await this.core.package.read(manifest_path, options)
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
...this.core.db.defaultPackageState({ ...manifest }),
|
||||||
|
...manifest,
|
||||||
|
name: manifest.pkg_name,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"pkg:install": async (event, manifest_path) => {
|
||||||
|
return await this.core.package.install(manifest_path)
|
||||||
|
},
|
||||||
|
"pkg:update": async (event, pkg_id, { execOnFinish = false } = {}) => {
|
||||||
|
await this.core.package.update(pkg_id)
|
||||||
|
|
||||||
|
if (execOnFinish) {
|
||||||
|
await this.core.package.execute(pkg_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
"pkg:apply": async (event, pkg_id, changes) => {
|
||||||
|
return await this.core.package.apply(pkg_id, changes)
|
||||||
|
},
|
||||||
|
"pkg:uninstall": async (event, pkg_id) => {
|
||||||
|
return await this.core.package.uninstall(pkg_id)
|
||||||
|
},
|
||||||
|
"pkg:execute": async (event, pkg_id, { force = false } = {}) => {
|
||||||
|
// check for updates first
|
||||||
|
if (!force) {
|
||||||
|
const update = await this.core.package.checkUpdate(pkg_id)
|
||||||
|
|
||||||
|
if (update) {
|
||||||
|
return sendToRender("pkg:update_available", update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.core.package.execute(pkg_id)
|
||||||
|
},
|
||||||
|
"pkg:open": async (event, pkg_id) => {
|
||||||
|
return await this.core.openPath(pkg_id)
|
||||||
|
},
|
||||||
|
"pkg:last_operation_retry": async (event, pkg_id) => {
|
||||||
|
return await this.core.package.lastOperationRetry(pkg_id)
|
||||||
|
},
|
||||||
|
"pkg:cancel_current_operation": async (event, pkg_id) => {
|
||||||
|
return await this.core.package.cancelCurrentOperation(pkg_id)
|
||||||
|
},
|
||||||
|
"core:open-path": async (event, pkg_id) => {
|
||||||
|
return await this.core.openPath(pkg_id)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
coreEvents = {
|
||||||
"pkg:new": (pkg) => {
|
"pkg:new": (pkg) => {
|
||||||
sendToRender("pkg:new", pkg)
|
sendToRender("pkg:new", pkg)
|
||||||
},
|
},
|
||||||
@ -51,22 +113,53 @@ export default class CoreAdapter {
|
|||||||
sendToRender(`new:notification`, {
|
sendToRender(`new:notification`, {
|
||||||
type: "error",
|
type: "error",
|
||||||
message: `An error occurred`,
|
message: `An error occurred`,
|
||||||
description: `Something failed to ${data.event} package ${data.pkg_id}`,
|
description: `Something failed to ${data.event} package ${data.id}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
sendToRender(`pkg:update:state`, data)
|
sendToRender(`pkg:update:state`, data)
|
||||||
|
},
|
||||||
|
"logger:new": (data) => {
|
||||||
|
if (this.loggerWindow) {
|
||||||
|
this.loggerWindow.webContents.send("logger:new", data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize = () => {
|
attachLogger = (window) => {
|
||||||
for (const [key, handler] of Object.entries(this.events)) {
|
this.loggerWindow = window
|
||||||
|
|
||||||
|
window.webContents.send("logger:new", {
|
||||||
|
timestamp: new Date().getTime(),
|
||||||
|
message: "Core adapter Logger attached",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize = async () => {
|
||||||
|
if (this.initialized) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, handler] of Object.entries(this.coreEvents)) {
|
||||||
global._relic_eventBus.on(key, handler)
|
global._relic_eventBus.on(key, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const [key, handler] of Object.entries(this.ipcEvents)) {
|
||||||
|
ipcMain.handle(key, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.core.initialize()
|
||||||
|
await this.core.setup()
|
||||||
|
|
||||||
|
this.initialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
deinitialize = () => {
|
deinitialize = () => {
|
||||||
for (const [key, handler] of Object.entries(this.events)) {
|
for (const [key, handler] of Object.entries(this.coreEvents)) {
|
||||||
global._relic_eventBus.off(key, handler)
|
global._relic_eventBus.off(key, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const [key, handler] of Object.entries(this.ipcEvents)) {
|
||||||
|
ipcMain.removeHandler(key, handler)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -26,62 +26,54 @@ const ProtocolRegistry = require("protocol-registry")
|
|||||||
|
|
||||||
const protocolRegistryNamespace = "relic"
|
const protocolRegistryNamespace = "relic"
|
||||||
|
|
||||||
|
class LogsViewer {
|
||||||
|
window = null
|
||||||
|
|
||||||
|
async createWindow() {
|
||||||
|
this.window = new BrowserWindow({
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
show: false,
|
||||||
|
resizable: true,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
icon: "../../resources/icon.png",
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, "../preload/index.js"),
|
||||||
|
sandbox: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||||
|
this.window.loadURL(`${process.env["ELECTRON_RENDERER_URL"]}/logs`)
|
||||||
|
} else {
|
||||||
|
this.window.loadFile(path.join(__dirname, "../renderer/index.html"))
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => this.window.once("ready-to-show", resolve))
|
||||||
|
|
||||||
|
this.window.show()
|
||||||
|
|
||||||
|
return this.window
|
||||||
|
}
|
||||||
|
|
||||||
|
closeWindow() {
|
||||||
|
if (this.window) {
|
||||||
|
this.window.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ElectronApp {
|
class ElectronApp {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.win = null
|
|
||||||
this.core = new RelicCore()
|
this.core = new RelicCore()
|
||||||
this.adapter = new CoreAdapter(this, this.core)
|
this.adapter = new CoreAdapter(this, this.core)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window = null
|
||||||
|
|
||||||
|
logsViewer = new LogsViewer()
|
||||||
|
|
||||||
handlers = {
|
handlers = {
|
||||||
"pkg:list": async () => {
|
|
||||||
return await this.core.package.list()
|
|
||||||
},
|
|
||||||
"pkg:get": async (event, pkg_id) => {
|
|
||||||
return await this.core.db.getPackages(pkg_id)
|
|
||||||
},
|
|
||||||
"pkg:read": async (event, manifest_path, options = {}) => {
|
|
||||||
const manifest = await this.core.package.read(manifest_path, options)
|
|
||||||
|
|
||||||
return JSON.stringify({
|
|
||||||
...this.core.db.defaultPackageState({ ...manifest }),
|
|
||||||
...manifest,
|
|
||||||
name: manifest.pkg_name,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
"pkg:install": async (event, manifest_path) => {
|
|
||||||
return await this.core.package.install(manifest_path)
|
|
||||||
},
|
|
||||||
"pkg:update": async (event, pkg_id, { execOnFinish = false } = {}) => {
|
|
||||||
await this.core.package.update(pkg_id)
|
|
||||||
|
|
||||||
if (execOnFinish) {
|
|
||||||
await this.core.package.execute(pkg_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
"pkg:apply": async (event, pkg_id, changes) => {
|
|
||||||
return await this.core.package.apply(pkg_id, changes)
|
|
||||||
},
|
|
||||||
"pkg:uninstall": async (event, pkg_id) => {
|
|
||||||
return await this.core.package.uninstall(pkg_id)
|
|
||||||
},
|
|
||||||
"pkg:execute": async (event, pkg_id, { force = false } = {}) => {
|
|
||||||
// check for updates first
|
|
||||||
if (!force) {
|
|
||||||
const update = await this.core.package.checkUpdate(pkg_id)
|
|
||||||
|
|
||||||
if (update) {
|
|
||||||
return sendToRender("pkg:update_available", update)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.core.package.execute(pkg_id)
|
|
||||||
},
|
|
||||||
"pkg:open": async (event, pkg_id) => {
|
|
||||||
return await this.core.openPath(pkg_id)
|
|
||||||
},
|
|
||||||
"updater:check": () => {
|
"updater:check": () => {
|
||||||
autoUpdater.checkForUpdates()
|
autoUpdater.checkForUpdates()
|
||||||
},
|
},
|
||||||
@ -90,22 +82,31 @@ class ElectronApp {
|
|||||||
autoUpdater.quitAndInstall()
|
autoUpdater.quitAndInstall()
|
||||||
}, 3000)
|
}, 3000)
|
||||||
},
|
},
|
||||||
"settings:get": (e, key) => {
|
"settings:get": (event, key) => {
|
||||||
return global.SettingsStore.get(key)
|
return global.SettingsStore.get(key)
|
||||||
},
|
},
|
||||||
"settings:set": (e, key, value) => {
|
"settings:set": (event, key, value) => {
|
||||||
return global.SettingsStore.set(key, value)
|
return global.SettingsStore.set(key, value)
|
||||||
},
|
},
|
||||||
"settings:delete": (e, key) => {
|
"settings:delete": (event, key) => {
|
||||||
return global.SettingsStore.delete(key)
|
return global.SettingsStore.delete(key)
|
||||||
},
|
},
|
||||||
"settings:has": (e, key) => {
|
"settings:has": (event, key) => {
|
||||||
return global.SettingsStore.has(key)
|
return global.SettingsStore.has(key)
|
||||||
},
|
},
|
||||||
|
"app:open-logs": async (event) => {
|
||||||
|
const loggerWindow = await this.logsViewer.createWindow()
|
||||||
|
|
||||||
|
this.adapter.attachLogger(loggerWindow)
|
||||||
|
|
||||||
|
loggerWindow.webContents.send("logger:new", {
|
||||||
|
timestamp: new Date().getTime(),
|
||||||
|
message: "Logger opened, starting watching logs",
|
||||||
|
})
|
||||||
|
},
|
||||||
"app:init": async (event, data) => {
|
"app:init": async (event, data) => {
|
||||||
try {
|
try {
|
||||||
await this.core.initialize()
|
await this.adapter.initialize()
|
||||||
await this.core.setup()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pkg: pkg,
|
pkg: pkg,
|
||||||
@ -126,19 +127,8 @@ class ElectronApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
events = {
|
|
||||||
"open-runtime-path": () => {
|
|
||||||
return this.core.openPath()
|
|
||||||
},
|
|
||||||
"open-dev-logs": () => {
|
|
||||||
return sendToRender("new:message", {
|
|
||||||
message: "Not implemented yet",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createWindow() {
|
createWindow() {
|
||||||
this.win = global.win = new BrowserWindow({
|
this.window = global.mainWindow = new BrowserWindow({
|
||||||
width: 450,
|
width: 450,
|
||||||
height: 670,
|
height: 670,
|
||||||
show: false,
|
show: false,
|
||||||
@ -151,20 +141,20 @@ class ElectronApp {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.win.on("ready-to-show", () => {
|
this.window.on("ready-to-show", () => {
|
||||||
this.win.show()
|
this.window.show()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.win.webContents.setWindowOpenHandler((details) => {
|
this.window.webContents.setWindowOpenHandler((details) => {
|
||||||
shell.openExternal(details.url)
|
shell.openExternal(details.url)
|
||||||
|
|
||||||
return { action: "deny" }
|
return { action: "deny" }
|
||||||
})
|
})
|
||||||
|
|
||||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||||
this.win.loadURL(process.env["ELECTRON_RENDERER_URL"])
|
this.window.loadURL(process.env["ELECTRON_RENDERER_URL"])
|
||||||
} else {
|
} else {
|
||||||
this.win.loadFile(path.join(__dirname, "../renderer/index.html"))
|
this.window.loadFile(path.join(__dirname, "../renderer/index.html"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,12 +196,12 @@ class ElectronApp {
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
// Someone tried to run a second instance, we should focus our window.
|
// Someone tried to run a second instance, we should focus our window.
|
||||||
if (this.win) {
|
if (this.window) {
|
||||||
if (this.win.isMinimized()) {
|
if (this.window.isMinimized()) {
|
||||||
this.win.restore()
|
this.window.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.win.focus()
|
this.window.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Second instance >`, commandLine)
|
console.log(`Second instance >`, commandLine)
|
||||||
@ -235,10 +225,6 @@ class ElectronApp {
|
|||||||
ipcMain.handle(key, this.handlers[key])
|
ipcMain.handle(key, this.handlers[key])
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key in this.events) {
|
|
||||||
ipcMain.on(key, this.events[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
app.on("second-instance", this.handleOnSecondInstance)
|
app.on("second-instance", this.handleOnSecondInstance)
|
||||||
|
|
||||||
app.on("open-url", (event, url) => {
|
app.on("open-url", (event, url) => {
|
||||||
|
@ -32,7 +32,7 @@ export default (event, data) => {
|
|||||||
return copy
|
return copy
|
||||||
}
|
}
|
||||||
|
|
||||||
global.win.webContents.send(event, serializeIpc(data))
|
global.mainWindow.webContents.send(event, serializeIpc(data))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
@ -12,14 +12,19 @@ import AppDrawer from "layout/components/Drawer"
|
|||||||
|
|
||||||
import { InternalRouter, PageRender } from "./router.jsx"
|
import { InternalRouter, PageRender } from "./router.jsx"
|
||||||
|
|
||||||
|
import CrashError from "components/Crash"
|
||||||
|
import LogsViewer from "./pages/logs"
|
||||||
|
|
||||||
// create a global app context
|
// create a global app context
|
||||||
window.app = GlobalApp
|
window.app = GlobalApp
|
||||||
|
|
||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
initializing: true,
|
|
||||||
pkg: null,
|
pkg: null,
|
||||||
|
|
||||||
|
crash: null,
|
||||||
|
initializing: true,
|
||||||
|
|
||||||
appSetup: {
|
appSetup: {
|
||||||
error: false,
|
error: false,
|
||||||
installed: false,
|
installed: false,
|
||||||
@ -98,6 +103,15 @@ class App extends React.Component {
|
|||||||
console.log(`React version > ${versions["react"]}`)
|
console.log(`React version > ${versions["react"]}`)
|
||||||
console.log(`DOMRouter version > ${versions["react-router-dom"]}`)
|
console.log(`DOMRouter version > ${versions["react-router-dom"]}`)
|
||||||
|
|
||||||
|
//check if path is /logs
|
||||||
|
if (window.location.pathname === "/logs") {
|
||||||
|
return await this.setState({
|
||||||
|
initializing: false,
|
||||||
|
no_layout: true,
|
||||||
|
log_viewer_mode: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
window.app.style.appendClassname("initializing")
|
window.app.style.appendClassname("initializing")
|
||||||
|
|
||||||
for (const event in this.ipcEvents) {
|
for (const event in this.ipcEvents) {
|
||||||
@ -133,17 +147,34 @@ class App extends React.Component {
|
|||||||
algorithm: antd.theme.darkAlgorithm
|
algorithm: antd.theme.darkAlgorithm
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<InternalRouter>
|
{
|
||||||
<GlobalStateContext.Provider value={this.state}>
|
this.state.log_viewer_mode && <LogsViewer />
|
||||||
|
}
|
||||||
|
|
||||||
<AppDrawer />
|
{
|
||||||
<AppModalDialog />
|
!this.state.log_viewer_mode && <>
|
||||||
|
<InternalRouter>
|
||||||
|
<GlobalStateContext.Provider value={this.state}>
|
||||||
|
{
|
||||||
|
!this.state.crash && <>
|
||||||
|
<AppDrawer />
|
||||||
|
<AppModalDialog />
|
||||||
|
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
<PageRender />
|
<PageRender />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</GlobalStateContext.Provider>
|
</>
|
||||||
</InternalRouter>
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
this.state.crash && <CrashError
|
||||||
|
crash={this.state.crash}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</GlobalStateContext.Provider>
|
||||||
|
</InternalRouter>
|
||||||
|
</>
|
||||||
|
}
|
||||||
</antd.ConfigProvider>
|
</antd.ConfigProvider>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -14,7 +14,7 @@ const PackageItem = (props) => {
|
|||||||
const isLoading = manifest.last_status === "loading" || manifest.last_status === "installing" || manifest.last_status === "updating"
|
const isLoading = manifest.last_status === "loading" || manifest.last_status === "installing" || manifest.last_status === "updating"
|
||||||
const isInstalling = manifest.last_status === "installing"
|
const isInstalling = manifest.last_status === "installing"
|
||||||
const isInstalled = !!manifest.installed_at
|
const isInstalled = !!manifest.installed_at
|
||||||
const isFailed = manifest.last_status === "error"
|
const isFailed = manifest.last_status === "failed"
|
||||||
|
|
||||||
console.log(manifest, {
|
console.log(manifest, {
|
||||||
isLoading,
|
isLoading,
|
||||||
@ -38,7 +38,7 @@ const PackageItem = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onClickFolder = () => {
|
const onClickFolder = () => {
|
||||||
ipc.exec("pkg:open", manifest.id)
|
ipc.exec("core:open-path", manifest.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClickDelete = () => {
|
const onClickDelete = () => {
|
||||||
@ -60,7 +60,7 @@ const PackageItem = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onClickRetryInstall = () => {
|
const onClickRetryInstall = () => {
|
||||||
ipc.exec("pkg:retry_install", manifest.id)
|
ipc.exec("pkg:last_operation_retry", manifest.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUpdate(event, data) {
|
function handleUpdate(event, data) {
|
||||||
@ -75,7 +75,7 @@ const PackageItem = (props) => {
|
|||||||
return manifest.last_status
|
return manifest.last_status
|
||||||
}
|
}
|
||||||
|
|
||||||
return `v${manifest.version}` ?? "N/A"
|
return `${isFailed ? "failed |" : ""} v${manifest.version}` ?? "N/A"
|
||||||
}
|
}
|
||||||
|
|
||||||
const MenuProps = {
|
const MenuProps = {
|
||||||
@ -148,7 +148,6 @@ const PackageItem = (props) => {
|
|||||||
manifest.icon && <img src={manifest.icon} className="installation_item_icon" />
|
manifest.icon && <img src={manifest.icon} className="installation_item_icon" />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
<div className="installation_item_info">
|
<div className="installation_item_info">
|
||||||
<h2>
|
<h2>
|
||||||
{
|
{
|
||||||
@ -164,16 +163,24 @@ const PackageItem = (props) => {
|
|||||||
|
|
||||||
<div className="installation_item_actions">
|
<div className="installation_item_actions">
|
||||||
{
|
{
|
||||||
isFailed && <antd.Button
|
isFailed && <>
|
||||||
type="primary"
|
<antd.Button
|
||||||
onClick={onClickRetryInstall}
|
type="primary"
|
||||||
>
|
onClick={onClickRetryInstall}
|
||||||
Retry
|
>
|
||||||
</antd.Button>
|
Retry
|
||||||
|
</antd.Button>
|
||||||
|
|
||||||
|
<antd.Button
|
||||||
|
icon={<MdDelete />}
|
||||||
|
type="primary"
|
||||||
|
onClick={onClickDelete}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isInstalled && manifest.executable && <antd.Dropdown.Button
|
!isFailed && isInstalled && manifest.executable && <antd.Dropdown.Button
|
||||||
menu={MenuProps}
|
menu={MenuProps}
|
||||||
onClick={onClickPlay}
|
onClick={onClickPlay}
|
||||||
type="primary"
|
type="primary"
|
||||||
@ -186,7 +193,7 @@ const PackageItem = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isInstalled && !manifest.executable && <antd.Dropdown
|
isFailed && isInstalled && !manifest.executable && <antd.Dropdown
|
||||||
menu={MenuProps}
|
menu={MenuProps}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
@ -199,7 +206,7 @@ const PackageItem = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isInstalling && <antd.Button
|
isFailed && isInstalling && <antd.Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={onClickCancelInstall}
|
onClick={onClickCancelInstall}
|
||||||
>
|
>
|
||||||
|
67
packages/gui/src/renderer/src/pages/logs/index.jsx
Normal file
67
packages/gui/src/renderer/src/pages/logs/index.jsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
const Timestamp = ({ timestamp }) => {
|
||||||
|
if (isNaN(timestamp)) {
|
||||||
|
return <span className="timestamp">{timestamp}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span
|
||||||
|
className="timestamp"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
new Date(timestamp).toLocaleString().split(", ").join("|")
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
const LogEntry = ({ log }) => {
|
||||||
|
return <div className="log-entry">
|
||||||
|
<span className="line_indicator">
|
||||||
|
{">"}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{log.timestamp && <Timestamp timestamp={log.timestamp} />}
|
||||||
|
|
||||||
|
{!log.timestamp && <span className="timestamp">- no timestamp -</span>}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{log.message ?? "No message"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const LogsViewer = () => {
|
||||||
|
const listRef = React.useRef()
|
||||||
|
const [timeline, setTimeline] = React.useState([])
|
||||||
|
|
||||||
|
const events = {
|
||||||
|
"logger:new": (event, log) => {
|
||||||
|
setTimeline((timeline) => [...timeline, log])
|
||||||
|
|
||||||
|
listRef.current.scrollTop = listRef.current.scrollHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
for (const event in events) {
|
||||||
|
ipc.exclusiveListen(event, events[event])
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return <div
|
||||||
|
className="app-logs"
|
||||||
|
ref={listRef}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
timeline.length === 0 && <p>No logs</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
timeline.map((log) => <LogEntry key={log.id} log={log} />)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LogsViewer
|
47
packages/gui/src/renderer/src/pages/logs/index.less
Normal file
47
packages/gui/src/renderer/src/pages/logs/index.less
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
.app-logs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
font-family: "DM Mono", monospace;
|
||||||
|
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
gap: 7px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 0.8rem;
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
opacity: 0.9;
|
||||||
|
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(odd) {
|
||||||
|
background-color: var(--background-color-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -307,8 +307,6 @@ const PackageOptionsLoader = (props) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(manifest)
|
|
||||||
|
|
||||||
if (!manifest) {
|
if (!manifest) {
|
||||||
return <antd.Skeleton active />
|
return <antd.Skeleton active />
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import loadable from "@loadable/component"
|
|||||||
import GlobalStateContext from "contexts/global"
|
import GlobalStateContext from "contexts/global"
|
||||||
|
|
||||||
import SplashScreen from "components/Splash"
|
import SplashScreen from "components/Splash"
|
||||||
import CrashError from "components/Crash"
|
|
||||||
|
|
||||||
const DefaultNotFoundRender = () => {
|
const DefaultNotFoundRender = () => {
|
||||||
return <div>Not found</div>
|
return <div>Not found</div>
|
||||||
@ -131,18 +130,6 @@ export const InternalRouter = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PageRender = (props) => {
|
export const PageRender = (props) => {
|
||||||
const globalState = React.useContext(GlobalStateContext)
|
|
||||||
|
|
||||||
if (globalState.crash) {
|
|
||||||
return <CrashError
|
|
||||||
crash={globalState.crash}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (globalState.initializing) {
|
|
||||||
return <SplashScreen />
|
|
||||||
}
|
|
||||||
|
|
||||||
const routes = React.useMemo(() => {
|
const routes = React.useMemo(() => {
|
||||||
let paths = {
|
let paths = {
|
||||||
...import.meta.glob("/src/pages/**/[a-z[]*.jsx"),
|
...import.meta.glob("/src/pages/**/[a-z[]*.jsx"),
|
||||||
@ -167,6 +154,12 @@ export const PageRender = (props) => {
|
|||||||
return paths
|
return paths
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const globalState = React.useContext(GlobalStateContext)
|
||||||
|
|
||||||
|
if (globalState.initializing) {
|
||||||
|
return <SplashScreen />
|
||||||
|
}
|
||||||
|
|
||||||
return <Routes>
|
return <Routes>
|
||||||
{
|
{
|
||||||
routes.map((route, index) => {
|
routes.map((route, index) => {
|
||||||
|
@ -20,6 +20,7 @@ export default [
|
|||||||
render: (props) => {
|
render: (props) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
disabled
|
||||||
type={props.value ? "primary" : "default"}
|
type={props.value ? "primary" : "default"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!props.value) {
|
if (!props.value) {
|
||||||
@ -65,7 +66,10 @@ export default [
|
|||||||
icon: "MdUpdate",
|
icon: "MdUpdate",
|
||||||
type: "switch",
|
type: "switch",
|
||||||
storaged: true,
|
storaged: true,
|
||||||
defaultValue: false
|
defaultValue: false,
|
||||||
|
props: {
|
||||||
|
disabled: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -83,7 +87,7 @@ export default [
|
|||||||
props: {
|
props: {
|
||||||
children: "Open",
|
children: "Open",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
ipc.send("open-runtime-path")
|
ipc.exec("core:open-path")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
storaged: false
|
storaged: false
|
||||||
@ -97,7 +101,7 @@ export default [
|
|||||||
props: {
|
props: {
|
||||||
children: "Open",
|
children: "Open",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
ipc.send("open-dev-logs")
|
ipc.exec("app:open-logs")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
storaged: false
|
storaged: false
|
||||||
|
Loading…
x
Reference in New Issue
Block a user