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.name} -

+

+ {setting.name} +

+
+ +
+

+ {setting.description} +

+
-
-

- {setting.description} -

+
+ { + loading && + } + { + !loading && + }
-
+
{ - loading && - } - { - !loading && + !loading &&
+
+
}
diff --git a/packages/gui/src/renderer/src/pages/settings/index.less b/packages/gui/src/renderer/src/pages/settings/index.less index 7ede25b..d005eda 100644 --- a/packages/gui/src/renderer/src/pages/settings/index.less +++ b/packages/gui/src/renderer/src/pages/settings/index.less @@ -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; + } } \ No newline at end of file diff --git a/packages/gui/src/renderer/src/settings_list.jsx b/packages/gui/src/renderer/src/settings_list.jsx index 809b799..c2d70ef 100644 --- a/packages/gui/src/renderer/src/settings_list.jsx +++ b/packages/gui/src/renderer/src/settings_list.jsx @@ -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 ( - - ) - } - } - ] - }, - { - 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"} + + ) + } + } + ] + }, + { + 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 <> + + + + }, + footer: (props) => { + return {props.value} + }, + 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 + } + ] + } ]