mirror of
https://github.com/ragestudio/relic.git
synced 2025-06-09 02:24:18 +00:00
init
This commit is contained in:
commit
2f8f735710
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
4
.eslintignore
Normal file
4
.eslintignore
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
out
|
||||
.gitignore
|
9
.eslintrc.cjs
Normal file
9
.eslintrc.cjs
Normal file
@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'@electron-toolkit',
|
||||
'@electron-toolkit/eslint-config-prettier'
|
||||
]
|
||||
}
|
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
# Secrets
|
||||
/**/**/.env
|
||||
/**/**/origin.server
|
||||
/**/**/server.manifest
|
||||
/**/**/server.registry
|
||||
|
||||
/**/**/_shared
|
||||
|
||||
# Trash
|
||||
/**/**/*.log
|
||||
/**/**/dumps.log
|
||||
/**/**/.crash.log
|
||||
/**/**/.tmp
|
||||
/**/**/.cache
|
||||
/**/**/cache
|
||||
/**/**/out
|
||||
/**/**/.out
|
||||
/**/**/dist
|
||||
/**/**/node_modules
|
||||
/**/**/corenode_modules
|
||||
/**/**/.DS_Store
|
||||
/**/**/package-lock.json
|
||||
/**/**/yarn.lock
|
||||
/**/**/.evite
|
||||
/**/**/build
|
||||
/**/**/uploads
|
||||
/**/**/d_data
|
||||
/**/**/*.tar
|
||||
/**/**/*.7z
|
||||
/**/**/*.zip
|
||||
/**/**/*.env
|
||||
|
||||
# Logs
|
||||
/**/**/npm-debug.log*
|
||||
/**/**/yarn-error.log
|
||||
/**/**/dumps.log
|
||||
/**/**/corenode.log
|
||||
|
||||
# Temporal configurations
|
||||
/**/**/.aliaser
|
6
.prettierignore
Normal file
6
.prettierignore
Normal file
@ -0,0 +1,6 @@
|
||||
out
|
||||
dist
|
||||
pnpm-lock.yaml
|
||||
LICENSE.md
|
||||
tsconfig.json
|
||||
tsconfig.*.json
|
4
.prettierrc.yaml
Normal file
4
.prettierrc.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
singleQuote: true
|
||||
semi: false
|
||||
printWidth: 100
|
||||
trailingComma: none
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["dbaeumer.vscode-eslint"]
|
||||
}
|
39
.vscode/launch.json
vendored
Normal file
39
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Main Process",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
|
||||
},
|
||||
"runtimeArgs": ["--sourcemap"],
|
||||
"env": {
|
||||
"REMOTE_DEBUGGING_PORT": "9222"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Debug Renderer Process",
|
||||
"port": 9222,
|
||||
"request": "attach",
|
||||
"type": "chrome",
|
||||
"webRoot": "${workspaceFolder}/src/renderer",
|
||||
"timeout": 60000,
|
||||
"presentation": {
|
||||
"hidden": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Debug All",
|
||||
"configurations": ["Debug Main Process", "Debug Renderer Process"],
|
||||
"presentation": {
|
||||
"order": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
34
README.md
Normal file
34
README.md
Normal file
@ -0,0 +1,34 @@
|
||||
# rs-bundler
|
||||
|
||||
An Electron application with React
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||
|
||||
## Project Setup
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
$ npm run dev
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
# For windows
|
||||
$ npm run build:win
|
||||
|
||||
# For macOS
|
||||
$ npm run build:mac
|
||||
|
||||
# For Linux
|
||||
$ npm run build:linux
|
||||
```
|
3
dev-app-update.yml
Normal file
3
dev-app-update.yml
Normal file
@ -0,0 +1,3 @@
|
||||
provider: generic
|
||||
url: https://example.com/auto-updates
|
||||
updaterCacheDirName: rs-bundler-updater
|
42
electron-builder.yml
Normal file
42
electron-builder.yml
Normal file
@ -0,0 +1,42 @@
|
||||
appId: com.electron.app
|
||||
productName: rs-bundler
|
||||
directories:
|
||||
buildResources: build
|
||||
files:
|
||||
- '!**/.vscode/*'
|
||||
- '!src/*'
|
||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||
asarUnpack:
|
||||
- resources/**
|
||||
win:
|
||||
executableName: rs-bundler
|
||||
nsis:
|
||||
artifactName: ${name}-${version}-setup.${ext}
|
||||
shortcutName: ${productName}
|
||||
uninstallDisplayName: ${productName}
|
||||
createDesktopShortcut: always
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||
notarize: false
|
||||
dmg:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
linux:
|
||||
target:
|
||||
- AppImage
|
||||
- snap
|
||||
- deb
|
||||
maintainer: electronjs.org
|
||||
category: Utility
|
||||
appImage:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
npmRebuild: false
|
||||
publish:
|
||||
provider: generic
|
||||
url: https://example.com/auto-updates
|
34
electron.vite.config.js
Normal file
34
electron.vite.config.js
Normal file
@ -0,0 +1,34 @@
|
||||
import { resolve } from 'path'
|
||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
},
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
},
|
||||
renderer: {
|
||||
resolve: {
|
||||
alias: {
|
||||
"style": resolve('src/renderer/src/style'),
|
||||
"components": resolve('src/renderer/src/components'),
|
||||
"utils": resolve('src/renderer/src/utils'),
|
||||
"contexts": resolve('src/renderer/src/contexts'),
|
||||
"pages": resolve('src/renderer/src/pages'),
|
||||
"hooks": resolve('src/renderer/src/hooks'),
|
||||
"services": resolve('src/renderer/src/services'),
|
||||
'@renderer': resolve('src/renderer/src')
|
||||
}
|
||||
},
|
||||
plugins: [react()],
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
less: {
|
||||
javascriptEnabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
47
package.json
Normal file
47
package.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "rs-bundler",
|
||||
"version": "0.1.0",
|
||||
"description": "An Electron application with React",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "RageStudio",
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "electron-vite dev",
|
||||
"build": "electron-vite build",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"build:win": "npm run build && electron-builder --win --config",
|
||||
"build:mac": "npm run build && electron-builder --mac --config",
|
||||
"build:linux": "npm run build && electron-builder --linux --config"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^2.0.0",
|
||||
"@electron-toolkit/utils": "^2.0.0",
|
||||
"antd": "^5.10.2",
|
||||
"classnames": "^2.3.2",
|
||||
"electron-updater": "^6.1.1",
|
||||
"got": "11.8.3",
|
||||
"less": "^4.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"node-7z": "^3.0.0",
|
||||
"open": "8.4.2",
|
||||
"react-icons": "^4.11.0",
|
||||
"react-spinners": "^0.13.8",
|
||||
"rimraf": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config": "^1.0.1",
|
||||
"@electron-toolkit/eslint-config-prettier": "^1.0.1",
|
||||
"@vitejs/plugin-react": "^4.0.4",
|
||||
"electron": "^25.6.0",
|
||||
"electron-builder": "^24.6.3",
|
||||
"electron-vite": "^1.0.27",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"prettier": "^3.0.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"vite": "^4.4.9"
|
||||
}
|
||||
}
|
BIN
resources/icon.png
Normal file
BIN
resources/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
116
src/main/index.js
Normal file
116
src/main/index.js
Normal file
@ -0,0 +1,116 @@
|
||||
import path from "node:path"
|
||||
|
||||
import { app, shell, BrowserWindow, ipcMain } from "electron"
|
||||
import { electronApp, optimizer, is } from "@electron-toolkit/utils"
|
||||
|
||||
import open from "open"
|
||||
|
||||
import icon from "../../resources/icon.png?asset"
|
||||
import pkg from "../../package.json"
|
||||
|
||||
import setup from "./setup"
|
||||
|
||||
import PkgManager from "./pkgManager"
|
||||
|
||||
class ElectronApp {
|
||||
constructor() {
|
||||
this.pkgManager = new PkgManager()
|
||||
this.win = null
|
||||
}
|
||||
|
||||
handlers = {
|
||||
pkg: () => {
|
||||
return pkg
|
||||
},
|
||||
"get:installations": async () => {
|
||||
return await this.pkgManager.getInstallations()
|
||||
},
|
||||
"bundle:update": (event, manifest_id) => {
|
||||
this.pkgManager.update(manifest_id)
|
||||
},
|
||||
"bundle:exec": (event, manifest_id) => {
|
||||
this.pkgManager.exec(manifest_id)
|
||||
},
|
||||
"bundle:install": async (event, manifest) => {
|
||||
this.pkgManager.install(manifest)
|
||||
},
|
||||
"bundle:uninstall": (event, manifest_id) => {
|
||||
this.pkgManager.uninstall(manifest_id)
|
||||
},
|
||||
"bundle:open": (event, manifest_id) => {
|
||||
this.pkgManager.openBundleFolder(manifest_id)
|
||||
},
|
||||
"check:setup": async () => {
|
||||
return await setup()
|
||||
}
|
||||
}
|
||||
|
||||
events = {
|
||||
"open-runtime-path": () => {
|
||||
return open(this.pkgManager.runtimePath)
|
||||
},
|
||||
}
|
||||
|
||||
createWindow() {
|
||||
this.win = global.win = new BrowserWindow({
|
||||
width: 450,
|
||||
height: 670,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
...(process.platform === "linux" ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "../preload/index.js"),
|
||||
sandbox: false
|
||||
}
|
||||
})
|
||||
|
||||
this.win.on("ready-to-show", () => {
|
||||
this.win.show()
|
||||
})
|
||||
|
||||
this.win.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
|
||||
return { action: "deny" }
|
||||
})
|
||||
|
||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||
this.win.loadURL(process.env["ELECTRON_RENDERER_URL"])
|
||||
} else {
|
||||
this.win.loadFile(path.join(__dirname, "../renderer/index.html"))
|
||||
}
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
for (const key in this.handlers) {
|
||||
ipcMain.handle(key, this.handlers[key])
|
||||
}
|
||||
|
||||
for (const key in this.events) {
|
||||
ipcMain.on(key, this.events[key])
|
||||
}
|
||||
|
||||
await app.whenReady()
|
||||
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId("com.electron")
|
||||
|
||||
app.on("browser-window-created", (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window)
|
||||
})
|
||||
|
||||
this.createWindow()
|
||||
|
||||
app.on("activate", function () {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
new ElectronApp().initialize()
|
601
src/main/pkgManager.js
Normal file
601
src/main/pkgManager.js
Normal file
@ -0,0 +1,601 @@
|
||||
import path from "node:path"
|
||||
import { pipeline as streamPipeline } from "node:stream/promises"
|
||||
import ChildProcess from "node:child_process"
|
||||
import fs from "node:fs"
|
||||
import os from "node:os"
|
||||
|
||||
import open from "open"
|
||||
import got from "got"
|
||||
import { extractFull } from "node-7z"
|
||||
import { rimraf } from "rimraf"
|
||||
import lodash from "lodash"
|
||||
|
||||
import pkg from "../../package.json"
|
||||
|
||||
global.OS_USERDATA_PATH = path.resolve(
|
||||
process.env.APPDATA ||
|
||||
(process.platform == "darwin" ? process.env.HOME + "/Library/Preferences" : process.env.HOME + "/.local/share"),
|
||||
)
|
||||
|
||||
global.RUNTIME_PATH = path.join(global.OS_USERDATA_PATH, "rs-bundler")
|
||||
|
||||
const TMP_PATH = path.resolve(os.tmpdir(), "RS-MCPacks")
|
||||
const INSTALLERS_PATH = path.join(global.RUNTIME_PATH, "installers")
|
||||
const MANIFEST_PATH = path.join(global.RUNTIME_PATH, "manifests")
|
||||
|
||||
const RealmDBDefault = {
|
||||
created_at_version: pkg.version,
|
||||
installations: [],
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function sendToRenderer(event, data) {
|
||||
global.win.webContents.send(event, serializeIpc(data))
|
||||
}
|
||||
|
||||
async function fetchAndCreateModule(manifest) {
|
||||
console.log(`Fetching ${manifest}...`)
|
||||
|
||||
try {
|
||||
const response = await got.get(manifest)
|
||||
const moduleCode = response.body
|
||||
|
||||
const newModule = new module.constructor()
|
||||
newModule._compile(moduleCode, manifest)
|
||||
|
||||
return newModule
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function readManifest(manifest, { just_read = false } = {}) {
|
||||
// check if manifest is a directory or a url
|
||||
const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi
|
||||
|
||||
if (urlRegex.test(manifest)) {
|
||||
const _module = await fetchAndCreateModule(manifest)
|
||||
const remoteUrl = lodash.clone(manifest)
|
||||
|
||||
manifest = _module.exports
|
||||
|
||||
manifest.remote_url = remoteUrl
|
||||
} else {
|
||||
if (!fs.existsSync(manifest)) {
|
||||
throw new Error(`Manifest not found: ${manifest}`)
|
||||
}
|
||||
|
||||
if (!fs.statSync(manifest).isFile()) {
|
||||
throw new Error(`Manifest is not a file: ${manifest}`)
|
||||
}
|
||||
|
||||
const manifestFilePath = lodash.clone(manifest)
|
||||
|
||||
manifest = require(manifest)
|
||||
|
||||
if (!just_read) {
|
||||
// copy manifest
|
||||
fs.copyFileSync(manifestFilePath, path.resolve(MANIFEST_PATH, path.basename(manifest)))
|
||||
|
||||
manifest.remote_url = manifestFilePath
|
||||
}
|
||||
}
|
||||
|
||||
return manifest
|
||||
}
|
||||
|
||||
export default class PkgManager {
|
||||
constructor() {
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
get realmDbPath() {
|
||||
return path.join(RUNTIME_PATH, "local_realm.json")
|
||||
}
|
||||
|
||||
get runtimePath() {
|
||||
return RUNTIME_PATH
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
if (!fs.existsSync(RUNTIME_PATH)) {
|
||||
fs.mkdirSync(RUNTIME_PATH, { recursive: true })
|
||||
}
|
||||
|
||||
if (!fs.existsSync(INSTALLERS_PATH)) {
|
||||
fs.mkdirSync(INSTALLERS_PATH, { recursive: true })
|
||||
}
|
||||
|
||||
if (!fs.existsSync(MANIFEST_PATH)) {
|
||||
fs.mkdirSync(MANIFEST_PATH, { recursive: true })
|
||||
}
|
||||
|
||||
if (!fs.existsSync(TMP_PATH)) {
|
||||
fs.mkdirSync(TMP_PATH, { recursive: true })
|
||||
}
|
||||
|
||||
if (!fs.existsSync(this.realmDbPath)) {
|
||||
console.log(`Creating default realm db...`, this.realmDbPath)
|
||||
|
||||
await this.writeDb(RealmDBDefault)
|
||||
}
|
||||
}
|
||||
|
||||
// DB Operations
|
||||
async readDb() {
|
||||
return JSON.parse(await fs.promises.readFile(this.realmDbPath, "utf8"))
|
||||
}
|
||||
|
||||
async writeDb(data) {
|
||||
return fs.promises.writeFile(this.realmDbPath, JSON.stringify(data, null, 2))
|
||||
}
|
||||
|
||||
async appendInstallation(manifest) {
|
||||
const db = await this.readDb()
|
||||
|
||||
const prevIndex = db.installations.findIndex((i) => i.id === manifest.id)
|
||||
|
||||
if (prevIndex !== -1) {
|
||||
db.installations[prevIndex] = manifest
|
||||
} else {
|
||||
db.installations.push(manifest)
|
||||
}
|
||||
|
||||
await this.writeDb(db)
|
||||
}
|
||||
|
||||
// CRUD Operations
|
||||
async getInstallations() {
|
||||
const db = await this.readDb()
|
||||
|
||||
return db.installations
|
||||
}
|
||||
|
||||
async openBundleFolder(manifest_id) {
|
||||
const db = await this.readDb()
|
||||
|
||||
const index = db.installations.findIndex((i) => i.id === manifest_id)
|
||||
|
||||
if (index !== -1) {
|
||||
const manifest = db.installations[index]
|
||||
|
||||
open(manifest.install_path)
|
||||
}
|
||||
}
|
||||
|
||||
async install(manifest) {
|
||||
let pendingTasks = []
|
||||
|
||||
manifest = await readManifest(manifest).catch((error) => {
|
||||
sendToRenderer("runtime:error", "Cannot fetch this manifest")
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
if (!manifest) {
|
||||
return false
|
||||
}
|
||||
|
||||
const packPath = path.resolve(INSTALLERS_PATH, manifest.id)
|
||||
|
||||
if (typeof manifest.init === "function") {
|
||||
const init_result = await manifest.init({
|
||||
pack_dir: packPath,
|
||||
tmp_dir: TMP_PATH
|
||||
})
|
||||
|
||||
manifest = {
|
||||
...manifest,
|
||||
...init_result,
|
||||
}
|
||||
|
||||
delete manifest.init
|
||||
}
|
||||
|
||||
manifest.status = "installing"
|
||||
|
||||
console.log(`Starting to install ${manifest.pack_name}...`)
|
||||
console.log(`Installing at >`, packPath)
|
||||
|
||||
sendToRenderer("new:installation", manifest)
|
||||
|
||||
fs.mkdirSync(packPath, { recursive: true })
|
||||
|
||||
await this.appendInstallation(manifest)
|
||||
|
||||
try {
|
||||
if (typeof manifest.git_clones_steps !== "undefined" && Array.isArray(manifest.git_clones_steps)) {
|
||||
for await (const step of manifest.git_clones_steps) {
|
||||
const _path = path.resolve(packPath, step.path)
|
||||
|
||||
console.log(`Cloning ${step.url}...`)
|
||||
|
||||
sendToRenderer(`installation:status`, {
|
||||
...manifest,
|
||||
statusText: `Cloning ${step.url}`,
|
||||
})
|
||||
|
||||
fs.mkdirSync(_path, { recursive: true })
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const process = ChildProcess.exec(`git clone --recurse-submodules --remote-submodules ${step.url} ${_path}`, {
|
||||
shell: true,
|
||||
})
|
||||
|
||||
process.on("exit", resolve)
|
||||
process.on("error", reject)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof manifest.http_downloads !== "undefined" && Array.isArray(manifest.http_downloads)) {
|
||||
for await (const step of manifest.http_downloads) {
|
||||
let _path = path.resolve(packPath, step.path ?? ".")
|
||||
|
||||
console.log(`Downloading ${step.url}...`)
|
||||
|
||||
sendToRenderer(`installation:status`, {
|
||||
...manifest,
|
||||
statusText: `Downloading ${step.url}`,
|
||||
})
|
||||
|
||||
if (step.tmp) {
|
||||
_path = path.resolve(TMP_PATH, String(new Date().getTime()))
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.resolve(_path, ".."), { recursive: true })
|
||||
|
||||
await streamPipeline(
|
||||
got.stream(step.url),
|
||||
fs.createWriteStream(_path)
|
||||
)
|
||||
|
||||
if (step.execute) {
|
||||
pendingTasks.push(async () => {
|
||||
await new Promise(async (resolve, reject) => {
|
||||
const process = ChildProcess.execFile(_path, {
|
||||
shell: true,
|
||||
}, (error) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
|
||||
process.on("exit", resolve)
|
||||
process.on("error", reject)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (step.extract) {
|
||||
console.log(`Extracting ${step.extract}...`)
|
||||
|
||||
sendToRenderer(`installation:status`, {
|
||||
...manifest,
|
||||
statusText: `Extracting bundle ${step.extract}`,
|
||||
})
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const extract = extractFull(_path, step.extract, {
|
||||
$bin: global.SEVENZIP_PATH
|
||||
})
|
||||
|
||||
extract.on("error", reject)
|
||||
extract.on("end", resolve)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pendingTasks.length > 0) {
|
||||
console.log(`Performing pending tasks...`)
|
||||
|
||||
sendToRenderer(`installation:status`, {
|
||||
...manifest,
|
||||
statusText: `Performing pending tasks...`,
|
||||
})
|
||||
|
||||
for await (const task of pendingTasks) {
|
||||
await task()
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof manifest.after_install === "function") {
|
||||
console.log(`Performing after_install hook...`)
|
||||
|
||||
sendToRenderer(`installation:status`, {
|
||||
...manifest,
|
||||
statusText: `Performing after_install hook...`,
|
||||
})
|
||||
|
||||
await manifest.after_install({
|
||||
manifest,
|
||||
pack_dir: packPath,
|
||||
tmp_dir: TMP_PATH
|
||||
})
|
||||
}
|
||||
|
||||
manifest.status = "installed"
|
||||
manifest.install_path = packPath
|
||||
manifest.installed_at = new Date()
|
||||
manifest.last_update = null
|
||||
|
||||
await this.appendInstallation(manifest)
|
||||
|
||||
console.log(`Successfully installed ${manifest.pack_name}!`)
|
||||
|
||||
sendToRenderer(`installation:done`, {
|
||||
...manifest,
|
||||
statusText: "Successfully installed",
|
||||
})
|
||||
} catch (error) {
|
||||
manifest.status = "failed"
|
||||
|
||||
sendToRenderer(`installation:error`, {
|
||||
...manifest,
|
||||
statusText: error.toString(),
|
||||
})
|
||||
|
||||
console.error(error)
|
||||
|
||||
fs.rmdirSync(packPath, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
async uninstall(manifest_id) {
|
||||
console.log(`Uninstalling >`, manifest_id)
|
||||
|
||||
sendToRenderer("installation:status", {
|
||||
status: "uninstalling",
|
||||
id: manifest_id,
|
||||
statusText: `Uninstalling ${manifest_id}...`,
|
||||
})
|
||||
|
||||
const db = await this.readDb()
|
||||
|
||||
const manifest = db.installations.find((i) => i.id === manifest_id)
|
||||
|
||||
if (!manifest) {
|
||||
sendToRenderer("runtime:error", "Manifest not found")
|
||||
return false
|
||||
}
|
||||
|
||||
if (manifest.remote_url) {
|
||||
const remoteManifest = await readManifest(manifest.remote_url, { just_read: true })
|
||||
|
||||
if (typeof remoteManifest.uninstall === "function") {
|
||||
console.log(`Performing uninstall hook...`)
|
||||
|
||||
await remoteManifest.uninstall({
|
||||
manifest: remoteManifest,
|
||||
pack_dir: remoteManifest.install_path,
|
||||
tmp_dir: TMP_PATH,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await rimraf(manifest.install_path)
|
||||
|
||||
db.installations = db.installations.filter((i) => i.id !== manifest_id)
|
||||
|
||||
await this.writeDb(db)
|
||||
|
||||
sendToRenderer("installation:uninstalled", {
|
||||
id: manifest_id,
|
||||
})
|
||||
}
|
||||
|
||||
async update(manifest_id) {
|
||||
try {
|
||||
let pendingTasks = []
|
||||
|
||||
console.log(`Updating >`, manifest_id)
|
||||
|
||||
sendToRenderer("installation:status", {
|
||||
status: "updating",
|
||||
id: manifest_id,
|
||||
statusText: `Updating ${manifest_id}...`,
|
||||
})
|
||||
|
||||
const db = await this.readDb()
|
||||
|
||||
let manifest = db.installations.find((i) => i.id === manifest_id)
|
||||
|
||||
if (!manifest) {
|
||||
sendToRenderer("runtime:error", "Manifest not found")
|
||||
return false
|
||||
}
|
||||
|
||||
console.log(manifest)
|
||||
|
||||
const packPath = manifest.install_path
|
||||
|
||||
if (manifest.remote_url) {
|
||||
manifest = await readManifest(manifest.remote_url, { just_read: true })
|
||||
}
|
||||
|
||||
manifest.status = "updating"
|
||||
|
||||
if (typeof manifest.init === "function") {
|
||||
const init_result = await manifest.init({
|
||||
pack_dir: packPath,
|
||||
tmp_dir: TMP_PATH
|
||||
})
|
||||
|
||||
manifest = {
|
||||
...manifest,
|
||||
...init_result,
|
||||
}
|
||||
|
||||
delete manifest.init
|
||||
}
|
||||
|
||||
console.log(manifest)
|
||||
|
||||
if (typeof manifest.update === "function") {
|
||||
sendToRenderer(`installation:status`, {
|
||||
...manifest,
|
||||
statusText: `Performing update hook...`,
|
||||
})
|
||||
|
||||
console.log(`Performing update hook...`)
|
||||
|
||||
await manifest.update({
|
||||
manifest,
|
||||
pack_dir: packPath,
|
||||
tmp_dir: TMP_PATH
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof manifest.git_update !== "undefined" && Array.isArray(manifest.git_update)) {
|
||||
for await (const step of manifest.git_update) {
|
||||
const _path = path.resolve(packPath, step.path)
|
||||
|
||||
console.log(`GIT Pulling ${step.url}`)
|
||||
|
||||
sendToRenderer(`installation:status`, {
|
||||
...manifest,
|
||||
statusText: `GIT Pulling ${step.url}`,
|
||||
})
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const process = ChildProcess.exec(`git pull`, {
|
||||
cwd: _path,
|
||||
shell: true,
|
||||
})
|
||||
|
||||
process.on("exit", resolve)
|
||||
process.on("error", reject)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof manifest.http_downloads !== "undefined" && Array.isArray(manifest.http_downloads)) {
|
||||
for await (const step of manifest.http_downloads) {
|
||||
let _path = path.resolve(packPath, step.path ?? ".")
|
||||
|
||||
console.log(`Downloading ${step.url}...`)
|
||||
|
||||
sendToRenderer(`installation:status`, {
|
||||
...manifest,
|
||||
statusText: `Downloading ${step.url}`,
|
||||
})
|
||||
|
||||
if (step.tmp) {
|
||||
_path = path.resolve(TMP_PATH, String(new Date().getTime()))
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.resolve(_path, ".."), { recursive: true })
|
||||
|
||||
await streamPipeline(
|
||||
got.stream(step.url),
|
||||
fs.createWriteStream(_path)
|
||||
)
|
||||
|
||||
if (step.execute) {
|
||||
pendingTasks.push(async () => {
|
||||
await new Promise(async (resolve, reject) => {
|
||||
const process = ChildProcess.execFile(_path, {
|
||||
shell: true,
|
||||
}, (error) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
|
||||
process.on("exit", resolve)
|
||||
process.on("error", reject)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (step.extract) {
|
||||
console.log(`Extracting ${step.extract}...`)
|
||||
|
||||
sendToRenderer(`installation:status`, {
|
||||
...manifest,
|
||||
statusText: `Extracting bundle ${step.extract}`,
|
||||
})
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const extract = extractFull(_path, step.extract, {
|
||||
$bin: global.SEVENZIP_PATH
|
||||
})
|
||||
|
||||
extract.on("error", reject)
|
||||
extract.on("end", resolve)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pendingTasks.length > 0) {
|
||||
console.log(`Performing pending tasks...`)
|
||||
|
||||
sendToRenderer(`installation:status`, {
|
||||
...manifest,
|
||||
statusText: `Performing pending tasks...`,
|
||||
})
|
||||
|
||||
for await (const task of pendingTasks) {
|
||||
await task()
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof manifest.after_install === "function") {
|
||||
console.log(`Performing after_install hook...`)
|
||||
|
||||
sendToRenderer(`installation:status`, {
|
||||
...manifest,
|
||||
statusText: `Performing after_install hook...`,
|
||||
})
|
||||
|
||||
await manifest.after_install({
|
||||
manifest,
|
||||
pack_dir: packPath,
|
||||
tmp_dir: TMP_PATH
|
||||
})
|
||||
}
|
||||
|
||||
manifest.status = "installed"
|
||||
manifest.install_path = packPath
|
||||
manifest.last_update = new Date()
|
||||
|
||||
await this.appendInstallation(manifest)
|
||||
|
||||
console.log(`Successfully updated ${manifest.pack_name}!`)
|
||||
|
||||
sendToRenderer(`installation:done`, {
|
||||
...manifest,
|
||||
statusText: "Successfully updated",
|
||||
})
|
||||
} catch (error) {
|
||||
manifest.status = "failed"
|
||||
|
||||
sendToRenderer(`installation:error`, {
|
||||
...manifest,
|
||||
statusText: error.toString(),
|
||||
})
|
||||
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
74
src/main/setup.js
Normal file
74
src/main/setup.js
Normal file
@ -0,0 +1,74 @@
|
||||
import path from "node:path"
|
||||
import fs from "node:fs"
|
||||
import os from "node:os"
|
||||
import ChildProcess from "node:child_process"
|
||||
import { pipeline as streamPipeline } from "node:stream/promises"
|
||||
|
||||
import got from "got"
|
||||
|
||||
function resolveDestBin(pre, post) {
|
||||
let url = null
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
url = `${pre}/mac/${process.arch}/${post}`
|
||||
}
|
||||
else if (process.platform === "win32") {
|
||||
url = `${pre}/win/${process.arch}/${post}`
|
||||
}
|
||||
else {
|
||||
url = `${pre}/linux/${process.arch}/${post}`
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const sevenzip_exec = path.resolve(global.RUNTIME_PATH, "7z-bin", process.platform === "win32" ? "7za.exe" : "7za")
|
||||
const git_exec = path.resolve(global.RUNTIME_PATH, "git", process.platform === "win32" ? "git.exe" : "git")
|
||||
|
||||
if (!fs.existsSync(sevenzip_exec)) {
|
||||
global.win.webContents.send("initializing_text", "Downloading 7z binaries...")
|
||||
|
||||
console.log(`Downloading 7z binaries...`)
|
||||
|
||||
fs.mkdirSync(path.resolve(global.RUNTIME_PATH, "7z-bin"), { recursive: true })
|
||||
|
||||
let url = resolveDestBin(`https://storage.ragestudio.net/rstudio/binaries/7zip-bin`, process.platform === "win32" ? "7za.exe" : "7za")
|
||||
|
||||
await streamPipeline(
|
||||
got.stream(url),
|
||||
fs.createWriteStream(sevenzip_exec)
|
||||
)
|
||||
|
||||
if (os.platform() !== "win32") {
|
||||
ChildProcess.execSync("chmod +x " + sevenzip_exec)
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(git_exec) && process.platform === "win32") {
|
||||
global.win.webContents.send("initializing_text", "Downloading GIT binaries...")
|
||||
|
||||
console.log(`Downloading git binaries...`)
|
||||
|
||||
fs.mkdirSync(path.resolve(global.RUNTIME_PATH, "git"), { recursive: true })
|
||||
|
||||
let url = resolveDestBin(`https://storage.ragestudio.net/rstudio/binaries/git`, "git.7z")
|
||||
|
||||
await streamPipeline(
|
||||
got.stream(url),
|
||||
fs.createWriteStream(git_exec)
|
||||
)
|
||||
|
||||
if (os.platform() !== "win32") {
|
||||
ChildProcess.execSync("chmod +x " + git_exec)
|
||||
}
|
||||
}
|
||||
|
||||
global.SEVENZIP_PATH = sevenzip_exec
|
||||
global.GIT_PATH = git_exec
|
||||
|
||||
console.log(`7z binaries: ${sevenzip_exec}`)
|
||||
console.log(`GIT binaries: ${git_exec}`)
|
||||
}
|
||||
|
||||
export default main
|
33
src/preload/index.js
Normal file
33
src/preload/index.js
Normal file
@ -0,0 +1,33 @@
|
||||
import { contextBridge, ipcRenderer } from "electron"
|
||||
import { electronAPI } from "@electron-toolkit/preload"
|
||||
|
||||
const api = {}
|
||||
|
||||
if (process.contextIsolated) {
|
||||
try {
|
||||
contextBridge.exposeInMainWorld(
|
||||
"ipc",
|
||||
{
|
||||
exec: (channel, ...args) => {
|
||||
return ipcRenderer.invoke(channel, ...args)
|
||||
},
|
||||
send: (channel, args) => {
|
||||
ipcRenderer.send(channel, args)
|
||||
},
|
||||
on: (channel, listener) => {
|
||||
ipcRenderer.on(channel, (event, ...args) => listener(event, ...args))
|
||||
},
|
||||
off: (channel, listener) => {
|
||||
ipcRenderer.removeListener(channel, listener)
|
||||
}
|
||||
},
|
||||
)
|
||||
contextBridge.exposeInMainWorld('electron', electronAPI)
|
||||
contextBridge.exposeInMainWorld('api', api)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
} else {
|
||||
window.electron = electronAPI
|
||||
window.api = api
|
||||
}
|
16
src/renderer/index.html
Normal file
16
src/renderer/index.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>RageStudio Bundler</title>
|
||||
<!-- <meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
|
||||
/> -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
125
src/renderer/src/App.jsx
Normal file
125
src/renderer/src/App.jsx
Normal file
@ -0,0 +1,125 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
|
||||
import BarLoader from "react-spinners/BarLoader"
|
||||
|
||||
import GlobalStateContext from "contexts/global"
|
||||
|
||||
import getRootCssVar from "utils/getRootCssVar"
|
||||
|
||||
import InstallationsManager from "pages/manager"
|
||||
|
||||
import { MdFolder } from "react-icons/md"
|
||||
|
||||
globalThis.getRootCssVar = getRootCssVar
|
||||
|
||||
const PageRender = () => {
|
||||
const globalState = React.useContext(GlobalStateContext)
|
||||
|
||||
if (globalState.initializing_text && globalState.loading) {
|
||||
return <div className="app_setup">
|
||||
<BarLoader
|
||||
className="app_loader"
|
||||
color={getRootCssVar("--primary-color")}
|
||||
/>
|
||||
|
||||
<h1>Setting up...</h1>
|
||||
|
||||
<code>
|
||||
<pre>{globalState.initializing_text}</pre>
|
||||
</code>
|
||||
</div>
|
||||
}
|
||||
|
||||
return <InstallationsManager />
|
||||
}
|
||||
|
||||
class App extends React.Component {
|
||||
state = {
|
||||
loading: true,
|
||||
pkg: null,
|
||||
initializing: false,
|
||||
}
|
||||
|
||||
ipcEvents = {
|
||||
"runtime:error": (event, data) => {
|
||||
antd.message.error(data)
|
||||
},
|
||||
"runtime:info": (event, data) => {
|
||||
antd.message.info(data)
|
||||
},
|
||||
"initializing_text": (event, data) => {
|
||||
this.setState({
|
||||
initializing_text: data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount = async () => {
|
||||
for (const event in this.ipcEvents) {
|
||||
ipc.on(event, this.ipcEvents[event])
|
||||
}
|
||||
|
||||
const pkg = await ipc.exec("pkg")
|
||||
|
||||
await ipc.exec("check:setup")
|
||||
|
||||
this.setState({
|
||||
pkg: pkg,
|
||||
loading: false,
|
||||
})
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
for (const event in this.ipcEvents) {
|
||||
ipc.off(event, this.ipcEvents[event])
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, pkg } = this.state
|
||||
|
||||
return <antd.ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: getRootCssVar("--primary-color"),
|
||||
colorBgContainer: getRootCssVar("--background-color-primary"),
|
||||
colorPrimaryBg: getRootCssVar("--background-color-primary"),
|
||||
},
|
||||
algorithm: antd.theme.darkAlgorithm
|
||||
}}
|
||||
>
|
||||
<GlobalStateContext.Provider value={this.state}>
|
||||
<antd.Layout className="app_layout">
|
||||
<antd.Layout.Header className="app_header">
|
||||
<h1>RageStudio Bundler</h1>
|
||||
</antd.Layout.Header>
|
||||
|
||||
<antd.Layout.Content className="app_content">
|
||||
<PageRender />
|
||||
</antd.Layout.Content>
|
||||
|
||||
{
|
||||
!loading && <antd.Layout.Footer className="app_footer">
|
||||
<span>
|
||||
{pkg.name}
|
||||
|
||||
<antd.Tag>
|
||||
v{pkg.version}
|
||||
</antd.Tag>
|
||||
</span>
|
||||
|
||||
<antd.Button
|
||||
size="small"
|
||||
icon={<MdFolder />}
|
||||
onClick={() => ipc.send("open-runtime-path")}
|
||||
/>
|
||||
</antd.Layout.Footer>
|
||||
}
|
||||
</antd.Layout>
|
||||
</GlobalStateContext.Provider>
|
||||
</antd.ConfigProvider>
|
||||
}
|
||||
}
|
||||
|
||||
export default App
|
16
src/renderer/src/components/Versions.jsx
Normal file
16
src/renderer/src/components/Versions.jsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
function Versions() {
|
||||
const [versions] = useState(window.electron.process.versions)
|
||||
|
||||
return (
|
||||
<ul className="versions">
|
||||
<li className="electron-version">Electron v{versions.electron}</li>
|
||||
<li className="chrome-version">Chromium v{versions.chrome}</li>
|
||||
<li className="node-version">Node v{versions.node}</li>
|
||||
<li className="v8-version">V8 v{versions.v8}</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default Versions
|
8
src/renderer/src/contexts/global.js
Normal file
8
src/renderer/src/contexts/global.js
Normal file
@ -0,0 +1,8 @@
|
||||
import React from "react"
|
||||
|
||||
const GlobalStateContext = React.createContext({
|
||||
pkg: {},
|
||||
installations: [],
|
||||
})
|
||||
|
||||
export default GlobalStateContext
|
108
src/renderer/src/contexts/installations.jsx
Normal file
108
src/renderer/src/contexts/installations.jsx
Normal file
@ -0,0 +1,108 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
|
||||
export const Context = React.createContext([])
|
||||
|
||||
export class WithContext extends React.Component {
|
||||
state = {
|
||||
installations: []
|
||||
}
|
||||
|
||||
ipcEvents = {
|
||||
"new:installation": (event, data) => {
|
||||
antd.message.loading(`Installing ${data.id}`)
|
||||
|
||||
let newData = this.state.installations
|
||||
|
||||
// search if installation already exists
|
||||
const prev = this.state.installations.findIndex((item) => item.id === data.id)
|
||||
|
||||
if (prev !== -1) {
|
||||
newData[prev] = data
|
||||
} else {
|
||||
newData.push(data)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
installations: newData,
|
||||
})
|
||||
},
|
||||
"installation:status": (event, data) => {
|
||||
console.log(`INSTALLATION STATUS: ${data.id} >`, data)
|
||||
|
||||
const { id } = data
|
||||
|
||||
let newData = this.state.installations
|
||||
|
||||
const index = newData.findIndex((item) => item.id === id)
|
||||
|
||||
if (index !== -1) {
|
||||
newData[index] = {
|
||||
...newData[index],
|
||||
...data,
|
||||
}
|
||||
|
||||
this.setState({
|
||||
installations: newData
|
||||
})
|
||||
}
|
||||
},
|
||||
"installation:error": (event, data) => {
|
||||
antd.message.error(`Failed to install ${data.id}`)
|
||||
|
||||
this.ipcEvents["installation:status"](event, data)
|
||||
},
|
||||
"installation:done": (event, data) => {
|
||||
antd.message.success(`Successfully installed ${data.id}`)
|
||||
|
||||
this.ipcEvents["installation:status"](event, data)
|
||||
},
|
||||
"installation:uninstalled": (event, data) => {
|
||||
antd.message.success(`Successfully uninstalled ${data.id}`)
|
||||
|
||||
const index = this.state.installations.findIndex((item) => item.id === data.id)
|
||||
|
||||
if (index !== -1) {
|
||||
this.setState({
|
||||
installations: [
|
||||
...this.state.installations.slice(0, index),
|
||||
...this.state.installations.slice(index + 1),
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount = async () => {
|
||||
const installations = await ipc.exec("get:installations")
|
||||
|
||||
for (const event in this.ipcEvents) {
|
||||
ipc.on(event, this.ipcEvents[event])
|
||||
}
|
||||
|
||||
this.setState({
|
||||
installations: [
|
||||
...this.state.installations,
|
||||
...installations,
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
for (const event in this.ipcEvents) {
|
||||
ipc.off(event, this.ipcEvents[event])
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Context.Provider
|
||||
value={{
|
||||
installations: this.state.installations
|
||||
}}
|
||||
>
|
||||
{this.props.children}
|
||||
</Context.Provider>
|
||||
}
|
||||
}
|
||||
|
||||
export default Context
|
8
src/renderer/src/main.jsx
Normal file
8
src/renderer/src/main.jsx
Normal file
@ -0,0 +1,8 @@
|
||||
import "./style/index.less"
|
||||
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom"
|
||||
|
||||
import App from "./App"
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById("root"))
|
211
src/renderer/src/pages/manager/index.jsx
Normal file
211
src/renderer/src/pages/manager/index.jsx
Normal file
@ -0,0 +1,211 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
import classnames from "classnames"
|
||||
|
||||
import BarLoader from "react-spinners/BarLoader"
|
||||
|
||||
import { MdAdd, MdUploadFile, MdFolder, MdDelete, MdPlayArrow, MdUpdate } from "react-icons/md"
|
||||
|
||||
import { Context as InstallationsContext, WithContext } from "contexts/installations"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const NewInstallation = (props) => {
|
||||
const [manifestUrl, setManifestUrl] = React.useState("")
|
||||
|
||||
const handleInstall = (manifest) => {
|
||||
ipc.exec("bundle:install", manifest)
|
||||
.then(() => {
|
||||
props.close()
|
||||
})
|
||||
.catch((error) => {
|
||||
antd.message.error(error)
|
||||
})
|
||||
}
|
||||
|
||||
return <div className="new_installation_prompt">
|
||||
<antd.Input
|
||||
placeholder="Manifest URL"
|
||||
value={manifestUrl}
|
||||
onChange={(e) => setManifestUrl(e.target.value)}
|
||||
onPressEnter={() => handleInstall(manifestUrl)}
|
||||
/>
|
||||
|
||||
<h2>
|
||||
or
|
||||
</h2>
|
||||
|
||||
<antd.Button
|
||||
icon={<MdUploadFile />}
|
||||
disabled
|
||||
>
|
||||
Local file
|
||||
</antd.Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
const InstallationItem = (props) => {
|
||||
const { manifest } = props
|
||||
|
||||
const isLoading = manifest.status === "installing" || manifest.status === "uninstalling" || manifest.status === "updating"
|
||||
const isInstalled = manifest.status === "installed"
|
||||
const isFailed = manifest.status === "failed"
|
||||
|
||||
const onClickUpdate = () => {
|
||||
ipc.exec("bundle:update", manifest.id)
|
||||
}
|
||||
|
||||
const onClickPlay = () => {
|
||||
ipc.exec("bundle:exec", manifest.id)
|
||||
}
|
||||
|
||||
const onClickFolder = () => {
|
||||
ipc.exec("bundle:open", manifest.id)
|
||||
}
|
||||
|
||||
const onClickDelete = () => {
|
||||
ipc.exec("bundle:uninstall", manifest.id)
|
||||
}
|
||||
|
||||
return <div
|
||||
className={classnames(
|
||||
"installation_item_wrapper",
|
||||
{
|
||||
["status_visible"]: !isInstalled
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="installation_item">
|
||||
<img src={manifest.icon} className="installation_item_icon" />
|
||||
|
||||
<div className="installation_item_info">
|
||||
<h2>
|
||||
{
|
||||
manifest.pack_name
|
||||
}
|
||||
</h2>
|
||||
<p>
|
||||
{
|
||||
isLoading ? manifest.status : manifest.version ?? "N/A"
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="installation_item_actions">
|
||||
{
|
||||
isFailed && <antd.Button
|
||||
type="primary"
|
||||
>
|
||||
Retry
|
||||
</antd.Button>
|
||||
}
|
||||
|
||||
{
|
||||
isInstalled && <antd.Button
|
||||
type="primary"
|
||||
icon={<MdUpdate />}
|
||||
onClick={onClickUpdate}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isInstalled && manifest.exec_path && <antd.Button
|
||||
type="primary"
|
||||
icon={<MdPlayArrow />}
|
||||
onClick={onClickPlay}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isInstalled && <antd.Button
|
||||
type="primary"
|
||||
icon={<MdFolder />}
|
||||
onClick={onClickFolder}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isInstalled && <antd.Popconfirm
|
||||
title="Delete Installation"
|
||||
onConfirm={onClickDelete}
|
||||
>
|
||||
<antd.Button
|
||||
type="ghost"
|
||||
icon={<MdDelete />}
|
||||
/>
|
||||
</antd.Popconfirm>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="installation_status"
|
||||
>
|
||||
{
|
||||
isLoading && <BarLoader color={getRootCssVar("--primary-color")} className="app_loader" />
|
||||
}
|
||||
<p>{manifest.statusText ?? "Unknown status"}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
class InstallationsManager extends React.Component {
|
||||
static contextType = InstallationsContext
|
||||
|
||||
state = {
|
||||
drawerVisible: false,
|
||||
}
|
||||
|
||||
toggleDrawer = (to) => {
|
||||
this.setState({
|
||||
drawerVisible: to ?? !this.state.drawerVisible,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { installations } = this.context
|
||||
|
||||
const empty = installations.length == 0
|
||||
|
||||
return <div className="installations_manager">
|
||||
<antd.Button
|
||||
type="primary"
|
||||
icon={<MdAdd />}
|
||||
onClick={() => this.toggleDrawer(true)}
|
||||
>
|
||||
Add new installation
|
||||
</antd.Button>
|
||||
|
||||
<div className={empty ? "installations_list empty" : "installations_list"}>
|
||||
{
|
||||
empty && <antd.Empty description="No installations" />
|
||||
}
|
||||
|
||||
{
|
||||
installations.map((manifest) => {
|
||||
return <InstallationItem key={manifest.id} manifest={manifest} />
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<antd.Drawer
|
||||
title="Add new installation"
|
||||
placement="bottom"
|
||||
open={this.state.drawerVisible}
|
||||
onClose={() => this.toggleDrawer(false)}
|
||||
>
|
||||
<NewInstallation
|
||||
close={() => this.toggleDrawer(false)}
|
||||
/>
|
||||
</antd.Drawer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const InstallationsManagerPage = (props) => {
|
||||
return <WithContext>
|
||||
<InstallationsManager {...props} />
|
||||
</WithContext>
|
||||
}
|
||||
|
||||
export default InstallationsManagerPage
|
157
src/renderer/src/pages/manager/index.less
Normal file
157
src/renderer/src/pages/manager/index.less
Normal file
@ -0,0 +1,157 @@
|
||||
.installations_manager {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
height: 100%;
|
||||
|
||||
gap: 20px;
|
||||
|
||||
.installations_list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
background-color: var(--background-color-secondary);
|
||||
border-radius: 12px;
|
||||
|
||||
&.empty {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@installation-item-borderRadius: 12px;
|
||||
|
||||
.installation_item_wrapper {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.status_visible {
|
||||
.installation_item {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.installation_status {
|
||||
height: fit-content;
|
||||
|
||||
padding: 10px 20px;
|
||||
padding-top: calc(8px + 10px);
|
||||
|
||||
opacity: 1;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(odd) {
|
||||
.installation_item {
|
||||
background-color: var(--background-color-primary);
|
||||
}
|
||||
|
||||
.installation_status {
|
||||
background-color: var(--background-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.installation_item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
gap: 20px;
|
||||
|
||||
padding: 5px;
|
||||
|
||||
border-radius: @installation-item-borderRadius;
|
||||
|
||||
background-color: var(--background-color-primary);
|
||||
|
||||
z-index: 50;
|
||||
|
||||
.installation_item_info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
p {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.installation_item_icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
|
||||
min-width: 50px;
|
||||
min-height: 50px;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
border-radius: 12px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.installation_item_actions {
|
||||
display: flex;
|
||||
|
||||
width: 100%;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.installation_status {
|
||||
position: relative;
|
||||
|
||||
z-index: 49;
|
||||
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
|
||||
background-color: var(--background-color-primary);
|
||||
|
||||
gap: 10px;
|
||||
|
||||
width: 100%;
|
||||
|
||||
border-radius: 0 0 12px 12px;
|
||||
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
p {
|
||||
font-size: 0.7rem;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new_installation_prompt {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
gap: 20px;
|
||||
}
|
153
src/renderer/src/style/index.less
Normal file
153
src/renderer/src/style/index.less
Normal file
@ -0,0 +1,153 @@
|
||||
@import "style/reset.css";
|
||||
|
||||
@var-text-color: #fff;
|
||||
@var-background-color-primary: #424549;
|
||||
@var-background-color-secondary: #1e2124;
|
||||
@var-primary-color: #36d7b7; //#F3B61F;
|
||||
@var-border-color: #a1a2a2;
|
||||
|
||||
:root {
|
||||
--background-color-primary: @var-background-color-primary;
|
||||
--background-color-secondary: @var-background-color-secondary;
|
||||
--primary-color: @var-primary-color;
|
||||
--text-color: @var-text-color;
|
||||
--border-color: @var-border-color;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: var(--background-color-primary);
|
||||
color: var(--text-color);
|
||||
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen;
|
||||
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app_layout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-color: var(--background-color-primary);
|
||||
}
|
||||
|
||||
.app_header {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
|
||||
background-color: darken(@var-background-color-primary, 5%);
|
||||
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.app_footer {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 30px;
|
||||
|
||||
background-color: darken(@var-background-color-primary, 5%);
|
||||
|
||||
border: 1px solid @var-border-color;
|
||||
|
||||
padding: 10px 40px;
|
||||
|
||||
margin: 10px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.app_content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
padding: 20px;
|
||||
|
||||
background-color: var(--background-color-primary);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
span {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
|
||||
color: var(--text-color);
|
||||
|
||||
margin: 0;
|
||||
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
* svg {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
display: inline-flex;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
margin: 0;
|
||||
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ant-message-notice-wrapper {
|
||||
.ant-message-notice-content {
|
||||
color: var(--text-color) !important;
|
||||
background-color: var(--background-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.app_setup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 20px;
|
||||
|
||||
h1 {
|
||||
font-size: 2.3rem;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: var(--background-color-secondary);
|
||||
|
||||
padding: 20px;
|
||||
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.app_loader {
|
||||
width: 100% !important;
|
||||
}
|
48
src/renderer/src/style/reset.css
Normal file
48
src/renderer/src/style/reset.css
Normal file
@ -0,0 +1,48 @@
|
||||
/* http://meyerweb.com/eric/tools/css/reset/
|
||||
v2.0 | 20110126
|
||||
License: none (public domain)
|
||||
*/
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
6
src/renderer/src/utils/getRootCssVar/index.js
Normal file
6
src/renderer/src/utils/getRootCssVar/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
function getRootCssVar(key) {
|
||||
const root = document.querySelector(':root')
|
||||
return window.getComputedStyle(root).getPropertyValue(key)
|
||||
}
|
||||
|
||||
export default getRootCssVar
|
Loading…
x
Reference in New Issue
Block a user