From 9c055e0d7198a21b8bd006e5ee33e4b982ec12bf Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Tue, 19 Dec 2023 13:21:19 +0100 Subject: [PATCH] Added olympus manager --- .gitignore | 6 +- client/configurator.js | 84 +++++++-- client/package.json | 7 +- manager/.vscode/launch.json | 13 ++ manager/app.js | 33 ++++ manager/icons/check-solid.svg | 1 + manager/icons/circle-info-solid.svg | 1 + manager/icons/folder-open-solid.svg | 1 + manager/icons/plus-solid.svg | 1 + manager/icons/trash-can-regular.svg | 1 + manager/index.html | 27 +++ manager/instanceDiv.ejs | 51 ++++++ manager/package.json | 18 ++ manager/preload.js | 253 ++++++++++++++++++++++++++++ manager/style.css | 226 +++++++++++++++++++++++++ 15 files changed, 702 insertions(+), 21 deletions(-) create mode 100644 manager/.vscode/launch.json create mode 100644 manager/app.js create mode 100644 manager/icons/check-solid.svg create mode 100644 manager/icons/circle-info-solid.svg create mode 100644 manager/icons/folder-open-solid.svg create mode 100644 manager/icons/plus-solid.svg create mode 100644 manager/icons/trash-can-regular.svg create mode 100644 manager/index.html create mode 100644 manager/instanceDiv.ejs create mode 100644 manager/package.json create mode 100644 manager/preload.js create mode 100644 manager/style.css diff --git a/.gitignore b/.gitignore index 45a81dc0..8a0e4af3 100644 --- a/.gitignore +++ b/.gitignore @@ -23,5 +23,7 @@ client/public/javascripts/L.Path.Drag.js /installer/archive/Scripts /installer/archive/Mods /installer/installer/DCSOlympus*.exe -client/public/javascripts/L.Path.Drag.js -client/public/stylesheets/leaflet/leaflet-gesture-handling.css + +L.Path.Drag.js +leaflet-gesture-handling.css +package-lock.json diff --git a/client/configurator.js b/client/configurator.js index 0a954e8c..540ac0dd 100644 --- a/client/configurator.js +++ b/client/configurator.js @@ -3,7 +3,11 @@ const path = require('path') const yargs = require('yargs'); const prompt = require('prompt-sync')({sigint: true}); const sha256 = require('sha256'); -const jsonPath = path.join('..', 'olympus.json'); +var jsonPath = path.join('..', 'olympus.json'); +var regedit = require('regedit') + +const shellFoldersKey = 'HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders' +const saveGamesKey = '{4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4}' /* Set the acceptable values */ yargs.alias('a', 'address').describe('a', 'Backend address').string('a'); @@ -12,6 +16,7 @@ yargs.alias('c', 'clientPort').describe('c', 'Client port').number('c'); yargs.alias('p', 'gameMasterPassword').describe('p', 'Game Master password').string('p'); yargs.alias('bp', 'blueCommanderPassword').describe('bp', 'Blue Commander password').string('bp'); yargs.alias('rp', 'redCommanderPassword').describe('rp', 'Red Commander password').string('rp'); +yargs.alias('d', 'directory').describe('d', 'Directory where the DCS Olympus configurator is located').string('rp'); args = yargs.argv; async function run() { @@ -30,21 +35,6 @@ async function run() { if (args.address === undefined && args.clientPort === undefined && args.backendPort === undefined && args.gameMasterPassword === undefined && args.blueCommanderPassword === undefined && args.redCommanderPassword === undefined) { - console.log('\x1b[36m%s\x1b[0m', "*********************************************************************"); - console.log('\x1b[36m%s\x1b[0m', "* _____ _____ _____ ____ _ *"); - console.log('\x1b[36m%s\x1b[0m', "* | __ \\ / ____|/ ____| / __ \\| | *"); - console.log('\x1b[36m%s\x1b[0m', "* | | | | | | (___ | | | | |_ _ _ __ ___ _ __ _ _ ___ *"); - console.log('\x1b[36m%s\x1b[0m', "* | | | | | \\___ \\ | | | | | | | | '_ ` _ \\| '_ \\| | | / __| *"); - console.log('\x1b[36m%s\x1b[0m', "* | |__| | |____ ____) | | |__| | | |_| | | | | | | |_) | |_| \\__ \\ *"); - console.log('\x1b[36m%s\x1b[0m', "* |_____/ \\_____|_____/ \\____/|_|\\__, |_| |_| |_| .__/ \\__,_|___/ *"); - console.log('\x1b[36m%s\x1b[0m', "* __/ | | | *"); - console.log('\x1b[36m%s\x1b[0m', "* |___/ |_| *"); - console.log('\x1b[36m%s\x1b[0m', "*********************************************************************"); - console.log('\x1b[36m%s\x1b[0m', ""); - - console.log("DCS Olympus configurator {{OLYMPUS_VERSION_NUMBER}}.{{OLYMPUS_COMMIT_HASH}}"); - console.log(""); - var newValue; var result; @@ -113,5 +103,65 @@ async function run() { await new Promise(resolve => setTimeout(resolve, 3000)); } +console.log('\x1b[36m%s\x1b[0m', "*********************************************************************"); +console.log('\x1b[36m%s\x1b[0m', "* _____ _____ _____ ____ _ *"); +console.log('\x1b[36m%s\x1b[0m', "* | __ \\ / ____|/ ____| / __ \\| | *"); +console.log('\x1b[36m%s\x1b[0m', "* | | | | | | (___ | | | | |_ _ _ __ ___ _ __ _ _ ___ *"); +console.log('\x1b[36m%s\x1b[0m', "* | | | | | \\___ \\ | | | | | | | | '_ ` _ \\| '_ \\| | | / __| *"); +console.log('\x1b[36m%s\x1b[0m', "* | |__| | |____ ____) | | |__| | | |_| | | | | | | |_) | |_| \\__ \\ *"); +console.log('\x1b[36m%s\x1b[0m', "* |_____/ \\_____|_____/ \\____/|_|\\__, |_| |_| |_| .__/ \\__,_|___/ *"); +console.log('\x1b[36m%s\x1b[0m', "* __/ | | | *"); +console.log('\x1b[36m%s\x1b[0m', "* |___/ |_| *"); +console.log('\x1b[36m%s\x1b[0m', "*********************************************************************"); +console.log('\x1b[36m%s\x1b[0m', ""); + +console.log("DCS Olympus configurator {{OLYMPUS_VERSION_NUMBER}}.{{OLYMPUS_COMMIT_HASH}}"); +console.log(""); + /* Run the configurator */ -run(); \ No newline at end of file +if (args.directory) { + jsonPath = path.join(args.directory, "olympus.json"); +} +else { + /* Automatically detect possible DCS installation folders */ + regedit.list(shellFoldersKey, function(err, result) { + if (err) { + console.log(err); + } + else { + if (result[shellFoldersKey] !== undefined && result[shellFoldersKey]["exists"] && result[shellFoldersKey]['values'][saveGamesKey] !== undefined && result[shellFoldersKey]['values'][saveGamesKey]['value'] !== undefined) + { + const searchpath = result[shellFoldersKey]['values'][saveGamesKey]['value']; + const folders = fs.readdirSync(searchpath); + var options = []; + folders.forEach((folder) => { + if (fs.existsSync(path.join(searchpath, folder, "Logs", "dcs.log"))) { + options.push(folder); + } + }) + console.log("The following DCS Saved Games folders have been automatically detected.") + options.forEach((folder, index) => { + console.log(`(${index + 1}) ${folder}`) + }); + while (true) { + var newValue = prompt(`Please choose a folder onto which the configurator shall operate by typing the associated number: `) + result = Number(newValue); + + if (!isNaN(result) && Number.isInteger(result) && result > 0 && result <= options.length) { + jsonPath = path.join(searchpath, options[result - 1], "Config", "olympus.json"); + break; + } + else { + console.log(`Please type a number between 1 and ${options.length}`); + } + } + + } else { + console.error("An error occured while trying to fetch the location of the DCS folder. Please type the folder location manually.") + jsonPath = path.join(prompt(`DCS Saved Games folder location: `), "olympus.json"); + } + console.log(`Configurator will run on ${jsonPath}, if this is incorrect please restart the configurator`) + run(); + } + }) +} diff --git a/client/package.json b/client/package.json index 69866012..361af86f 100644 --- a/client/package.json +++ b/client/package.json @@ -14,15 +14,18 @@ "watch": "watchify .\\src\\index.ts --debug -o .\\public\\javascripts\\bundle.js -t [ babelify --global true --presets [ @babel/preset-env ] --extensions '.js'] -p [ tsify --noImplicitAny ]" }, "dependencies": { + "appjs": "^0.0.20", "body-parser": "^1.20.2", "debug": "~2.6.9", "ejs": "^3.1.8", + "electron": "^28.0.0", "express": "~4.16.1", "express-basic-auth": "^1.2.1", "http-proxy-middleware": "^2.0.6", "js-sha256": "^0.10.1", "morgan": "~1.9.1", "prompt-sync": "^4.2.0", + "regedit": "^5.1.2", "save": "^2.9.0", "sha256": "^0.2.0", "srtm-elevation": "^2.1.2", @@ -31,10 +34,9 @@ "yargs": "^17.7.2" }, "devDependencies": { - "leaflet-gesture-handling": "^1.2.2", - "@turf/turf": "^6.5.0", "@babel/preset-env": "^7.21.4", "@tanem/svg-injector": "^10.1.68", + "@turf/turf": "^6.5.0", "@types/formatcoords": "^1.1.0", "@types/geojson": "^7946.0.10", "@types/leaflet": "^1.9.0", @@ -50,6 +52,7 @@ "geodesy": "^1.1.2", "leaflet": "^1.9.3", "leaflet-control-mini-map": "^0.4.0", + "leaflet-gesture-handling": "^1.2.2", "leaflet-path-drag": "*", "leaflet.nauticscale": "^1.1.0", "nodemon": "^2.0.20", diff --git a/manager/.vscode/launch.json b/manager/.vscode/launch.json new file mode 100644 index 00000000..064d058c --- /dev/null +++ b/manager/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "runtimeExecutable": "npm", + "runtimeArgs": [ "start" ], + "port": 9229 + } + ] +} \ No newline at end of file diff --git a/manager/app.js b/manager/app.js new file mode 100644 index 00000000..c6db5299 --- /dev/null +++ b/manager/app.js @@ -0,0 +1,33 @@ +const { app, BrowserWindow } = require('electron/main') +const path = require('node:path') + +function createWindow() { + const win = new BrowserWindow({ + width: 1310, + height: 800, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + nodeIntegration: true, // like here + }, + icon: "./../img/olympus.ico" + }) + + win.loadFile('index.html'); + win.setMenuBarVisibility(false); +} + +app.whenReady().then(() => { + createWindow() + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } + }) +}) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) \ No newline at end of file diff --git a/manager/icons/check-solid.svg b/manager/icons/check-solid.svg new file mode 100644 index 00000000..b22c3099 --- /dev/null +++ b/manager/icons/check-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/manager/icons/circle-info-solid.svg b/manager/icons/circle-info-solid.svg new file mode 100644 index 00000000..589da70e --- /dev/null +++ b/manager/icons/circle-info-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/manager/icons/folder-open-solid.svg b/manager/icons/folder-open-solid.svg new file mode 100644 index 00000000..847b3122 --- /dev/null +++ b/manager/icons/folder-open-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/manager/icons/plus-solid.svg b/manager/icons/plus-solid.svg new file mode 100644 index 00000000..4d70bcf5 --- /dev/null +++ b/manager/icons/plus-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/manager/icons/trash-can-regular.svg b/manager/icons/trash-can-regular.svg new file mode 100644 index 00000000..641a8a31 --- /dev/null +++ b/manager/icons/trash-can-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/manager/index.html b/manager/index.html new file mode 100644 index 00000000..70c56dc2 --- /dev/null +++ b/manager/index.html @@ -0,0 +1,27 @@ + + + + + + + + + + DCS Olympus Manager v{{OLYMPUS_VERSION_NUMBER}} + + + +
+ +
+ + + \ No newline at end of file diff --git a/manager/instanceDiv.ejs b/manager/instanceDiv.ejs new file mode 100644 index 00000000..a0e7f588 --- /dev/null +++ b/manager/instanceDiv.ejs @@ -0,0 +1,51 @@ +
+
+
+ <%= folder %> +
+
+ + + + + + + + + + + + + +
+
Client port
+ " min="1023" max="65535" <%= !installed? "disabled": "" %> tabindex="<%= index %>"> + +
+
Game Master password
+ tabindex="<%= index + 3 %>"> + +
+
Backend port
+ " min="1023" max="65535" <%= !installed? "disabled": "" %> tabindex="<%= index + 1 %>"> + +
+
Blue Commander password
+ tabindex="<%= index + 4 %>"> + +
+
Backend address
+ " min="1023" max="65535" <%= !installed? "disabled": "" %> tabindex="<%= index + 2 %>"> + +
+
Red Commander password
+ tabindex="<%= index + 5 %>"> + +
+
+ + + +
+
+
\ No newline at end of file diff --git a/manager/package.json b/manager/package.json new file mode 100644 index 00000000..462f269a --- /dev/null +++ b/manager/package.json @@ -0,0 +1,18 @@ +{ + "name": "dcsolympus_manager", + "version": "1.0.0", + "description": "", + "main": "app.js", + "scripts": { + "start": "electron ." + }, + "author": "", + "license": "ISC", + "dependencies": { + "ejs": "^3.1.9", + "electron": "^28.0.0", + "portfinder": "^1.0.32", + "regedit": "^5.1.2", + "sha256": "^0.2.0" + } +} diff --git a/manager/preload.js b/manager/preload.js new file mode 100644 index 00000000..7873b573 --- /dev/null +++ b/manager/preload.js @@ -0,0 +1,253 @@ +var regedit = require('regedit') +var fs = require('fs') +var path = require('path') +const ejs = require('ejs') +const portfinder = require('portfinder') +const sha256 = require('sha256') + +const shellFoldersKey = 'HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders' +const saveGamesKey = '{4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4}' + +var instanceDivs = []; + +function checkPort(port, callback) { + portfinder.getPort({port: port, stopPort: port}, (err, res) => { + if (err !== null) { + console.error(`Port ${port} already in use`); + callback(false); + } else { + callback(true); + } + }); +} + +function installOlympus(folder) { + console.log(`Installing Olympus in ${folder}`); + fs.cpSync(path.join("..", "Mod"), path.join(folder, "Mods", "Services", "Olympus"), {recursive: true}); + fs.cpSync(path.join("..", "Scripts", "OlympusHook.lua"), path.join(folder, "Scripts", "OlympusHook.lua"), {recursive: true}); + fs.cpSync(path.join("..", "olympus.json"), path.join(folder, "Config", "olympus.json"), {recursive: true}); + loadDivs(); +} + +function uninstallOlympus(folder) { + console.log(`Uninstalling Olympus from ${folder}`); + fs.rmSync(path.join(folder, "Mods", "Services", "Olympus"), {recursive: true}); + fs.rmSync(path.join(folder, "Config", "olympus.json"), {recursive: true}); + loadDivs(); +} + +function applyConfiguration(folder, data) { + console.log(`Applying configuration to Olympus from ${folder}`); + + if (fs.existsSync(path.join(folder, "Config", "olympus.json"))) { + var config = JSON.parse(fs.readFileSync(path.join(folder, "Config", "olympus.json"))); + + config["client"]["port"] = data["clientPort"]; + config["server"]["port"] = data["backendPort"]; + config["server"]["address"] = data["backendAddress"]; + config["authentication"]["gameMasterPassword"] = sha256(data["gameMasterPassword"]); + config["authentication"]["blueCommanderPassword"] = sha256(data["blueCommanderPassword"]); + config["authentication"]["redCommanderPassword"] = sha256(data["redCommanderPassword"]); + + fs.writeFileSync(path.join(folder, "Config", "olympus.json"), JSON.stringify(config, null, 4)); + } +} + +class InstanceDiv { + element = null; + parent = null; + folder = ""; + + constructor(parent, folder) { + this.element = parent; + this.folder = folder; + this.render(); + } + + render() { + this.element = document.createElement("div"); + + var data = { + folder: this.folder, + installed: false, + index: instanceDivs.length * 9 + }; + + + if (fs.existsSync(path.join(this.folder, "Config", "olympus.json"))) { + var config = JSON.parse(fs.readFileSync(path.join(this.folder, "Config", "olympus.json"))); + data = { + ...data, + ...config + } + data["installed"] = true; + } + + ejs.renderFile("./instanceDiv.ejs", data, {}, (err, str) => { + this.element.innerHTML = str; + this.element.querySelector(".add").addEventListener("click", (e) => { + if (!e.srcElement.classList.contains("disabled")) + installOlympus(this.folder); + }); + + this.element.querySelector(".remove").addEventListener("click", (e) => { + if (!e.srcElement.classList.contains("disabled")) + uninstallOlympus(this.folder); + }); + + this.element.querySelector(".apply").addEventListener("click", (e) => { + if (!e.srcElement.classList.contains("disabled")) + applyConfiguration(this.folder, this.getFields()); + }); + + var inputs = this.element.querySelectorAll("input"); + for (let i = 0; i < inputs.length; i++) { + inputs[i].addEventListener("change", () => { + inputs[i].classList.remove("error"); + instanceDivs.forEach((instanceDiv) => instanceDiv.checkFields()) + }) + } + }); + } + + getDiv() { + return this.element; + } + + getFields() { + return { + clientPort: Number(this.element.querySelector("#client-port").value), + backendPort: Number(this.element.querySelector("#backend-port").value), + backendAddress: this.element.querySelector("#backend-address").value, + gameMasterPassword: this.element.querySelector("#game-master-password").value, + blueCommanderPassword: this.element.querySelector("#blue-commander-password").value, + redCommanderPassword: this.element.querySelector("#red-commander-password").value, + } + } + + checkFields() { + var data = this.getFields(); + + /* Clear existing errors */ + var inputs = this.element.querySelectorAll("input"); + for (let i = 0; i < inputs.length; i++) { + inputs[i].classList.remove("error"); + } + var messages = this.element.querySelectorAll(".error"); + for (let i = 0; i < messages.length; i++) { + messages[i].innerText = ""; + } + + /* Enable the button */ + this.element.querySelector(".apply").classList.remove("disabled"); + + if (data["clientPort"] !== 0 && data["backendPort"] !== 0) { + if (data["clientPort"] === data["backendPort"]) { + this.element.querySelector("#client-port").classList.add("error"); + this.element.querySelector("#client-port-error").innerText = "Ports must be different"; + this.element.querySelector("#backend-port").classList.add("error"); + this.element.querySelector("#backend-port-error").innerText = "Ports must be different"; + this.element.querySelector(".apply").classList.add("disabled"); + } + else { + checkPort(data["clientPort"], (res) => { + var otherInstanceUsesPort = instanceDivs.find((instanceDiv) => { + if (instanceDiv != this) { + var fields = instanceDiv.getFields(); + if (fields["clientPort"] === data["clientPort"] || fields["backendPort"] === data["clientPort"]) { + return true; + } + } + }) + + if (!res || otherInstanceUsesPort) { + this.element.querySelector("#client-port").classList.add("error"); + this.element.querySelector("#client-port-error").innerText = "Port already in use"; + this.element.querySelector(".apply").classList.add("disabled"); + } + }); + + checkPort(data["backendPort"], (res) => { + var otherInstanceUsesPort = instanceDivs.find((instanceDiv) => { + if (instanceDiv != this) { + var fields = instanceDiv.getFields(); + if (fields["clientPort"] === data["backendPort"] || fields["backendPort"] === data["backendPort"]) { + return true; + } + } + }) + + if (!res || otherInstanceUsesPort) { + this.element.querySelector("#backend-port").classList.add("error"); + this.element.querySelector("#backend-port-error").innerText = "Port already in use"; + this.element.querySelector(".apply").classList.add("disabled"); + } + }); + } + } + + if (data["gameMasterPassword"] !== "" && data["blueCommanderPassword"] !== "" && data["gameMasterPassword"] === data["blueCommanderPassword"]) { + this.element.querySelector("#game-master-password").classList.add("error"); + this.element.querySelector("#game-master-password-error").innerText = "Passwords must be different"; + this.element.querySelector("#blue-commander-password").classList.add("error"); + this.element.querySelector("#blue-commander-password-error").innerText = "Passwords must be different"; + this.element.querySelector(".apply").classList.add("disabled"); + } + + if (data["gameMasterPassword"] !== "" && data["redCommanderPassword"] !== "" && data["gameMasterPassword"] === data["redCommanderPassword"]) { + this.element.querySelector("#game-master-password").classList.add("error"); + this.element.querySelector("#game-master-password-error").innerText = "Passwords must be different"; + this.element.querySelector("#red-commander-password").classList.add("error"); + this.element.querySelector("#red-commander-password-error").innerText = "Passwords must be different"; + this.element.querySelector(".apply").classList.add("disabled"); + } + + if (data["blueCommanderPassword"] !== "" && data["redCommanderPassword"] !== "" && data["blueCommanderPassword"] === data["redCommanderPassword"]) { + this.element.querySelector("#blue-commander-password").classList.add("error"); + this.element.querySelector("#blue-commander-password-error").innerText = "Passwords must be different"; + this.element.querySelector("#red-commander-password").classList.add("error"); + this.element.querySelector("#red-commander-password-error").innerText = "Passwords must be different"; + this.element.querySelector(".apply").classList.add("disabled"); + } + + if (data["gameMasterPassword"] === "" || data["blueCommanderPassword"] === "" || data["redCommanderPassword"] === "") { + this.element.querySelector(".apply").classList.add("disabled"); + } + } +} + +function loadDivs() { + regedit.list(shellFoldersKey, function(err, result) { + if (err) { + console.log(err); + } + else { + if (result[shellFoldersKey] !== undefined && result[shellFoldersKey]["exists"] && result[shellFoldersKey]['values'][saveGamesKey] !== undefined && result[shellFoldersKey]['values'][saveGamesKey]['value'] !== undefined) + { + const searchpath = result[shellFoldersKey]['values'][saveGamesKey]['value']; + const folders = fs.readdirSync(searchpath); + instanceDivs = []; + const mainDiv = document.getElementById("main-div"); + + folders.forEach((folder) => { + if (fs.existsSync(path.join(searchpath, folder, "Logs", "dcs.log"))) { + instanceDivs.push(new InstanceDiv(mainDiv, path.join(searchpath, folder))); + } + }); + + mainDiv.replaceChildren(...instanceDivs.map((instanceDiv) => { + return instanceDiv.getDiv(); + })); + + instanceDivs.forEach((instanceDiv) => instanceDiv.checkFields()) + + } else { + console.error("An error occured while trying to fetch the location of the DCS folders.") + } + } + }) +} + +window.addEventListener('DOMContentLoaded', () => { + loadDivs(); +}) \ No newline at end of file diff --git a/manager/style.css b/manager/style.css new file mode 100644 index 00000000..0c9d6f3a --- /dev/null +++ b/manager/style.css @@ -0,0 +1,226 @@ +* { + font-family: "Open Sans", sans-serif; + box-sizing: border-box; +} + +body { + background-color: #181e25; +} + +#header { + display: flex; + justify-content: start; + align-items: center; + color: #F2F2F2; + font-weight: bold; + font-size: 16px; + padding: 10px; +} + +#header>div:first-child>div:last-child { + color: green; +} + +.main-icon { + width: 60px; + height: 60px; + margin: 10px; +} + +#main-div { + display: flex; + flex-direction: row; + height: 100%; + row-gap: 30px; + column-gap: 30px; + flex-wrap: wrap; + padding: 15px; +} + +body { + overflow-y: auto; + scrollbar-color: white transparent; + scrollbar-width: thin; +} + +::-webkit-scrollbar { + width: 10px; +} + +::-webkit-scrollbar-track { + background-color: transparent; + border-bottom-right-radius: 10px; + border-top-right-radius: 10px; + margin-top: 0px; +} + +::-webkit-scrollbar-thumb { + background-color: white; + border-radius: 100px; + margin-top: 10px; + opacity: 0.8; +} + +.instance-div { + display: flex; + flex-direction: column; + row-gap: 10px; + background-color: #3d4651; + height: fit-content; + padding: 20px 40px; + border-radius: 5px; + border-left: 5px solid #017DC1; + width: 600px; +} + +.instance-content { + display: flex; + flex-direction: column; + row-gap: 15px; +} + +.folder-name { + color: #F2F2F2; + font-weight: bold; + display: flex; + border-bottom: 1px solid #F2F2F2; + padding-bottom: 10px; + width: 100%; +} + +.folder-name span { + width: 500px; + overflow: hidden; + text-wrap: nowrap; + text-overflow: ellipsis; + direction: rtl; + text-align: left; +} + +.folder { + width: 20px; + height: 20px; + background-image: url("./icons/folder-open-solid.svg"); + margin-right: 15px; +} + +.input-table { + padding: 0px; + margin: 0px; + border-width: 0px; + border-collapse: collapse; +} + +.input-table td { + color: #F2F2F2; + padding: 5px 0px; + font-size: 13px; + vertical-align: top; +} + +.input-table td>* { + margin: 2px 0px; +} + +.action-buttons { + display: flex; + flex-direction: row; + column-gap: 12px; + justify-content: start; +} + +.label { + display: flex; + align-items: center; + column-gap: 5px; +} + +.icon { + background-size: 100% 100%; + background-repeat: no-repeat; +} + +.button { + width: fit-content; + height: 40px; + color: #F2F2F2; + border: 1px solid #F2F2F2; + background-size: 40px 60%; + background-position: 0px 50%; + background-repeat: no-repeat; + border-radius: 5px; + padding: 5px 15px 5px 45px; + font-weight: 600; + display: flex; + align-items: center; + font-size: 14px; + background-color: transparent; +} + +.button:not(.disabled) { + cursor: pointer; +} + +.apply { + background-image: url("./icons/check-solid.svg"); + background-color: #017DC1; + border: 1px solid transparent; +} + +.add { + padding: 5px 15px 5px 15px; +} + +.update { + padding: 5px 15px 5px 15px; +} + +.remove { + background-image: url("./icons/trash-can-regular.svg"); +} + +.disabled { + background-color: #797E83; +} + +.hide { + display: none; +} + +.message { + font-weight: 600; + height: 20px; + font-size: 13px; + color: #F2F2F2; +} + +input { + font-weight: 600; + font-size: 13px; + width: 240px; + border-radius: 4px; +} + +input:focus{ + outline: none; +} + +input.error { + border-color: #FF5858; +} + +.error { + font-weight: bold; + color: #FF5858; +} + +.accent-green { + color: #8bff63; +} + +.info { + width: 12px; + height: 12px; + background-image: url("./icons/circle-info-solid.svg"); + background-position: 50% 50%; +} \ No newline at end of file