diff --git a/packages/core/package.json b/packages/core/package.json index 8ed14e6..9931ca7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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" } -} +} \ No newline at end of file diff --git a/packages/core/src/classes/Settings.js b/packages/core/src/classes/Settings.js new file mode 100644 index 0000000..9acc16d --- /dev/null +++ b/packages/core/src/classes/Settings.js @@ -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 + } +} \ No newline at end of file diff --git a/packages/core/src/generic_steps/index.js b/packages/core/src/generic_steps/index.js index 76a0c82..ab1d9f8 100644 --- a/packages/core/src/generic_steps/index.js +++ b/packages/core/src/generic_steps/index.js @@ -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 } diff --git a/packages/core/src/generic_steps/torrent.js b/packages/core/src/generic_steps/torrent.js new file mode 100644 index 0000000..32666df --- /dev/null +++ b/packages/core/src/generic_steps/torrent.js @@ -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`, + }) + } + }) + +} \ No newline at end of file diff --git a/packages/core/src/helpers/downloadTorrent.js b/packages/core/src/helpers/downloadTorrent.js new file mode 100644 index 0000000..6f10070 --- /dev/null +++ b/packages/core/src/helpers/downloadTorrent.js @@ -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) + }) + }) +} \ No newline at end of file diff --git a/packages/core/src/index.js b/packages/core/src/index.js index f8bb2de..4f1dec3 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -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) } } \ No newline at end of file diff --git a/packages/core/src/manifest/libs/mcl/authenticator.js b/packages/core/src/manifest/libs/mcl/authenticator.js index c5973d8..12c6a95 100644 --- a/packages/core/src/manifest/libs/mcl/authenticator.js +++ b/packages/core/src/manifest/libs/mcl/authenticator.js @@ -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' diff --git a/packages/core/src/manifest/libs/mcl/handler.js b/packages/core/src/manifest/libs/mcl/handler.js index ca0a477..4ee17e8 100644 --- a/packages/core/src/manifest/libs/mcl/handler.js +++ b/packages/core/src/manifest/libs/mcl/handler.js @@ -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 { diff --git a/packages/core/src/manifest/vm.js b/packages/core/src/manifest/vm.js index 65bcbe3..bcf42e3 100644 --- a/packages/core/src/manifest/vm.js +++ b/packages/core/src/manifest/vm.js @@ -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) { diff --git a/packages/core/src/utils/resolveUserDataPath.js b/packages/core/src/utils/resolveUserDataPath.js new file mode 100644 index 0000000..9d36342 --- /dev/null +++ b/packages/core/src/utils/resolveUserDataPath.js @@ -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"), + )) +} \ No newline at end of file diff --git a/packages/core/src/vars.js b/packages/core/src/vars.js index 40dd654..117a441 100644 --- a/packages/core/src/vars.js +++ b/packages/core/src/vars.js @@ -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")) diff --git a/packages/gui/package.json b/packages/gui/package.json index 3a7b522..3512b2b 100644 --- a/packages/gui/package.json +++ b/packages/gui/package.json @@ -52,4 +52,4 @@ "react-dom": "^18.2.0", "vite": "^4.4.9" } -} +} \ No newline at end of file diff --git a/packages/gui/src/main/classes/CoreAdapter.js b/packages/gui/src/main/classes/CoreAdapter.js index 29fb206..bc1cc28 100644 --- a/packages/gui/src/main/classes/CoreAdapter.js +++ b/packages/gui/src/main/classes/CoreAdapter.js @@ -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 = { diff --git a/packages/gui/src/main/index.js b/packages/gui/src/main/index.js index 7af50cb..2f4e06d 100644 --- a/packages/gui/src/main/index.js +++ b/packages/gui/src/main/index.js @@ -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() diff --git a/packages/gui/src/renderer/src/pages/settings/index.jsx b/packages/gui/src/renderer/src/pages/settings/index.jsx index 9b9c294..de3dd98 100644 --- a/packages/gui/src/renderer/src/pages/settings/index.jsx +++ b/packages/gui/src/renderer/src/pages/settings/index.jsx @@ -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 {setting.footer} + } + return
+ {setting.description} +
+- {setting.description} -
+