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",
"upath": "^2.0.1",
"uuid": "^9.0.1",
"webtorrent": "^2.4.1",
"winston": "^3.13.0"
},
"devDependencies": {
"@swc/cli": "^0.3.12",
"@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_RESET from "./git_reset"
import ISM_HTTP from "./http"
import ISM_TORRENT from "./torrent"
const InstallationStepsMethods = {
git_clone: ISM_GIT_CLONE,
git_pull: ISM_GIT_PULL,
git_reset: ISM_GIT_RESET,
http_file: ISM_HTTP,
torrent: ISM_TORRENT,
}
const StepsOrders = [
"git_clones",
"git_pull",
"git_reset",
"torrent",
"http_file",
]
@ -37,7 +40,7 @@ export default async function processGenericSteps(pkg, steps, logger = Logger, a
for await (let step of steps) {
step.type = step.type.toLowerCase()
if (abortController.signal.aborted) {
if (abortController?.signal?.aborted) {
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 Logger from "./logger"
import Settings from "./classes/Settings"
import Vars from "./vars"
import DB from "./db"
@ -37,10 +38,16 @@ export default class RelicCore {
async initialize() {
globalThis.relic_core = {
tasks: [],
vars: Vars,
}
await DB.initialize()
await Settings.initialize()
if (!await Settings.get("packages_path")) {
await Settings.set("packages_path", Vars.packages_path)
}
onExit(this.onExit)
}
@ -71,11 +78,13 @@ export default class RelicCore {
lastOperationRetry: PackageLastOperationRetry,
}
openPath(pkg_id) {
async openPath(pkg_id) {
if (!pkg_id) {
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')
const { v3 } = require('uuid')
import request from "request"
import {v3} from "uuid"
let uuid
let api_url = 'https://authserver.mojang.com'

View File

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

View File

@ -8,11 +8,15 @@ import ManifestConfigManager from "../classes/ManifestConfig"
import resolveOs from "../utils/resolveOs"
import FetchLibraries from "./libraries"
import Settings from "../classes/Settings"
import Vars from "../vars"
async function BuildManifest(baseClass, context, { soft = false } = {}) {
const packagesPath = await Settings.get("packages_path") ?? Vars.packages_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
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 upath from "upath"
import resolveUserDataPath from "./utils/resolveUserDataPath"
const isWin = process.platform.includes("win")
const isMac = process.platform.includes("darwin")
const runtimeName = "rs-relic"
const userdata_path = upath.normalizeSafe(path.resolve(
process.env.APPDATA ||
(process.platform == "darwin" ? process.env.HOME + "/Library/Preferences" : process.env.HOME + "/.local/share"),
))
const userdata_path = resolveUserDataPath()
const runtime_path = upath.normalizeSafe(path.join(userdata_path, runtimeName))
const cache_path = upath.normalizeSafe(path.join(runtime_path, "cache"))
const packages_path = upath.normalizeSafe(path.join(runtime_path, "packages"))

View File

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

View File

@ -1,5 +1,6 @@
import sendToRender from "../utils/sendToRender"
import { ipcMain } from "electron"
import { ipcMain, dialog } from "electron"
import path from "node:path"
export default class CoreAdapter {
constructor(electronApp, RelicCore) {
@ -48,7 +49,7 @@ export default class CoreAdapter {
return await this.core.package.reinstall(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 } = {}) => {
// check for updates first
@ -77,6 +78,28 @@ export default class CoreAdapter {
"core:open-path": async (event, 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 = {

View File

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

View File

@ -91,31 +91,53 @@ const SettingItem = (props) => {
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
className="app_settings-list-item"
>
<div className="app_settings-list-item-info">
<div className="app_settings-list-item-label">
<Icon icon={setting.icon} />
<div className="app_settings-list-item-row">
<div className="app_settings-list-item-info">
<div className="app_settings-list-item-label">
<Icon icon={setting.icon} />
<h2>
{setting.name}
</h2>
<h2>
{setting.name}
</h2>
</div>
<div className="app_settings-list-item-description">
<p>
{setting.description}
</p>
</div>
</div>
<div className="app_settings-list-item-description">
<p>
{setting.description}
</p>
<div className="app_settings-list-item-component">
{
loading && <antd.Spin />
}
{
!loading && <Render />
}
</div>
</div>
<div className="app_settings-list-item-component">
<div className="app_settings-list-item-row">
{
loading && <antd.Spin />
}
{
!loading && <Render />
!loading && <div className="app_settings-list-item-footer">
<Footer />
</div>
}
</div>
</div>

View File

@ -73,52 +73,61 @@
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;
flex-direction: row;
align-items: center;
gap: 6px;
}
&:nth-child(odd) {
background-color: mix(#fff, @var-background-color-secondary, 5%);
}
.app_settings-list-item-description {
display: inline-flex;
flex-direction: row;
border-radius: 12px;
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;
}
font-size: 0.7rem;
}
}
.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"
export default [
{
id: "services",
name: "Services",
icon: "MdAccountTree",
children: [
{
id: "drive_auth",
name: "Google Drive",
description: "Authorize your Google Drive account to be used for bundles installation.",
icon: "SiGoogledrive",
type: "button",
storaged: false,
watchIpc: ["drive:authorized", "drive:unauthorized"],
defaultValue: async () => {
return await api.settings.get("drive_auth")
},
render: (props) => {
return (
<Button
disabled
type={props.value ? "primary" : "default"}
onClick={() => {
if (!props.value) {
message.info("Authorizing...")
{
id: "services",
name: "Services",
icon: "MdAccountTree",
children: [
{
id: "drive_auth",
name: "Google Drive",
description: "Authorize your Google Drive account to be used for bundles installation.",
icon: "SiGoogledrive",
type: "button",
storaged: false,
watchIpc: ["drive:authorized", "drive:unauthorized"],
defaultValue: async () => {
return await api.settings.get("drive_auth")
},
render: (props) => {
return (
<Button
disabled
type={props.value ? "primary" : "default"}
onClick={() => {
if (!props.value) {
message.info("Authorizing...")
return ipc.exec("drive:authorize")
}
return ipc.exec("drive:authorize")
}
return ipc.exec("drive:unauthorize")
}}
>
{props.value ? "Unauthorize" : "Authorize"}
</Button>
)
}
}
]
},
{
id: "updates",
name: "Updates",
icon: "MdUpdate",
children: [
{
id: "check_update",
name: "Check for updates",
description: "Check for updates to the app.",
icon: "MdUpdate",
type: "button",
props: {
children: "Check",
onClick: () => {
message.info("Checking for updates...")
app.checkUpdates()
}
},
storaged: false
},
{
id: "pkg_auto_update_on_execute",
name: "Packages auto update",
description: "If a update is available, automatically update the app when it is executed.",
icon: "MdUpdate",
type: "switch",
storaged: true,
defaultValue: false,
props: {
disabled: true
}
}
]
},
{
id: "other",
name: "Other",
icon: "MdSettings",
children: [
{
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
}
]
}
return ipc.exec("drive:unauthorize")
}}
>
{props.value ? "Unauthorize" : "Authorize"}
</Button>
)
}
}
]
},
{
id: "updates",
name: "Updates",
icon: "MdUpdate",
children: [
{
id: "check_update",
name: "Check for updates",
description: "Check for updates to the app.",
icon: "MdUpdate",
type: "button",
props: {
children: "Check",
onClick: () => {
message.info("Checking for updates...")
app.checkUpdates()
}
},
storaged: false
},
{
id: "pkg_auto_update_on_execute",
name: "Packages auto update",
description: "If a update is available, automatically update the app when it is executed.",
icon: "MdUpdate",
type: "switch",
storaged: true,
defaultValue: false,
props: {
disabled: true
}
}
]
},
{
id: "other",
name: "Other",
icon: "MdSettings",
children: [
{
id: "change_packages_path",
name: "Change packages path",
description: "Change the folder where all packages will be installed.",
icon: "MdFolder",
type: "button",
defaultValue: async () => {
return await ipc.exec("settings:get", "packages_path")
},
render: (props) => {
return <>
<Button
style={{
width: "100%",
}}
onClick={async () => {
const path = await ipc.exec("core:change-packages-path")
if (path) {
props.handleChange(path)
}
}}
>
Change
</Button>
<Button
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
}
]
}
]