added installation path selector

This commit is contained in:
SrGooglo 2024-06-18 14:34:54 +02:00
parent 67db27cf28
commit f1257ec7f3
17 changed files with 510 additions and 194 deletions

View File

@ -34,10 +34,11 @@
"unzipper": "^0.10.14", "unzipper": "^0.10.14",
"upath": "^2.0.1", "upath": "^2.0.1",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"webtorrent": "^2.4.1",
"winston": "^3.13.0" "winston": "^3.13.0"
}, },
"devDependencies": { "devDependencies": {
"@swc/cli": "^0.3.12", "@swc/cli": "^0.3.12",
"@swc/core": "^1.4.11" "@swc/core": "^1.4.11"
} }
} }

View File

@ -0,0 +1,57 @@
import fs from "node:fs"
import path from "node:path"
import Vars from "../vars"
const settingsPath = path.resolve(Vars.runtime_path, "settings.json")
export default class Settings {
static filePath = settingsPath
static async initialize() {
if (!fs.existsSync(settingsPath)) {
await fs.promises.writeFile(settingsPath, "{}")
}
}
static async read() {
return JSON.parse(await fs.promises.readFile(settingsPath, "utf8"))
}
static async write(data) {
await fs.promises.writeFile(settingsPath, JSON.stringify(data, null, 2))
}
static async get(key) {
const data = await this.read()
if (key) {
return data[key]
}
return data
}
static async has(key) {
const data = await this.read()
return key in data
}
static async set(key, value) {
const data = await this.read()
data[key] = value
await this.write(data)
}
static async delete(key) {
const data = await this.read()
delete data[key]
await this.write(data)
return data
}
}

View File

@ -4,18 +4,21 @@ import ISM_GIT_CLONE from "./git_clone"
import ISM_GIT_PULL from "./git_pull" import ISM_GIT_PULL from "./git_pull"
import ISM_GIT_RESET from "./git_reset" import ISM_GIT_RESET from "./git_reset"
import ISM_HTTP from "./http" import ISM_HTTP from "./http"
import ISM_TORRENT from "./torrent"
const InstallationStepsMethods = { const InstallationStepsMethods = {
git_clone: ISM_GIT_CLONE, git_clone: ISM_GIT_CLONE,
git_pull: ISM_GIT_PULL, git_pull: ISM_GIT_PULL,
git_reset: ISM_GIT_RESET, git_reset: ISM_GIT_RESET,
http_file: ISM_HTTP, http_file: ISM_HTTP,
torrent: ISM_TORRENT,
} }
const StepsOrders = [ const StepsOrders = [
"git_clones", "git_clones",
"git_pull", "git_pull",
"git_reset", "git_reset",
"torrent",
"http_file", "http_file",
] ]
@ -37,7 +40,7 @@ export default async function processGenericSteps(pkg, steps, logger = Logger, a
for await (let step of steps) { for await (let step of steps) {
step.type = step.type.toLowerCase() step.type = step.type.toLowerCase()
if (abortController.signal.aborted) { if (abortController?.signal?.aborted) {
return false return false
} }

View File

@ -0,0 +1,44 @@
import path from "node:path"
import parseStringVars from "../utils/parseStringVars"
//import downloadTorrent from "../helpers/downloadTorrent"
export default async (pkg, step, logger, abortController) => {
throw new Error("Not implemented")
if (typeof step.path === "undefined") {
step.path = `.`
}
step.path = await parseStringVars(step.path, pkg)
let _path = path.resolve(pkg.install_path, step.path)
global._relic_eventBus.emit(`pkg:update:state`, {
id: pkg.id,
status_text: `Preparing torrent...`,
})
logger.info(`Preparing torrent with magnet => [${step.magnet}]`)
if (step.tmp) {
_path = path.resolve(os.tmpdir(), String(new Date().getTime()))
}
const parentDir = path.resolve(_path, "..")
if (!fs.existsSync(parentDir)) {
fs.mkdirSync(parentDir, { recursive: true })
}
await downloadTorrent(step.magnet, _path, {
onProgress: (progress) => {
global._relic_eventBus.emit(`pkg:update:state`, {
id: pkg.id,
use_id_only: true,
status_text: `Downloaded ${progress.transferredString} / ${progress.totalString} | ${progress.speedString}/s`,
})
}
})
}

View File

@ -0,0 +1,93 @@
import fs from "node:fs"
import path from "node:path"
import cliProgress from "cli-progress"
import humanFormat from "human-format"
function convertSize(size) {
return `${humanFormat(size, {
decimals: 2,
})}B`
}
export default async function downloadTorrent(
magnet,
destination,
{
onStart,
onProgress,
onDone,
onError,
} = {}
) {
let progressInterval = null
let tickProgress = {
total: 0,
transferred: 0,
speed: 0,
totalString: "0B",
transferredString: "0B",
speedString: "0B/s",
}
const client = new WebTorrent()
await new Promise((resolve, reject) => {
client.add(magnet, (torrentInstance) => {
const progressBar = new cliProgress.SingleBar({
format: "[{bar}] {percentage}% | {total_formatted} | {speed}/s | {eta_formatted}",
barCompleteChar: "\u2588",
barIncompleteChar: "\u2591",
hideCursor: true
}, cliProgress.Presets.shades_classic)
if (typeof onStart === "function") {
onStart(torrentInstance)
}
progressBar.start(tickProgress.total, 0, {
speed: "0B/s",
total_formatted: tickProgress.totalString,
})
torrentInstance.on("done", () => {
progressBar.stop()
clearInterval(progressInterval)
if (typeof onDone === "function") {
onDone(torrentInstance)
}
resolve(torrentInstance)
})
torrentInstance.on("error", (error) => {
progressBar.stop()
clearInterval(progressInterval)
if (typeof onError === "function") {
onError(error)
} else {
reject(error)
}
})
progressInterval = setInterval(() => {
tickProgress.speed = torrentInstance.downloadSpeed
tickProgress.transferred = torrentInstance.downloaded
tickProgress.transferredString = convertSize(tickProgress.transferred)
tickProgress.totalString = convertSize(tickProgress.total)
tickProgress.speedString = convertSize(tickProgress.speed)
if (typeof onProgress === "function") {
onProgress(tickProgress)
}
progressBar.update(tickProgress.transferred, {
speed: tickProgress.speedString,
})
}, 1000)
})
})
}

View File

@ -6,6 +6,7 @@ import open from "open"
import SetupHelper from "./helpers/setup" import SetupHelper from "./helpers/setup"
import Logger from "./logger" import Logger from "./logger"
import Settings from "./classes/Settings"
import Vars from "./vars" import Vars from "./vars"
import DB from "./db" import DB from "./db"
@ -37,10 +38,16 @@ export default class RelicCore {
async initialize() { async initialize() {
globalThis.relic_core = { globalThis.relic_core = {
tasks: [], tasks: [],
vars: Vars,
} }
await DB.initialize() await DB.initialize()
await Settings.initialize()
if (!await Settings.get("packages_path")) {
await Settings.set("packages_path", Vars.packages_path)
}
onExit(this.onExit) onExit(this.onExit)
} }
@ -71,11 +78,13 @@ export default class RelicCore {
lastOperationRetry: PackageLastOperationRetry, lastOperationRetry: PackageLastOperationRetry,
} }
openPath(pkg_id) { async openPath(pkg_id) {
if (!pkg_id) { if (!pkg_id) {
return open(Vars.runtime_path) return open(Vars.runtime_path)
} }
return open(Vars.packages_path + "/" + pkg_id) const packagesPath = await Settings.get("packages_path") ?? Vars.packages_path
return open(packagesPath + "/" + pkg_id)
} }
} }

View File

@ -1,5 +1,5 @@
const request = require('request') import request from "request"
const { v3 } = require('uuid') import {v3} from "uuid"
let uuid let uuid
let api_url = 'https://authserver.mojang.com' let api_url = 'https://authserver.mojang.com'

View File

@ -1,9 +1,9 @@
const fs = require('fs') import fs from "node:fs"
const path = require('path') import path from "node:path"
const request = require('request') import child from "node:child_process"
const checksum = require('checksum') import request from "request"
const Zip = require('adm-zip') import checksum from "checksum"
const child = require('child_process') import Zip from "adm-zip"
let counter = 0 let counter = 0
export default class Handler { export default class Handler {

View File

@ -8,11 +8,15 @@ import ManifestConfigManager from "../classes/ManifestConfig"
import resolveOs from "../utils/resolveOs" import resolveOs from "../utils/resolveOs"
import FetchLibraries from "./libraries" import FetchLibraries from "./libraries"
import Settings from "../classes/Settings"
import Vars from "../vars" import Vars from "../vars"
async function BuildManifest(baseClass, context, { soft = false } = {}) { async function BuildManifest(baseClass, context, { soft = false } = {}) {
const packagesPath = await Settings.get("packages_path") ?? Vars.packages_path
// inject install_path // inject install_path
context.install_path = path.resolve(Vars.packages_path, baseClass.id) context.install_path = path.resolve(packagesPath, baseClass.id)
baseClass.install_path = context.install_path baseClass.install_path = context.install_path
if (soft === true) { if (soft === true) {

View File

@ -0,0 +1,9 @@
import path from "node:path"
import upath from "upath"
export default () => {
return upath.normalizeSafe(path.resolve(
process.env.APPDATA ||
(process.platform == "darwin" ? process.env.HOME + "/Library/Preferences" : process.env.HOME + "/.local/share"),
))
}

View File

@ -1,15 +1,13 @@
import path from "node:path" import path from "node:path"
import upath from "upath" import upath from "upath"
import resolveUserDataPath from "./utils/resolveUserDataPath"
const isWin = process.platform.includes("win") const isWin = process.platform.includes("win")
const isMac = process.platform.includes("darwin") const isMac = process.platform.includes("darwin")
const runtimeName = "rs-relic" const runtimeName = "rs-relic"
const userdata_path = upath.normalizeSafe(path.resolve( const userdata_path = resolveUserDataPath()
process.env.APPDATA ||
(process.platform == "darwin" ? process.env.HOME + "/Library/Preferences" : process.env.HOME + "/.local/share"),
))
const runtime_path = upath.normalizeSafe(path.join(userdata_path, runtimeName)) const runtime_path = upath.normalizeSafe(path.join(userdata_path, runtimeName))
const cache_path = upath.normalizeSafe(path.join(runtime_path, "cache")) const cache_path = upath.normalizeSafe(path.join(runtime_path, "cache"))
const packages_path = upath.normalizeSafe(path.join(runtime_path, "packages")) const packages_path = upath.normalizeSafe(path.join(runtime_path, "packages"))

View File

@ -52,4 +52,4 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"vite": "^4.4.9" "vite": "^4.4.9"
} }
} }

View File

@ -1,5 +1,6 @@
import sendToRender from "../utils/sendToRender" import sendToRender from "../utils/sendToRender"
import { ipcMain } from "electron" import { ipcMain, dialog } from "electron"
import path from "node:path"
export default class CoreAdapter { export default class CoreAdapter {
constructor(electronApp, RelicCore) { constructor(electronApp, RelicCore) {
@ -48,7 +49,7 @@ export default class CoreAdapter {
return await this.core.package.reinstall(pkg_id) return await this.core.package.reinstall(pkg_id)
}, },
"pkg:cancel_install": async (event, pkg_id) => { "pkg:cancel_install": async (event, pkg_id) => {
return await this.core.package.cancelInstall(pkg_id) return await this.core.package.cancelInstall(pkg_id)
}, },
"pkg:execute": async (event, pkg_id, { force = false } = {}) => { "pkg:execute": async (event, pkg_id, { force = false } = {}) => {
// check for updates first // check for updates first
@ -77,6 +78,28 @@ export default class CoreAdapter {
"core:open-path": async (event, pkg_id) => { "core:open-path": async (event, pkg_id) => {
return await this.core.openPath(pkg_id) return await this.core.openPath(pkg_id)
}, },
"core:change-packages-path": async (event) => {
const { canceled, filePaths } = await dialog.showOpenDialog(undefined, {
properties: ["openDirectory"]
})
if (canceled) {
return false
}
const targetPath = path.resolve(filePaths[0], "RelicPackages")
await global.Settings.set("packages_path", targetPath)
return targetPath
},
"core:set-default-packages-path": async (event) => {
const { packages_path } = globalThis.relic_core.vars
await global.Settings.set("packages_path", packages_path)
return packages_path
},
} }
coreEvents = { coreEvents = {

View File

@ -1,20 +1,18 @@
global.SettingsStore = new Store({
name: "settings",
watch: true,
})
import path from "node:path" import path from "node:path"
import { app, shell, BrowserWindow, ipcMain } from "electron" import { app, shell, BrowserWindow, ipcMain } from "electron"
import { electronApp, optimizer, is } from "@electron-toolkit/utils" import { electronApp, optimizer, is } from "@electron-toolkit/utils"
import isDev from "electron-is-dev" import isDev from "electron-is-dev"
import Store from "electron-store"
let RelicCore = null let RelicCore = null
let Settings = null
if (isDev) { if (isDev) {
RelicCore = require("../../../core").default RelicCore = require("../../../core/dist").default
Settings = global.Settings = require("../../../core/dist/classes/Settings").default
} else { } else {
RelicCore = require("@ragestudio/relic-core").default RelicCore = require("@ragestudio/relic-core").default
Settings = global.Settings = require("@ragestudio/relic-core/src/classes/Settings").default
} }
import CoreAdapter from "./classes/CoreAdapter" import CoreAdapter from "./classes/CoreAdapter"
@ -84,17 +82,17 @@ class ElectronApp {
autoUpdater.quitAndInstall() autoUpdater.quitAndInstall()
}, 3000) }, 3000)
}, },
"settings:get": (event, key) => { "settings:get": async (event, key) => {
return global.SettingsStore.get(key) return await Settings.get(key)
}, },
"settings:set": (event, key, value) => { "settings:set": async (event, key, value) => {
return global.SettingsStore.set(key, value) return await Settings.set(key, value)
}, },
"settings:delete": (event, key) => { "settings:delete": async (event, key) => {
return global.SettingsStore.delete(key) return await Settings.delete(key)
}, },
"settings:has": (event, key) => { "settings:has": async (event, key) => {
return global.SettingsStore.has(key) return await Settings.has(key)
}, },
"app:open-logs": async (event) => { "app:open-logs": async (event) => {
const loggerWindow = await this.logsViewer.createWindow() const loggerWindow = await this.logsViewer.createWindow()

View File

@ -91,31 +91,53 @@ const SettingItem = (props) => {
return React.createElement(Component, componentProps) return React.createElement(Component, componentProps)
} }
const Footer = () => {
if (typeof setting.footer === "undefined") {
return null
}
if (typeof setting.footer === "function") {
return setting.footer(componentProps)
}
return <span style={{ fontSize: "0.5rem" }}>{setting.footer}</span>
}
return <div return <div
className="app_settings-list-item" className="app_settings-list-item"
> >
<div className="app_settings-list-item-info"> <div className="app_settings-list-item-row">
<div className="app_settings-list-item-label"> <div className="app_settings-list-item-info">
<Icon icon={setting.icon} /> <div className="app_settings-list-item-label">
<Icon icon={setting.icon} />
<h2> <h2>
{setting.name} {setting.name}
</h2> </h2>
</div>
<div className="app_settings-list-item-description">
<p>
{setting.description}
</p>
</div>
</div> </div>
<div className="app_settings-list-item-description"> <div className="app_settings-list-item-component">
<p> {
{setting.description} loading && <antd.Spin />
</p> }
{
!loading && <Render />
}
</div> </div>
</div> </div>
<div className="app_settings-list-item-component"> <div className="app_settings-list-item-row">
{ {
loading && <antd.Spin /> !loading && <div className="app_settings-list-item-footer">
} <Footer />
{ </div>
!loading && <Render />
} }
</div> </div>
</div> </div>

View File

@ -73,52 +73,61 @@
gap: 7px; gap: 7px;
} }
} }
}
}
.app_settings-list-item { .app_settings-list-item {
display: flex;
flex-direction: column;
align-items: center;
&:nth-child(odd) {
background-color: mix(#fff, @var-background-color-secondary, 5%);
}
border-radius: 12px;
padding: 10px;
opacity: 0.9;
.app_settings-list-item-row {
display: flex;
flex-direction: row;
width: 100%;
}
.app_settings-list-item-info {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
.app_settings-list-item-label {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; gap: 6px;
}
&:nth-child(odd) { .app_settings-list-item-description {
background-color: mix(#fff, @var-background-color-secondary, 5%); display: inline-flex;
} flex-direction: row;
border-radius: 12px; font-size: 0.7rem;
padding: 10px;
opacity: 0.9;
.app_settings-list-item-info {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
.app_settings-list-item-label {
display: flex;
flex-direction: row;
gap: 6px;
}
.app_settings-list-item-description {
display: inline-flex;
flex-direction: row;
font-size: 0.7rem;
}
}
.app_settings-list-item-component {
display: flex;
flex-direction: column;
justify-content: flex-end;
}
} }
} }
.app_settings-list-item-component {
display: flex;
flex-direction: column;
justify-content: flex-end;
}
} }

View File

@ -1,111 +1,157 @@
import React from "react"
import { Button } from "antd" import { Button } from "antd"
export default [ export default [
{ {
id: "services", id: "services",
name: "Services", name: "Services",
icon: "MdAccountTree", icon: "MdAccountTree",
children: [ children: [
{ {
id: "drive_auth", id: "drive_auth",
name: "Google Drive", name: "Google Drive",
description: "Authorize your Google Drive account to be used for bundles installation.", description: "Authorize your Google Drive account to be used for bundles installation.",
icon: "SiGoogledrive", icon: "SiGoogledrive",
type: "button", type: "button",
storaged: false, storaged: false,
watchIpc: ["drive:authorized", "drive:unauthorized"], watchIpc: ["drive:authorized", "drive:unauthorized"],
defaultValue: async () => { defaultValue: async () => {
return await api.settings.get("drive_auth") return await api.settings.get("drive_auth")
}, },
render: (props) => { render: (props) => {
return ( return (
<Button <Button
disabled disabled
type={props.value ? "primary" : "default"} type={props.value ? "primary" : "default"}
onClick={() => { onClick={() => {
if (!props.value) { if (!props.value) {
message.info("Authorizing...") message.info("Authorizing...")
return ipc.exec("drive:authorize") return ipc.exec("drive:authorize")
} }
return ipc.exec("drive:unauthorize") return ipc.exec("drive:unauthorize")
}} }}
> >
{props.value ? "Unauthorize" : "Authorize"} {props.value ? "Unauthorize" : "Authorize"}
</Button> </Button>
) )
} }
} }
] ]
}, },
{ {
id: "updates", id: "updates",
name: "Updates", name: "Updates",
icon: "MdUpdate", icon: "MdUpdate",
children: [ children: [
{ {
id: "check_update", id: "check_update",
name: "Check for updates", name: "Check for updates",
description: "Check for updates to the app.", description: "Check for updates to the app.",
icon: "MdUpdate", icon: "MdUpdate",
type: "button", type: "button",
props: { props: {
children: "Check", children: "Check",
onClick: () => { onClick: () => {
message.info("Checking for updates...") message.info("Checking for updates...")
app.checkUpdates() app.checkUpdates()
} }
}, },
storaged: false storaged: false
}, },
{ {
id: "pkg_auto_update_on_execute", id: "pkg_auto_update_on_execute",
name: "Packages auto update", name: "Packages auto update",
description: "If a update is available, automatically update the app when it is executed.", description: "If a update is available, automatically update the app when it is executed.",
icon: "MdUpdate", icon: "MdUpdate",
type: "switch", type: "switch",
storaged: true, storaged: true,
defaultValue: false, defaultValue: false,
props: { props: {
disabled: true disabled: true
} }
} }
] ]
}, },
{ {
id: "other", id: "other",
name: "Other", name: "Other",
icon: "MdSettings", icon: "MdSettings",
children: [ children: [
{ {
id: "open_settings_path", id: "change_packages_path",
name: "Open settings path", name: "Change packages path",
description: "Open the folder where all packages are stored.", description: "Change the folder where all packages will be installed.",
icon: "MdFolder", icon: "MdFolder",
type: "button", type: "button",
props: { defaultValue: async () => {
children: "Open", return await ipc.exec("settings:get", "packages_path")
onClick: () => { },
ipc.exec("core:open-path") render: (props) => {
} return <>
}, <Button
storaged: false style={{
}, width: "100%",
{ }}
id: "open_dev_logs", onClick={async () => {
name: "Open internal logs", const path = await ipc.exec("core:change-packages-path")
description: "Open the internal logs of the app.",
icon: "MdTerminal", if (path) {
type: "button", props.handleChange(path)
props: { }
children: "Open", }}
onClick: () => { >
ipc.exec("app:open-logs") Change
} </Button>
}, <Button
storaged: false type="link"
} size="small"
] onClick={async () => {
} const path = await ipc.exec("core:set-default-packages-path")
if (path) {
props.handleChange(path)
}
}}
>
Reset
</Button>
</>
},
footer: (props) => {
return <span style={{ fontSize: "0.6rem" }}>{props.value}</span>
},
storaged: false
},
{
id: "open_settings_path",
name: "Open settings path",
description: "Open the folder where all packages are stored.",
icon: "MdFolder",
type: "button",
props: {
children: "Open",
onClick: () => {
ipc.exec("core:open-path")
}
},
storaged: false
},
{
id: "open_dev_logs",
name: "Open internal logs",
description: "Open the internal logs of the app.",
icon: "MdTerminal",
type: "button",
props: {
children: "Open",
onClick: () => {
ipc.exec("app:open-logs")
}
},
storaged: false
}
]
}
] ]