Merge branch 'Pax1601:release-candidate' into 681_newest_changes_skill
3
.gitignore
vendored
@ -27,6 +27,7 @@ hgt
|
||||
L.Path.Drag.js
|
||||
leaflet-gesture-handling.css
|
||||
leaflet.nauticscale.js
|
||||
leaflet.css
|
||||
package-lock.json
|
||||
|
||||
!client/bin
|
||||
!client/bin
|
||||
|
||||
25
client/@types/olympus/index.d.ts
vendored
@ -105,6 +105,10 @@ declare module "constants/constants" {
|
||||
export const ROEs: string[];
|
||||
export const reactionsToThreat: string[];
|
||||
export const emissionsCountermeasures: string[];
|
||||
export const ERAS: {
|
||||
name: string;
|
||||
chronologicalOrder: number;
|
||||
}[];
|
||||
export const ROEDescriptions: string[];
|
||||
export const reactionsToThreatDescriptions: string[];
|
||||
export const emissionsCountermeasuresDescriptions: string[];
|
||||
@ -837,6 +841,7 @@ declare module "other/utils" {
|
||||
}): UnitBlueprint | null;
|
||||
export function getMarkerCategoryByName(name: string): "aircraft" | "helicopter" | "groundunit-sam" | "navyunit" | "groundunit-other";
|
||||
export function getUnitDatabaseByCategory(category: string): import("unit/databases/aircraftdatabase").AircraftDatabase | import("unit/databases/helicopterdatabase").HelicopterDatabase | import("unit/databases/groundunitdatabase").GroundUnitDatabase | import("unit/databases/navyunitdatabase").NavyUnitDatabase | null;
|
||||
export function getCategoryBlueprintIconSVG(category: string, unitName: string): string | false;
|
||||
export function base64ToBytes(base64: string): ArrayBufferLike;
|
||||
export function enumToState(state: number): string;
|
||||
export function enumToROE(ROE: number): string;
|
||||
@ -1602,6 +1607,7 @@ declare module "map/map" {
|
||||
import { CoalitionAreaContextMenu } from "contextmenus/coalitionareacontextmenu";
|
||||
import { AirbaseSpawnContextMenu } from "contextmenus/airbasespawnmenu";
|
||||
export type MapMarkerVisibilityControl = {
|
||||
"category"?: string;
|
||||
"image": string;
|
||||
"isProtected"?: boolean;
|
||||
"name": string;
|
||||
@ -1999,6 +2005,25 @@ declare module "unit/importexport/unitdatafileexport" {
|
||||
showForm(units: Unit[]): void;
|
||||
}
|
||||
}
|
||||
declare module "schemas/schema" {
|
||||
import Ajv from "ajv";
|
||||
import { AnySchemaObject } from "ajv/dist/core";
|
||||
abstract class JSONSchemaValidator {
|
||||
#private;
|
||||
constructor(schema: AnySchemaObject);
|
||||
getAjv(): Ajv;
|
||||
getCompiledValidator(): any;
|
||||
getErrors(): any;
|
||||
getSchema(): AnySchemaObject;
|
||||
validate(data: any): any;
|
||||
}
|
||||
export class AirbasesJSONSchemaValidator extends JSONSchemaValidator {
|
||||
constructor();
|
||||
}
|
||||
export class ImportFileJSONSchemaValidator extends JSONSchemaValidator {
|
||||
constructor();
|
||||
}
|
||||
}
|
||||
declare module "unit/importexport/unitdatafileimport" {
|
||||
import { Dialog } from "dialog/dialog";
|
||||
import { UnitDataFile } from "unit/importexport/unitdatafile";
|
||||
|
||||
@ -60,6 +60,11 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.leaflet-container img.leaflet-tile {
|
||||
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
|
||||
mix-blend-mode: plus-lighter;
|
||||
}
|
||||
|
||||
.leaflet-container.leaflet-touch-zoom {
|
||||
-ms-touch-action: pan-x pan-y;
|
||||
touch-action: pan-x pan-y;
|
||||
@ -646,7 +651,7 @@ svg.leaflet-image-layer.leaflet-interactive path {
|
||||
}
|
||||
|
||||
/* Printing */
|
||||
|
||||
|
||||
@media print {
|
||||
/* Prevent printers from removing background-images of controls. */
|
||||
.leaflet-control {
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
<link rel="icon" href="/images/favicons/favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="images/favicons/apple-touch-icon.png"/>
|
||||
<link rel="manifest" href="/images/favicons/site.webmanifest" />
|
||||
<meta name="robots" context="noindex" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
2
manager/.vscode/tasks.json
vendored
@ -6,7 +6,7 @@
|
||||
{
|
||||
"label": "mirror-package",
|
||||
"type": "shell",
|
||||
"command": "call ./scripts/mirror-package.bat",
|
||||
"command": "./scripts/mirror-package.bat",
|
||||
"isBackground": true
|
||||
}
|
||||
]
|
||||
|
||||
@ -1,67 +1,43 @@
|
||||
<style>
|
||||
#manager-connections .success,
|
||||
#manager-connections .error {
|
||||
position: absolute;
|
||||
left: 420px;
|
||||
display: flex;
|
||||
width: 150px;
|
||||
column-gap: 8px;
|
||||
}
|
||||
|
||||
#manager-connections .success {
|
||||
content: url("./icons/check-solid-green.svg");
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
#manager-connections .error img {
|
||||
content: url("./icons/triangle-exclamation-solid.svg");
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
#manager-connections .error span {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: var(--red);
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
</style>
|
||||
<div id="manager-connections">
|
||||
<div class="step-summary">
|
||||
<div class="blue <%= !install || simplified? 'hide': '' %>">User path</div>
|
||||
<div class="white">Ports and address</div>
|
||||
<div class="empty">Passwords</div>
|
||||
<div class="empty"> <%= install? 'Install': 'Update' %></div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="instructions">
|
||||
<span>
|
||||
Accept or modify port settings (optional)
|
||||
</span>
|
||||
<span>
|
||||
If you are installing Olympus locally for Single player use, it's recommended you leave these as default and continue.
|
||||
If you are installing a dedicated server, then follow the instructions available on the DCS Olympus Wiki.
|
||||
</span>
|
||||
<div id="connections-page">
|
||||
<div class="instructions">
|
||||
<div class="step">
|
||||
Step <%= instances.length === 1? "3": "4" %> of <%= instances.length === 1? "4": "5" %>
|
||||
</div>
|
||||
<div class="input-group client-port">
|
||||
<div class="title">
|
||||
Manually set Olympus port and address settings
|
||||
</div>
|
||||
<div class="note">
|
||||
Please note: you may be required to allow these ports through your firewall and modem/router via port
|
||||
forwarding. <br>
|
||||
Otherwise, others may not be able to connect to Olympus.
|
||||
</div>
|
||||
</div>
|
||||
<div class="wizard-inputs">
|
||||
<div class="input-group client-port port-input">
|
||||
<span>Client port
|
||||
<img src="./icons/circle-info-solid.svg" title="This port is used to allow access to Olympus. Be sure to allow this port through your firewall if you want people to connect remotely">
|
||||
<img src="./icons/circle-info-solid.svg"
|
||||
title="This port is used to allow access to Olympus. Be sure to allow this port through your firewall if you want people to connect remotely">
|
||||
</span>
|
||||
<div>
|
||||
<input type="number" min="1024" max="65535" value="<%= instance["clientPort"] %>">
|
||||
<input type="number" min="1024" max="65535" value="<%= activeInstance["clientPort"] %>"
|
||||
onchange="signal('onClientPortChanged', this.value)">
|
||||
<img class="success hide">
|
||||
<div class="error hide">
|
||||
<img> <span>Port already in use</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group backend-port">
|
||||
<div class="input-group backend-port port-input">
|
||||
<span>Backend port
|
||||
<img src="./icons/circle-info-solid.svg" title="This port is used to allow access to Olympus. Be sure to allow this port through your firewall if you want people to connect remotely.">
|
||||
<img src="./icons/circle-info-solid.svg"
|
||||
title="This port is used by Olympus to communicate with DCS. You only need to allow it through your firewall if you enable direct API connection">
|
||||
</span>
|
||||
<div>
|
||||
<input type="number" min="1024" max="65535" value="<%= instance["backendPort"] %>">
|
||||
<input type="number" min="1024" max="65535" value="<%= activeInstance["backendPort"] %>"
|
||||
onchange="signal('onBackendPortChanged', this.value)">
|
||||
<img class="success hide">
|
||||
<div class="error hide">
|
||||
<img> <span>Port already in use</span>
|
||||
@ -69,25 +45,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group backend-address">
|
||||
<span>Backend address
|
||||
<img src="./icons/circle-info-solid.svg" title="This is the backend address Olympus will listen on. Unless you know what you are doing, leave it as localhost, even for dedicated server installations.">
|
||||
<span onclick="signal('onEnableAPIClicked')">
|
||||
<div class="checkbox"></div> Enable direct backend API connection
|
||||
<img src="./icons/circle-info-solid.svg"
|
||||
title="Allows services to connect to Olympus directly. This is NOT NEEDED for normal Olympus operation, even for dedicated servers. Leave it unchecked if in doubt.">
|
||||
</span>
|
||||
<input type="text" value="<%= instance["backendAddress"] %>">
|
||||
</div>
|
||||
<div class="buttons-footer">
|
||||
<% if (!simplified) { %>
|
||||
<div class="button back">
|
||||
Back
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="button next">
|
||||
Next
|
||||
</div>
|
||||
<div class="note warning hide">
|
||||
Note: if you enable direct backend API connection, you will be required to run DCS as admin or run the netsh
|
||||
command for others to connect. Leave unchecked if you don't know what this is. <br>See the Olympus
|
||||
documentation for more details.
|
||||
</div>
|
||||
<% if (!simplified) { %>
|
||||
<div class="button cancel">
|
||||
<%= install? "Cancel installation": "Cancel editing" %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
25
manager/ejs/connectionsType.ejs
Normal file
@ -0,0 +1,25 @@
|
||||
<style>
|
||||
|
||||
</style>
|
||||
<div>
|
||||
<div class="instructions">
|
||||
<div class="step">
|
||||
Step <%= instances.length === 1? "2": "3" %> of <%= instances.length === 1? "4": "5" %>
|
||||
</div>
|
||||
<div class="title">
|
||||
Do you want to set port and address settings?
|
||||
</div>
|
||||
<div class="description">
|
||||
We can automatically set port and address settings for you, or you can set them manually. <br>
|
||||
If you don't have a good understanding of how Olympus works, we recommend the <i>auto apply settings</i> option.
|
||||
</div>
|
||||
</div>
|
||||
<div class="wizard-inputs">
|
||||
<div class="button radio auto selected" onclick="signal('onConnectionsTypeClicked', 'auto')">
|
||||
Auto apply settings
|
||||
</div>
|
||||
<div class="button radio manual" onclick="signal('onConnectionsTypeClicked', 'manual')">
|
||||
Manually set
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
78
manager/ejs/expertsettings.ejs
Normal file
@ -0,0 +1,78 @@
|
||||
<style>
|
||||
#expert-settings .content {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
<div id="expert-settings">
|
||||
<div class="instructions">
|
||||
<div class="title">
|
||||
Edit Olympus instance
|
||||
</div>
|
||||
<div class="description">
|
||||
Please note: you may be required to allow these ports through your firewall and modem/router via port forwarding. <br>
|
||||
Otherwise, others may not be able to connect to Olympus.
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="wizard-inputs">
|
||||
<div class="input-group game-master">
|
||||
<span>Game Master Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to access Olympus as Game Master with full privileges.">
|
||||
</span>
|
||||
<input type="password" minlength="8" onchange="signal('onGameMasterPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["gameMasterPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="input-group blue-commander">
|
||||
<span>Blue Commander Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to access Olympus as blue coalition Commander.">
|
||||
</span>
|
||||
<input type="password" minlength="8" onchange="signal('onBlueCommanderPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["blueCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="input-group red-commander">
|
||||
<span>Red Commander Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to access Olympus as red coalition Commander.">
|
||||
</span>
|
||||
<input type="password" minlength="8" onchange="signal('onRedCommanderPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["redCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="<%= activeInstance["installed"]? '': 'hide' %>" style="color: var(--offwhite); font-size: var(--normal); color: var(--lightgray);">
|
||||
Note: to keep the old passwords, click <b>Next</b> without editing any value.
|
||||
</div>
|
||||
</div>
|
||||
<div class="wizard-inputs">
|
||||
<div class="input-group client-port port-input">
|
||||
<span>Client port
|
||||
<img src="./icons/circle-info-solid.svg"
|
||||
title="This port is used to allow access to Olympus. Be sure to allow this port through your firewall if you want people to connect remotely">
|
||||
</span>
|
||||
<div>
|
||||
<input type="number" min="1024" max="65535" value="<%= activeInstance["clientPort"] %>"
|
||||
onchange="signal('onClientPortChanged', this.value)">
|
||||
<img class="success hide">
|
||||
<div class="error hide">
|
||||
<img> <span>Port already in use</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group backend-port port-input">
|
||||
<span>Backend port
|
||||
<img src="./icons/circle-info-solid.svg"
|
||||
title="This port is used by Olympus to communicate with DCS. You only need to allow it through your firewall if you enable direct API connection">
|
||||
</span>
|
||||
<div>
|
||||
<input type="number" min="1024" max="65535" value="<%= activeInstance["backendPort"] %>"
|
||||
onchange="signal('onBackendPortChanged', this.value)">
|
||||
<img class="success hide">
|
||||
<div class="error hide">
|
||||
<img> <span>Port already in use</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group backend-address">
|
||||
<span onclick="signal('onEnableAPIClicked')">
|
||||
<div class="checkbox"></div> Enable direct backend API connection
|
||||
<img src="./icons/circle-info-solid.svg"
|
||||
title="Allows services to connect to Olympus directly. This is NOT NEEDED for normal Olympus operation, even for dedicated servers. Leave it unchecked if in doubt.">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
36
manager/ejs/folder.ejs
Normal file
@ -0,0 +1,36 @@
|
||||
<style>
|
||||
|
||||
</style>
|
||||
<div>
|
||||
<div class="instructions">
|
||||
<% if (instances.length > 0) { %>
|
||||
<div class="step">
|
||||
Step 1 of <%= instances.length === 1? "4": "5" %>
|
||||
</div>
|
||||
<div class="title">
|
||||
Which DCS instance you want to add Olympus to?
|
||||
</div>
|
||||
<div class="description">
|
||||
Olympus is added to DCS instances individually, and will only work for that specific instance. <br>
|
||||
You can have Olympus installed across multiple DCS instances. Re-run in the install wizard to add Olympus to another DCS install.
|
||||
</div>
|
||||
<% } else { %>
|
||||
<span class="title">
|
||||
No DCS installs detected
|
||||
</span>
|
||||
<span class="description">
|
||||
Please ensure you have DCS installed correctly. <br>
|
||||
Olympus cannot be added unless there is a DCS Saved Games folder on your computer. <br><br>
|
||||
If you are still having issues, try re-installing DCS and Olympus <br><br>
|
||||
<b>If DCS is installed but Olympus is failing to detect it, you can add it manually.<br> See the troubleshooting guide for more info.</b>
|
||||
</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="wizard-inputs">
|
||||
<% for (var i = 0; i < instances.length; i++) { %>
|
||||
<div class="button radio <%= (i === 0)? 'selected': '' %>" onclick="signal('onFolderClicked', '<%= instances[i].name %>')" data-folder="<%= instances[i].folder %>">
|
||||
<%= instances[i].name %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,126 +0,0 @@
|
||||
<style>
|
||||
#manager-installations .scroll-container {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#manager-installations .scrollable {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 8px;
|
||||
height: fit-content;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#manager-installations .option {
|
||||
cursor: pointer;
|
||||
background-color: var(--darkgray);
|
||||
width: 600px;
|
||||
height: 100px;
|
||||
color: white;
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding-left: 15px;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#manager-installations .option * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#manager-installations .option {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 5px;
|
||||
align-items: start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#manager-installations .option>span:nth-child(1) {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#manager-installations .option>span:nth-child(2) {
|
||||
display: flex;
|
||||
column-gap: 10px;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#manager-installations .option>span:nth-child(3) {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--lightgray);
|
||||
}
|
||||
|
||||
#manager-installations .option>span:nth-child(3).installed {
|
||||
font-weight: 600;
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
#manager-installations .option>span:nth-child(3).error {
|
||||
font-weight: 600;
|
||||
color: orange;
|
||||
}
|
||||
|
||||
#manager-installations .option.installed {
|
||||
pointer-events: none;
|
||||
background-color: var(--background-disabled);
|
||||
}
|
||||
|
||||
#manager-installations .option:not(.installed)::after {
|
||||
display: block;
|
||||
content: " ";
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: url("./icons/chevron-right-solid.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
}
|
||||
</style>
|
||||
<div id="manager-installations">
|
||||
<div class="step-summary">
|
||||
<div class="white">User path</div>
|
||||
<div class="empty">Ports and address</div>
|
||||
<div class="empty">Passwords</div>
|
||||
<div class="empty">Install</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="instructions">
|
||||
<span>
|
||||
Select a DCS path to install Olympus to.
|
||||
</span>
|
||||
<span>
|
||||
We have automatically detected the following DCS installations under your Saved Games / DCS folder.
|
||||
</span>
|
||||
<span>
|
||||
Please select which DCS installations you want to add Olympus to.
|
||||
</span>
|
||||
</div>
|
||||
<div class="scroll-container">
|
||||
<div class="scrollable">
|
||||
<% for (let i = 0; i < instances.length; i++) {%>
|
||||
<div class="option <%= instances[i].installed? 'installed': '' %>" data-folder="<%= instances[i].folder %>">
|
||||
<span><%= instances[i].name %></span>
|
||||
<span><img src="./icons/folder-open-solid.svg"> <%= instances[i].folder %></span>
|
||||
<span class="<%= instances[i].installed? (instances[i].error? 'error': 'installed'): '' %>">
|
||||
<%= instances[i].installed? (instances[i].error? 'Corrupted/outdated Olympus installation': 'Olympus installed'): 'Olympus not installed' %>
|
||||
</span>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button cancel">
|
||||
Cancel installation
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,274 +1,121 @@
|
||||
<style>
|
||||
#manager-instances {
|
||||
padding-left: 80px;
|
||||
padding-right: 80px;
|
||||
}
|
||||
|
||||
#manager-instances .scroll-container {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
max-width: 100% !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#manager-instances .scrollable {
|
||||
display: flex;
|
||||
row-gap: 15px;
|
||||
column-gap: 15px;
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#manager-instances .option {
|
||||
background-color: var(--darkgray);
|
||||
width: 48%;
|
||||
color: white;
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding: 15px;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
border-left: 5px solid var(--blue);
|
||||
flex-direction: column;
|
||||
row-gap: 25px;
|
||||
}
|
||||
|
||||
#manager-instances>.instructions {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#manager-instances .button.cancel {
|
||||
position: absolute;
|
||||
left: 110px;
|
||||
top: 130px;
|
||||
}
|
||||
|
||||
#manager-instances .server-data {
|
||||
display: flex;
|
||||
column-gap: 15px;
|
||||
row-gap: 5px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#manager-instances .server-status {
|
||||
font-weight: 600;
|
||||
font-size: 15;
|
||||
display: flex;
|
||||
column-gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#manager-instances .server-status::before {
|
||||
display: block;
|
||||
content: "";
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 999px;
|
||||
background-color: var(--gray);
|
||||
}
|
||||
|
||||
#manager-instances .server-status.offline {
|
||||
color: var(--gray)
|
||||
}
|
||||
|
||||
#manager-instances .server-status.offline::before {
|
||||
background-color: var(--gray);
|
||||
}
|
||||
|
||||
#manager-instances .server-status.online {
|
||||
color: var(--green)
|
||||
}
|
||||
|
||||
#manager-instances .server-status.online::before {
|
||||
background-color: var(--green);
|
||||
}
|
||||
|
||||
#manager-instances .server-status.backend {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
#manager-instances .server-data-entry {
|
||||
display: flex;
|
||||
column-gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#manager-instances .server-data-entry span:nth-child(2) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#manager-instances .server-data-entry span:nth-child(3) {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#manager-instances .instance-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#manager-instances .instance-info>span:nth-child(1) {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#manager-instances .instance-info>span:nth-child(2) {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--lightgray);
|
||||
}
|
||||
|
||||
#manager-instances .instance-info>span:nth-child(2).installed {
|
||||
font-weight: 600;
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
#manager-instances .instance-info>span:nth-child(2).error {
|
||||
font-weight: 600;
|
||||
color: orange;
|
||||
}
|
||||
|
||||
#manager-instances .instance-info>span:nth-child(3) {
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
color: var(--lightgray);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 8px;
|
||||
}
|
||||
|
||||
#manager-instances .instance-info>span:nth-child(4) {
|
||||
display: flex;
|
||||
column-gap: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#manager-instances .instance-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
#manager-instances .instance-info .info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#manager-instances .instance-info .info>div:nth-child(1) {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#manager-instances .instance-info .info>div:nth-child(2) {
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#manager-instances .instance-info .divider {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#manager-instances .start, #manager-instances .open-browser {
|
||||
margin-right: auto;
|
||||
color: var(--offwhite);
|
||||
background-color: var(--blue);
|
||||
}
|
||||
|
||||
#manager-instances .start {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
#manager-instances .start>div {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
#manager-instances .edit,
|
||||
#manager-instances .uninstall,
|
||||
#manager-instances .stop {
|
||||
color: var(--offwhite);
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--offwhite);
|
||||
}
|
||||
|
||||
#manager-instances .edit:hover,
|
||||
#manager-instances .uninstall:hover,
|
||||
#manager-instances .stop:hover {
|
||||
color: var(--background);
|
||||
background-color: var(--offwhite);
|
||||
}
|
||||
|
||||
</style>
|
||||
<div id="manager-instances">
|
||||
<div class="dashboard">
|
||||
<% if (state === 'INSTALL') { %>
|
||||
<div class="result-summary success <%= (activeInstance !== undefined && !activeInstance["error"] && activeInstance["installed"])? "": "hide" %>">
|
||||
<div class="title"><img src="./icons/check-solid-background.svg">Olympus installed successfully in
|
||||
<i style="margin-left: 3px"><%= activeInstance !== undefined? activeInstance["name"]: "" %></i>!</div>
|
||||
</div>
|
||||
<div class="result-summary error <%= (activeInstance !== undefined && (activeInstance["error"] || !activeInstance["installed"]))? "": "hide" %>">
|
||||
<div class="title"><img src="./icons/triangle-exclamation-solid-background.svg">An error occurred while installing Olympus in
|
||||
<i style="margin-left: 3px"><%= activeInstance !== undefined? activeInstance["name"]: "" %></i></div>
|
||||
</div>
|
||||
<% } else if (state === 'EDIT') {%>
|
||||
<div class="result-summary success <%= (activeInstance !== undefined && !activeInstance["error"])? "": "hide" %>">
|
||||
<div class="title"><img src="./icons/check-solid-background.svg">Olympus settings updated for
|
||||
<i style="margin-left: 3px"><%= activeInstance !== undefined? activeInstance["name"]: "" %></i>!</div>
|
||||
</div>
|
||||
<div class="result-summary error <%= (activeInstance !== undefined && activeInstance["error"])? "": "hide" %>">
|
||||
<div class="title"><img src="./icons/triangle-exclamation-solid-background.svg">An error occurred while updating Olympus settings for
|
||||
<i style="margin-left: 3px"><%= activeInstance !== undefined? activeInstance["name"]: "" %></i></div>
|
||||
</div>
|
||||
<% } else {%>
|
||||
<div class="result-summary success <%= (activeInstance !== undefined && !activeInstance["installed"])? "": "hide" %>">
|
||||
<div class="title"><img src="./icons/check-solid-background.svg">Olympus removed successfully from
|
||||
<i style="margin-left: 3px"><%= activeInstance !== undefined? activeInstance["name"]: "" %></i>!</div>
|
||||
</div>
|
||||
<div class="result-summary error <%= (activeInstance !== undefined && activeInstance["installed"])? "": "hide" %>">
|
||||
<div class="title"><img src="./icons/triangle-exclamation-solid-background.svg">An error occurred while removing Olympus settings from
|
||||
<i style="margin-left: 3px"><%= activeInstance !== undefined? activeInstance["name"]: "" %></i></div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="content">
|
||||
<div class="button cancel">
|
||||
<img src="./icons/chevron-left-solid.svg"/> Return to menu
|
||||
</div>
|
||||
<div class="instructions">
|
||||
<span>
|
||||
View and manage installs
|
||||
</span>
|
||||
<span>
|
||||
The following Oympus installs have been identified. <br>You can start an Olympus server, modify settings and uninstall below.
|
||||
</span>
|
||||
<% if (instances.length > 0) { %>
|
||||
<span class="title">
|
||||
View and manage installs
|
||||
</span>
|
||||
<span class="subtitle">
|
||||
The following DCS installations have been identified. <br>You can start an Olympus server, modify settings and uninstall below.
|
||||
</span>
|
||||
<% } else { %>
|
||||
<span class="title" style="margin-top: 150px;">
|
||||
No DCS installs detected
|
||||
</span>
|
||||
<span class="subtitle">
|
||||
Please ensure you have DCS installed correctly. <br>
|
||||
Olympus cannot be added unless there is a DCS Saved Games folder on your computer. <br><br>
|
||||
If you are still having issues, try re-installing DCS and Olympus <br><br>
|
||||
<b>If DCS is installed but Olympus is failing to detect it, you can add it manually.<br> See the troubleshooting guide for more info.</b>
|
||||
</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="scroll-container">
|
||||
<div class="scrollable">
|
||||
<% for (let i = 0; i < instances.length; i++) {%>
|
||||
<div class="option" data-folder="<%= instances[i].folder %>">
|
||||
<div class="instance-info">
|
||||
<span><%= instances[i].name %></span>
|
||||
<span class="<%= instances[i].installed? (instances[i].error? 'error': ''): '' %>">
|
||||
<%= instances[i].installed? (instances[i].error? 'Corrupted/outdated Olympus installation': ''): '' %>
|
||||
</span>
|
||||
<span><img src="./icons/folder-open-solid.svg"> <%= instances[i].folder %></span>
|
||||
<div class="server-data">
|
||||
<div class="server-status webserver online hide">ACTIVE</div>
|
||||
<div class="server-status webserver offline">OFFLINE</div>
|
||||
<div class="server-status backend online hide">CONNECTED</div>
|
||||
<div class="server-status backend offline">DISCONNECTED</div>
|
||||
<div class="server-data-entry fps"><img src="./icons/display-solid.svg"><span>FPS: </span><span class="data">0</span></div>
|
||||
<div class="server-data-entry load"><img src="./icons/server-solid.svg"><span>Load: </span><span class="data">0</span></div>
|
||||
<div class="server-data-entry uptime"></div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="info">
|
||||
<div>Client port</div>
|
||||
<div> <%= instances[i].clientPort %> </div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div>Backend port</div>
|
||||
<div> <%= instances[i].backendPort %> </div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div>Backend address</div>
|
||||
<div> <%= instances[i].backendAddress %> </div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="instance-buttons">
|
||||
<div class="button start collapse">
|
||||
Start Olympus
|
||||
<div>
|
||||
<div class="button start-server">Start server</div>
|
||||
<div class="button start-client">Start client</div>
|
||||
|
||||
<div class="scroll-container">
|
||||
<div class="scrollable">
|
||||
<% for (let i = 0; i < instances.length; i++) {%>
|
||||
<div class="option <%= instances[i].installed? 'installed': '' %>" data-folder="<%= instances[i].folder %>">
|
||||
<div class="instance-info">
|
||||
<span class="name"><%= instances[i].name %></span>
|
||||
|
||||
<span class="folder"><img src="./icons/folder-open-solid.svg"> <%= instances[i].folder %></span>
|
||||
|
||||
<div class="server-data">
|
||||
<div class="server-status webserver online hide">ACTIVE</div>
|
||||
<div class="server-status webserver offline">OFFLINE</div>
|
||||
<div class="server-status backend online hide">CONNECTED</div>
|
||||
<div class="server-status backend offline">DISCONNECTED</div>
|
||||
<div class="server-data-entry fps"><img src="./icons/display-solid.svg"><span>FPS: </span><span class="data">0</span></div>
|
||||
<div class="server-data-entry load"><img src="./icons/server-solid.svg"><span>Load: </span><span class="data">0</span></div>
|
||||
<div class="server-data-entry uptime"></div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<span class="status <%= instances[i].installed? (instances[i].error? 'error': 'installed'): '' %>">
|
||||
<%= instances[i].installed? (instances[i].error? 'Corrupted/outdated Olympus installation': 'Olympus installed'): 'Olympus not installed' %>
|
||||
</span>
|
||||
|
||||
<div class="summary">
|
||||
<div class="info">
|
||||
<div>Client port</div>
|
||||
<div> <%= instances[i].installed? instances[i].clientPort: "N/A" %> </div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div>Backend port</div>
|
||||
<div> <%= instances[i].installed? instances[i].backendPort: "N/A" %> </div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div>Backend address</div>
|
||||
<div> <%= instances[i].installed? instances[i].backendAddress: "N/A" %> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="instance-buttons">
|
||||
<div class="button start collapse">
|
||||
Start Olympus
|
||||
<div>
|
||||
<div class="button start-server" onclick="signal('onStartServerClicked', '<%= instances[i].name %>')">Start server</div>
|
||||
<div class="button start-client" onclick="signal('onStartClientClicked', '<%= instances[i].name %>')">Start client</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button edit">Edit settings</div>
|
||||
<div class="button uninstall">Uninstall Olympus</div>
|
||||
<div class="button open-browser hide">Open in browser</div>
|
||||
<div class="button stop hide">Stop Olympus</div>
|
||||
<div class="button edit" onclick="signal('onEditClicked', '<%= instances[i].name %>')">Edit settings</div>
|
||||
<div class="button install" onclick="signal('onInstallClicked', '<%= instances[i].name %>')">Install Olympus</div>
|
||||
<div class="button uninstall" onclick="signal('onUninstallClicked', '<%= instances[i].name %>')">Uninstall Olympus</div>
|
||||
<div class="button open-browser hide" onclick="signal('onOpenBrowserClicked', '<%= instances[i].name %>')">Open in browser</div>
|
||||
<div class="button stop hide" onclick="signal('onStopClicked', '<%= instances[i].name %>')">Stop Olympus</div>
|
||||
</div>
|
||||
<div class="logs-link" onclick="
|
||||
signal('onTextFileClicked', '<%= instances[i].folder.replaceAll('\\', '/')+'/Logs/dcs.log' %>');
|
||||
signal('onTextFileClicked', '<%= instances[i].folder.replaceAll('\\', '/')+'/Logs/Olympus_log.txt' %>');
|
||||
">Open logs</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,62 +1,55 @@
|
||||
<style>
|
||||
#summary {
|
||||
width: 70%;
|
||||
#manager-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
color: var(--offwhite);
|
||||
padding: 80px;
|
||||
min-height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#summary .icon {
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
margin-bottom: -30px;
|
||||
}
|
||||
|
||||
#summary div:nth-child(1) {
|
||||
font-size: 50px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#summary div:nth-child(2) {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#summary div:nth-child(3) {
|
||||
color: var(--lightgray);
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
margin-top: 20px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
#manager-menu #menu {
|
||||
#manager-menu>div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 20px;
|
||||
width: 60%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#manager-menu .option {
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--offwhite);
|
||||
width: 460px;
|
||||
height: 70px;
|
||||
height: 110px;
|
||||
color: var(--offwhite);
|
||||
display: flex;
|
||||
font-size: 18px;
|
||||
font-size: var(--large);
|
||||
font-weight: 600;
|
||||
padding-left: 15px;
|
||||
align-items: center;
|
||||
align-items: start;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
color: var(--offwhite);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
row-gap: 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#manager-menu .option>div {
|
||||
font-size: var(--normal);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#manager-menu .option::after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: " ";
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: url("./icons/chevron-right-solid.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
#manager-menu .option:hover {
|
||||
@ -64,6 +57,10 @@
|
||||
background-color: var(--offwhite);
|
||||
}
|
||||
|
||||
#manager-menu .option:hover::after {
|
||||
filter: invert();
|
||||
}
|
||||
|
||||
#manager-menu .option.disabled {
|
||||
pointer-events: none;
|
||||
color: var(--darkgray);
|
||||
@ -73,20 +70,31 @@
|
||||
#manager-menu .option * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
<div id="manager-menu">
|
||||
<div id="summary">
|
||||
<div>DCS OLYMPUS <img class="icon" src="../img/OlympusLogoFinal_4k.png" \></div>
|
||||
<div>INSTALL WIZARD AND MANAGER</div>
|
||||
<div>Using this manager, you can install Olympus, update settings, and view and manage instances</div>
|
||||
</div>
|
||||
<div id="menu">
|
||||
<div class="option install <%= installEnabled? '': 'disabled' %>">
|
||||
Install Olympus
|
||||
<div id="summary" style="width: 70%; height: 100%; color: var(--offwhite); padding: 80px;">
|
||||
<div style="font-size: 50px; font-weight: bold;">
|
||||
DCS OLYMPUS <img src="../img/OlympusLogoFinal_4k.png" style="height: 100px; width: 100px; margin-bottom: -30px;"\>
|
||||
</div>
|
||||
<div class="option manage <%= manageEnabled? '': 'disabled' %>">
|
||||
View / Manage installs
|
||||
<div style="font-size: 20px; font-weight: bold;">
|
||||
INSTALL WIZARD AND MANAGER
|
||||
</div>
|
||||
<div style="color: var(--lightgray); font-size: var(--normal); font-weight: normal; margin-top: 20px; width: 300px;">
|
||||
Using this manager, you can install Olympus, update settings, and view and manage instances
|
||||
</div>
|
||||
</div>
|
||||
<div id="menu" style="row-gap: 20px; width: 60%;">
|
||||
<div class="option" onclick="signal('onInstallMenuClicked')">
|
||||
Add Olympus
|
||||
<div>
|
||||
Add or update Olympus to a new DCS instance
|
||||
</div>
|
||||
</div>
|
||||
<div class="option <%= instances.find(instance => instance.installed)? '': 'disabled' %>" onclick="signal('onEditMenuClicked')">
|
||||
Change settings
|
||||
<div>
|
||||
Adjust port, address and password settings
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,50 +1,40 @@
|
||||
<style>
|
||||
|
||||
</style>
|
||||
<div id="manager-passwords">
|
||||
<div class="step-summary">
|
||||
<div class="blue <%= !install || simplified? 'hide': '' %>">User path</div>
|
||||
<div class="blue">Ports and address</div>
|
||||
<div class="white">Passwords</div>
|
||||
<div class="empty"> <%= install? 'Install': 'Update' %></div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="instructions">
|
||||
<span>
|
||||
Enter your passwords to access Olympus
|
||||
</span>
|
||||
<span>
|
||||
When logging into Olympus, these passwords will let you access the different roles. Gamemaster is the default.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</style>
|
||||
<div id="passwords-page">
|
||||
<div class="instructions">
|
||||
<div class="step">
|
||||
Step <%= instances.length === 1? "4": "5" %> of <%= instances.length === 1? "4": "5" %>
|
||||
</div>
|
||||
<div class="title">
|
||||
Enter your passwords for Olympus
|
||||
</div>
|
||||
<div class="description">
|
||||
When logging into Olympus, these passwords will let you access the different roles. <br>
|
||||
Game Master is the default and is used as a global commander. The other two are used as a part of the RTS mode.
|
||||
</div>
|
||||
</div>
|
||||
<div class="wizard-inputs">
|
||||
<div class="input-group game-master">
|
||||
<span>Game Master Password<img src="./icons/circle-info-solid.svg" title="This password is used to access Olympus as Game Master with full privileges.">
|
||||
<span>Game Master Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to access Olympus as Game Master with full privileges.">
|
||||
</span>
|
||||
<input type="password" minlength="8">
|
||||
<input type="password" minlength="8" onchange="signal('onGameMasterPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["gameMasterPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="input-group blue-commander">
|
||||
<span>Blue Commander Password<img src="./icons/circle-info-solid.svg" title="This password is used to access Olympus as blue coalition Commander.">
|
||||
<span>Blue Commander Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to access Olympus as blue coalition Commander.">
|
||||
</span>
|
||||
<input type="password" minlength="8">
|
||||
<input type="password" minlength="8" onchange="signal('onBlueCommanderPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["blueCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="input-group red-commander">
|
||||
<span>Red Commander Password<img src="./icons/circle-info-solid.svg" title="This password is used to access Olympus as red coalition Commander.">
|
||||
<span>Red Commander Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to access Olympus as red coalition Commander.">
|
||||
</span>
|
||||
<input type="password" minlength="8">
|
||||
<input type="password" minlength="8" onchange="signal('onRedCommanderPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["redCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="buttons-footer">
|
||||
<div class="button back">
|
||||
Back
|
||||
</div>
|
||||
<div class="button next">
|
||||
Next
|
||||
</div>
|
||||
<div class="<%= activeInstance["installed"]? '': 'hide' %>" style="color: var(--offwhite); font-size: var(--normal); color: var(--lightgray);">
|
||||
Note: to keep the old passwords, click <b>Next</b> without editing any value.
|
||||
</div>
|
||||
<% if (!simplified) { %>
|
||||
<div class="button cancel">
|
||||
<%= install? "Cancel installation": "Cancel editing" %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,146 +1,124 @@
|
||||
<style>
|
||||
#manager-result .content {
|
||||
width: 100% !important;
|
||||
#result-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 30px;
|
||||
padding: 60px 120px;
|
||||
}
|
||||
|
||||
#manager-result img.success {
|
||||
content: url("./icons/check-solid-green.svg");
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
#manager-result img.error {
|
||||
content: url("./icons/triangle-exclamation-solid.svg");
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
#manager-result img.wait {
|
||||
content: url("./icons/spinner-solid.svg");
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
animation: rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
#manager-result .summary {
|
||||
font-weight: 600;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
#manager-result .summary.success {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
#manager-result .summary.error {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
#manager-result .info {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--offwhite);
|
||||
}
|
||||
|
||||
#manager-result .step {
|
||||
#result-page .instructions-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--offwhite);
|
||||
column-gap: 10px;
|
||||
flex-direction: column;
|
||||
row-gap: 15px;
|
||||
}
|
||||
|
||||
#manager-result .result {
|
||||
cursor: pointer;
|
||||
background-color: var(--darkgray);
|
||||
border-left: 5px solid var(--blue);
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
color: white;
|
||||
#result-page .usage-instructions {
|
||||
background-color: var(--background-usage);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding-left: 15px;
|
||||
flex-direction: row;
|
||||
column-gap: 25px;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
width: 500px;
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
#manager-result .result>img {
|
||||
margin-left: 5px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
#manager-result .result>div {
|
||||
#result-page .usage-instructions>div {
|
||||
color: var(--offwhite);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 5px;
|
||||
justify-items: center;
|
||||
align-items: start;
|
||||
font-size: var(--normal);
|
||||
}
|
||||
|
||||
#manager-result .result>div>span:nth-child(1) {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
#result-page .usage-instructions>div>img {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
#manager-result .result>div>span:nth-child(2) {
|
||||
display: flex;
|
||||
column-gap: 10px;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
#result-page .usage-instructions>img {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
#result-page .link {
|
||||
display: inline;
|
||||
color: #5CA7FF;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<div id="manager-result">
|
||||
<div class="content">
|
||||
<div class="step hook <%= !install? 'hide': '' %>">
|
||||
Installing hook scripts<img class="wait"><img class="success hide"><img class="error hide">
|
||||
<div id="result-page">
|
||||
<div class="result-summary success hide">
|
||||
<div class="title"><img src="./icons/check-solid-background.svg">Olympus successfully added to <i style="margin-left: 3px"><%= activeInstance["name"] %></i>!</div>
|
||||
<div class="description">See the <b>DCS Olympus Wiki</b> for more information on how to use Olympus and for troubleshooting issues. You may now close the installer.</div>
|
||||
</div>
|
||||
<div class="result-summary error hide">
|
||||
<div class="title"><img src="./icons/triangle-exclamation-solid-background.svg">An error occurred while adding Olympus to <i><%= activeInstance["name"] %></i></div>
|
||||
<div class="description">See the manager log located in <i><%= logLocation %></i> for more information.</div>
|
||||
</div>
|
||||
<div class="instructions-group hide">
|
||||
<div style="font-size: var(--very-large); font-weight: bold; color: var(--offwhite);">
|
||||
How to launch Olympus
|
||||
</div>
|
||||
<div class="step mod <%= !install? 'hide': '' %>">
|
||||
Installing mod folder<img class="wait"><img class="success hide"><img class="error hide">
|
||||
<div style="font-size: var(--normal); color: var(--offwhite);">
|
||||
To launch Olympus, there are shortcuts available on the desktop and in the <i><b><%= activeInstance["name"] %></b></i> folder under <i><b>Saved Games</b></i>.
|
||||
</div>
|
||||
<div class="step json <%= !install? 'hide': '' %>">
|
||||
Installing configuration file<img class="wait"><img class="success hide"><img class="error hide">
|
||||
</div>
|
||||
<div class="step config">
|
||||
Applying configuration<img class="wait"><img class="success hide"><img class="error hide">
|
||||
</div>
|
||||
<div class="step shortcuts <%= !install? 'hide': '' %>">
|
||||
Creating shortcuts<img class="wait"><img class="success hide"><img class="error hide">
|
||||
</div>
|
||||
|
||||
<div class="summary success hide">
|
||||
Olympus successfully installed in the following DCS instance
|
||||
</div>
|
||||
|
||||
<div class="summary error hide">
|
||||
An error has occurred while installing Olympus
|
||||
</div>
|
||||
|
||||
<div class="info success hide">
|
||||
You may now start DCS and use Olympus either with the shortcuts or the "View and manage instances" entry in the
|
||||
main menu.
|
||||
</div>
|
||||
|
||||
<div class="info error hide">
|
||||
Please make sure DCS is not currently being executed. Check <%= logLocation %> for more info.
|
||||
</div>
|
||||
|
||||
<div class="result">
|
||||
<img class="wait"><img class="success hide"><img class="error hide">
|
||||
<div>
|
||||
<span>
|
||||
<%= instance.name %>
|
||||
</span>
|
||||
<span><img src="./icons/folder-open-solid.svg">
|
||||
<%= instance.folder %>
|
||||
</span>
|
||||
<% if (activeInstance["installationType"] === "singleplayer") { %>
|
||||
<div class="usage-instructions" style="width: 600px;">
|
||||
<div>
|
||||
<img src="./icons/olympus_white.png">
|
||||
<div>
|
||||
Launch the <b>Olympus Client</b> via the shortcut on your desktop or in <i><b><%= activeInstance["name"] %></b></i>.
|
||||
</div>
|
||||
</div>
|
||||
<img src="./icons/arrow-right-solid.svg">
|
||||
<div>
|
||||
<img src="./icons/gamepad-solid.svg">
|
||||
<div>
|
||||
<b>Launch DCS</b>, load a mission and unpause the game. Enjoy!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons-footer">
|
||||
<div class="button back">
|
||||
Back to main menu
|
||||
<div style="font-size: var(--normal);">
|
||||
Alternatively, you can run the <b>Olympus Server</b> instead and visit <div class="link" onclick="signal('onLinkClicked', 'http://localhost:<%= activeInstance["clientPort"] %>')" >http://localhost:<%= activeInstance["clientPort"] %></div> in a web browser (Google Chrome recommended) to replace the first step above.
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="usage-instructions">
|
||||
<div>
|
||||
<img src="./icons/server-solid.svg">
|
||||
<div>
|
||||
Launch the <b>Olympus Server</b> via the shortcut on your desktop or in <b><%= activeInstance["name"] %></b>.
|
||||
</div>
|
||||
</div>
|
||||
<img src="./icons/arrow-right-solid.svg">
|
||||
<div>
|
||||
<img src="./icons/chrome.svg">
|
||||
<div>
|
||||
To access Olympus remotely visit <div class="link" onclick="signal('onLinkClicked', 'http://<%= IP %>:<%= activeInstance["clientPort"] %>')">http://<%= IP %>:<%= activeInstance["clientPort"] %></div> <b>in a web browser</b> (Google Chrome recommended).
|
||||
</div>
|
||||
</div>
|
||||
<img src="./icons/arrow-right-solid.svg">
|
||||
<div>
|
||||
<img src="./icons/gamepad-solid.svg">
|
||||
<div>
|
||||
<b>Launch DCS</b>, load a mission and unpause the game. Enjoy!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size: var(--normal);">
|
||||
<b>To access Olympus from this PC</b>, you need to visit <div class="link" onclick="signal('onLinkClicked', 'http://localhost:<%= activeInstance["clientPort"] %>')">http://localhost:<%= activeInstance["clientPort"] %></div> in a web browser (Google Chrome recommended) instead.
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="buttons-footer">
|
||||
<div class="button back" style="color: var(--offwhite); background-color: var(--background); border: 1px solid var(--offwhite);" onclick="signal('onReturnClicked')">
|
||||
Return to main menu
|
||||
</div>
|
||||
<div class="button next" style="color: var(--background); background-color: var(--offwhite);" onclick="signal('onCloseManagerClicked')">
|
||||
Close manager
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
89
manager/ejs/settings.ejs
Normal file
@ -0,0 +1,89 @@
|
||||
<style>
|
||||
|
||||
</style>
|
||||
<div class="dashboard">
|
||||
<div class="cancel" style="font-size: var(--normal); font-weight: 600; color: var(--offwhite); display: flex; align-items: center; column-gap: 10px; cursor: pointer; text-decoration: underline; " onclick="signal('onCancelClicked')">
|
||||
<img src="./icons/chevron-left-solid.svg" style=" height: 14px;">Back to menu
|
||||
</div>
|
||||
<% if (state === 'EDIT') {%>
|
||||
<div class="result-summary success <%= (activeInstance !== undefined && !activeInstance["error"])? "": "hide" %>">
|
||||
<div class="title"><img src="./icons/check-solid-background.svg">Olympus settings updated for
|
||||
<i style="margin-left: 3px"><%= activeInstance !== undefined? activeInstance["name"]: "" %></i>!</div>
|
||||
</div>
|
||||
<div class="result-summary error <%= (activeInstance !== undefined && activeInstance["error"])? "": "hide" %>">
|
||||
<div class="title"><img src="./icons/triangle-exclamation-solid-background.svg">An error occurred while updating Olympus settings for
|
||||
<i style="margin-left: 3px"><%= activeInstance !== undefined? activeInstance["name"]: "" %></i></div>
|
||||
</div>
|
||||
<% } else {%>
|
||||
<div class="result-summary success <%= (activeInstance !== undefined && !activeInstance["installed"])? "": "hide" %>">
|
||||
<div class="title"><img src="./icons/check-solid-background.svg">Olympus removed successfully from
|
||||
<i style="margin-left: 3px"><%= activeInstance !== undefined? activeInstance["name"]: "" %></i>!</div>
|
||||
</div>
|
||||
<div class="result-summary error <%= (activeInstance !== undefined && activeInstance["installed"])? "": "hide" %>">
|
||||
<div class="title"><img src="./icons/triangle-exclamation-solid-background.svg">An error occurred while removing Olympus settings from
|
||||
<i style="margin-left: 3px"><%= activeInstance !== undefined? activeInstance["name"]: "" %></i></div>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="content">
|
||||
<div class="instructions">
|
||||
<% if (instances.some(instance => instance.installed)) { %>
|
||||
<span class="title">
|
||||
Change settings
|
||||
</span>
|
||||
<span class="subtitle">
|
||||
Here you can see the DCS instances on your computer that have Olympus installed. <br>
|
||||
You can edit settings and uninstall Olympus from this screen.
|
||||
</span>
|
||||
<% } else { %>
|
||||
<span class="title" style="margin-top: 150px;">
|
||||
No Olympus installs detected
|
||||
</span>
|
||||
<span class="subtitle">
|
||||
Use the <b>Add Olympus</b> option in the main manu to install Olympus in your DCS instance. <br>
|
||||
If you have more than one DCS instance, you will need to add Olympus to each one of them.
|
||||
</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="scroll-container">
|
||||
<div class="scrollable">
|
||||
<% for (let i = 0; i < instances.length; i++) {%>
|
||||
<% if (instances[i].installed) { %>
|
||||
<div class="option <%= instances[i].installed? 'installed': '' %>" data-folder="<%= instances[i].folder %>">
|
||||
<div class="instance-info">
|
||||
<span class="name"><%= instances[i].name %></span>
|
||||
<span class="folder"><img src="./icons/folder-open-solid.svg"> <%= instances[i].folder %></span>
|
||||
<div class="divider"></div>
|
||||
<span class="status <%= instances[i].installed? (instances[i].error? 'error': 'installed'): '' %>">
|
||||
<%= instances[i].installed? (instances[i].error? 'Corrupted/outdated Olympus installation': 'Olympus installed'): 'Olympus not installed' %>
|
||||
</span>
|
||||
<div class="summary">
|
||||
<div class="info">
|
||||
<div>Client port</div>
|
||||
<div> <%= instances[i].installed? instances[i].clientPort: "N/A" %> </div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div>Backend port</div>
|
||||
<div> <%= instances[i].installed? instances[i].backendPort: "N/A" %> </div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div>Backend address</div>
|
||||
<div> <%= instances[i].installed? instances[i].backendAddress: "N/A" %> </div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="logs-link" onclick="
|
||||
signal('onTextFileClicked', '<%= instances[i].folder.replaceAll('\\', '/')+'/Logs/dcs.log' %>');
|
||||
signal('onTextFileClicked', '<%= instances[i].folder.replaceAll('\\', '/')+'/Logs/Olympus_log.txt' %>');
|
||||
">Open logs</div>
|
||||
</div>
|
||||
<div class="instance-buttons">
|
||||
<div class="button edit" onclick="signal('onEditClicked', '<%= instances[i].name %>')">Edit settings</div>
|
||||
<div class="button uninstall" onclick="signal('onUninstallClicked', '<%= instances[i].name %>')">Uninstall Olympus</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
25
manager/ejs/type.ejs
Normal file
@ -0,0 +1,25 @@
|
||||
<style>
|
||||
|
||||
</style>
|
||||
<div>
|
||||
<div class="instructions">
|
||||
<div class="step">
|
||||
Step <%= instances.length === 1? "1": "2" %> of <%= instances.length === 1? "4": "5" %>
|
||||
</div>
|
||||
<div class="title">
|
||||
Do you want to add Olympus for singleplayer or multiplayer?
|
||||
</div>
|
||||
<div class="description">
|
||||
Select singleplayer if you only want to play locally on your own computer. <br>
|
||||
Select multiplayer if you want Olympus to be useable over the internet from a different computer, or this instance is a dedicated server.
|
||||
</div>
|
||||
</div>
|
||||
<div class="wizard-inputs">
|
||||
<div class="button radio singleplayer selected" onclick="signal('onInstallTypeClicked', 'singleplayer')">
|
||||
Singleplayer
|
||||
</div>
|
||||
<div class="button radio multiplayer" onclick="signal('onInstallTypeClicked', 'multiplayer')">
|
||||
Multiplayer
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
68
manager/ejs/welcome.ejs
Normal file
@ -0,0 +1,68 @@
|
||||
<style>
|
||||
#manager-welcome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 20px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#manager-welcome .instructions {
|
||||
width: 40%;
|
||||
text-align: center;
|
||||
color: var(--offwhite);
|
||||
}
|
||||
|
||||
#manager-welcome .option {
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--offwhite);
|
||||
width: 460px;
|
||||
height: 70px;
|
||||
color: var(--offwhite);
|
||||
display: flex;
|
||||
font-size: var(--very-large);
|
||||
font-weight: 600;
|
||||
padding-left: 15px;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
color: var(--offwhite);
|
||||
}
|
||||
|
||||
#manager-welcome .option:hover {
|
||||
color: var(--background);
|
||||
background-color: var(--offwhite);
|
||||
}
|
||||
|
||||
#manager-welcome .option.disabled {
|
||||
pointer-events: none;
|
||||
color: var(--darkgray);
|
||||
border-color: var(--darkgray);
|
||||
}
|
||||
|
||||
#manager-welcome .option * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
<div id="manager-welcome">
|
||||
<div class="instructions" style="font-size: 28px; font-weight: bold;">
|
||||
Do you want to use the Olympus Manager in basic or Expert mode?
|
||||
</div>
|
||||
<div class="instructions" style="color: var(--gray);">
|
||||
Basic mode is recommended for most users. <br>
|
||||
Expert mode is for those who know how Olympus works or for server owners.
|
||||
</div>
|
||||
<div class="instructions" style="color: var(--gray); font-weight: bold;">
|
||||
You can change this setting at any time.
|
||||
</div>
|
||||
<div class="option basic" onclick="signal('onBasicClicked')">
|
||||
Basic mode
|
||||
</div>
|
||||
<div class="option expert" onclick="signal('onExpertClicked')">
|
||||
Expert mode
|
||||
</div>
|
||||
</div>
|
||||
87
manager/ejs/wizard.ejs
Normal file
@ -0,0 +1,87 @@
|
||||
<style>
|
||||
.wizard-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 30px;
|
||||
padding: 60px 120px;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.wizard-page .instructions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 15px;
|
||||
color: var(--offwhite);
|
||||
}
|
||||
|
||||
.wizard-page .instructions .step {
|
||||
font-size: var(--normal);
|
||||
color: var(--lightgray);
|
||||
}
|
||||
|
||||
.wizard-page .instructions .description {
|
||||
font-size: var(--normal);
|
||||
color: var(--lightgray);
|
||||
}
|
||||
|
||||
.wizard-page .instructions .title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.wizard-page .content {
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.wizard-page .content > div {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 20px;
|
||||
align-items: start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wizard-page .wizard-inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
.wizard-page .button.radio {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.wizard-page .note {
|
||||
width: 100%;
|
||||
background-color: var(--background-note);
|
||||
color: var(--offwhite);
|
||||
border-left: 5px solid var(--offwhite);
|
||||
font-size: var(--normal);
|
||||
padding: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wizard-page .warning {
|
||||
background-color: var(--background-warning);
|
||||
border-left: 5px solid var(--orange);
|
||||
}
|
||||
</style>
|
||||
<div class="wizard-page">
|
||||
<div class="cancel" style="font-size: var(--normal); font-weight: 600; color: var(--offwhite); display: flex; align-items: center; column-gap: 10px; cursor: pointer; text-decoration: underline;" onclick="signal('onCancelClicked')">
|
||||
<img src="./icons/chevron-left-solid.svg" style=" height: 14px;"><%= state === 'INSTALL'? "Cancel install": "Cancel editing" %>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
||||
</div>
|
||||
<div class="buttons-footer">
|
||||
<div class="button back" style="color: var(--offwhite); background-color: var(--background); border: 1px solid var(--offwhite);" onclick="signal('onBackClicked')">
|
||||
Back
|
||||
</div>
|
||||
<div class="button next" style="color: var(--background); background-color: var(--offwhite);" onclick="signal('onNextClicked')">
|
||||
Next
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
1
manager/icons/arrow-right-solid.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="#F2F2F2" d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"/></svg>
|
||||
|
After Width: | Height: | Size: 490 B |
1
manager/icons/check-solid-background.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path fill="#181e25" d="M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 449 B |
1
manager/icons/chrome.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="#F2F2F2" d="M0 256C0 209.4 12.5 165.6 34.3 127.1L144.1 318.3C166 357.5 207.9 384 256 384C270.3 384 283.1 381.7 296.8 377.4L220.5 509.6C95.9 492.3 0 385.3 0 256zM365.1 321.6C377.4 302.4 384 279.1 384 256C384 217.8 367.2 183.5 340.7 160H493.4C505.4 189.6 512 222.1 512 256C512 397.4 397.4 511.1 256 512L365.1 321.6zM477.8 128H256C193.1 128 142.3 172.1 130.5 230.7L54.2 98.5C101 38.5 174 0 256 0C350.8 0 433.5 51.5 477.8 128V128zM168 256C168 207.4 207.4 168 256 168C304.6 168 344 207.4 344 256C344 304.6 304.6 344 256 344C207.4 344 168 304.6 168 256z"/></svg>
|
||||
|
After Width: | Height: | Size: 804 B |
1
manager/icons/gamepad-solid.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="20" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="#F2F2F2" d="M192 64C86 64 0 150 0 256S86 448 192 448H448c106 0 192-86 192-192s-86-192-192-192H192zM496 168a40 40 0 1 1 0 80 40 40 0 1 1 0-80zM392 304a40 40 0 1 1 80 0 40 40 0 1 1 -80 0zM168 200c0-13.3 10.7-24 24-24s24 10.7 24 24v32h32c13.3 0 24 10.7 24 24s-10.7 24-24 24H216v32c0 13.3-10.7 24-24 24s-24-10.7-24-24V280H136c-13.3 0-24-10.7-24-24s10.7-24 24-24h32V200z"/></svg>
|
||||
|
After Width: | Height: | Size: 622 B |
BIN
manager/icons/olympus_white.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path opacity="1" fill="#F2F2F2" d="M64 32C28.7 32 0 60.7 0 96v64c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zm280 72a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm48 24a24 24 0 1 1 48 0 24 24 0 1 1 -48 0zM64 288c-35.3 0-64 28.7-64 64v64c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V352c0-35.3-28.7-64-64-64H64zm280 72a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm56 24a24 24 0 1 1 48 0 24 24 0 1 1 -48 0z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="#F2F2F2" d="M64 32C28.7 32 0 60.7 0 96v64c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zm280 72a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm48 24a24 24 0 1 1 48 0 24 24 0 1 1 -48 0zM64 288c-35.3 0-64 28.7-64 64v64c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V352c0-35.3-28.7-64-64-64H64zm280 72a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm56 24a24 24 0 1 1 48 0 24 24 0 1 1 -48 0z"/></svg>
|
||||
|
Before Width: | Height: | Size: 659 B After Width: | Height: | Size: 647 B |
1
manager/icons/triangle-exclamation-solid-background.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path opacity="1" fill="#181e25" d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480H40c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24V296c0 13.3 10.7 24 24 24s24-10.7 24-24V184c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 584 B |
1
manager/icons/triangle-exclamation-solid-orange.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path opacity="1" fill="#ffa500" d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480H40c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24V296c0 13.3 10.7 24 24 24s24-10.7 24-24V184c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 584 B |
@ -20,7 +20,7 @@
|
||||
<button class="title-bar-button maximize"></button>
|
||||
<button class="title-bar-button close"></button>
|
||||
</div>
|
||||
<div id="header">
|
||||
<div id="header" class="hide">
|
||||
<img class="main-icon" src="../img/OlympusLogoFinal_4k.png" \>
|
||||
<div class="version">
|
||||
<div> DCS Olympus Manager</div>
|
||||
@ -28,13 +28,16 @@
|
||||
</div>
|
||||
<div class="link first" data-link="https://github.com/Pax1601/DCSOlympus/wiki/2.-User-Guide">User Guide</div>
|
||||
<div class="link" data-link="https://github.com/Pax1601/DCSOlympus/wiki/Setup-Troubleshooting">Troubleshooting Guide</div>
|
||||
<div id="switch-mode" class="link"> </div>
|
||||
<div style="width: 15px;"></div>
|
||||
<img class="link" data-link="https://github.com/Pax1601/DCSOlympus" src="./icons/github.svg"/>
|
||||
<img class="link" data-link="https://discord.gg/pCfCykAdrw" src="./icons/discord.svg"/>
|
||||
<img class="link" data-link="https://www.youtube.com/@DCSOlympus" src="./icons/youtube.svg"/>
|
||||
<img class="link" onClick="signal('onLinkClicked', 'https://github.com/Pax1601/DCSOlympus')" src="./icons/github.svg" />
|
||||
<img class="link" onClick="signal('onLinkClicked', 'https://discord.gg/pCfCykAdrw')" src="./icons/discord.svg" />
|
||||
<img class="link" onClick="signal('onLinkClicked', 'https://www.youtube.com/@DCSOlympus')" src="./icons/youtube.svg" />
|
||||
</div>
|
||||
<div id="loader" class="manager-page hide">
|
||||
Loading, please wait...
|
||||
<div id="loader" class="manager-page hide" style="opacity: 100%;">
|
||||
<div style="font-weight: bold;">Loading, please wait...</div>
|
||||
<div class="loading-bar" style="width: 400px; height: 15px;"></div>
|
||||
<div class="loading-message" style="font-size: var(--normal); color: var(--lightgray)"></div>
|
||||
</div>
|
||||
<div id="grayout" class="hide"></div>
|
||||
<div id="popup" class="hide">
|
||||
@ -42,7 +45,7 @@
|
||||
<img src="./icons/circle-question-regular.svg" class="confirm">
|
||||
<img src="./icons/spinner-solid.svg" class="wait">
|
||||
<div class="content">
|
||||
|
||||
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="button accept-popup"> Accept </div>
|
||||
@ -77,6 +80,19 @@
|
||||
document.querySelector('.restore').classList.add("hide");
|
||||
document.querySelector('.maximize').classList.remove("hide");
|
||||
})
|
||||
|
||||
function signal(callback, params) {
|
||||
const event = new CustomEvent("signal", { detail: { callback: callback, params: params } });
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
window.addEventListener("click", (ev) => {
|
||||
var buttons = document.querySelectorAll(".button.collapse");
|
||||
for (let button of buttons) {
|
||||
if (button != ev.srcElement)
|
||||
button.classList.remove("open");
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
</html>
|
||||
@ -1,87 +0,0 @@
|
||||
const ManagerPage = require("./managerpage");
|
||||
const ejs = require('ejs')
|
||||
const { logger } = require("./filesystem")
|
||||
|
||||
/** Connections page, allows the user to set the ports and address for each Olympus instance
|
||||
*
|
||||
*/
|
||||
class ConnectionsPage extends ManagerPage {
|
||||
onBackClicked;
|
||||
onNextClicked;
|
||||
onCancelClicked;
|
||||
instance;
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
render(str) {
|
||||
const element = this.getElement();
|
||||
element.innerHTML = str;
|
||||
|
||||
if (this.element.querySelector(".back"))
|
||||
this.element.querySelector(".back").addEventListener("click", (e) => this.onBackClicked(e));
|
||||
|
||||
if (this.element.querySelector(".next"))
|
||||
this.element.querySelector(".next").addEventListener("click", (e) => this.onNextClicked(e));
|
||||
|
||||
if (this.element.querySelector(".cancel"))
|
||||
this.element.querySelector(".cancel").addEventListener("click", (e) => this.onCancelClicked(e));
|
||||
|
||||
this.element.querySelector(".client-port").querySelector("input").addEventListener("change", async (e) => { this.setClientPort(Number(e.target.value)); })
|
||||
this.element.querySelector(".backend-port").querySelector("input").addEventListener("change", async (e) => { this.setBackendPort(Number(e.target.value)); })
|
||||
this.element.querySelector(".backend-address").querySelector("input").addEventListener("change", async (e) => { this.instance.setBackendAddress(e.target.value); })
|
||||
|
||||
super.render();
|
||||
}
|
||||
|
||||
show() {
|
||||
this.instance = this.options.instance;
|
||||
|
||||
ejs.renderFile("./ejs/connections.ejs", this.options, {}, (err, str) => {
|
||||
if (!err) {
|
||||
this.render(str);
|
||||
|
||||
/* Call the port setters to check if the ports are free */
|
||||
this.setClientPort(this.instance.clientPort);
|
||||
this.setBackendPort(this.instance.backendPort);
|
||||
} else {
|
||||
logger.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
super.show();
|
||||
}
|
||||
|
||||
/** Asynchronously check if the client port is free and if it is, set the new value
|
||||
*
|
||||
*/
|
||||
async setClientPort(newPort) {
|
||||
const success = await this.instance.setClientPort(newPort);
|
||||
var successEls = this.element.querySelector(".client-port").querySelectorAll(".success");
|
||||
for (let i = 0; i < successEls.length; i++) {
|
||||
successEls[i].classList.toggle("hide", !success);
|
||||
}
|
||||
var errorEls = this.element.querySelector(".client-port").querySelectorAll(".error");
|
||||
for (let i = 0; i < errorEls.length; i++) {
|
||||
errorEls[i].classList.toggle("hide", success);
|
||||
}
|
||||
}
|
||||
|
||||
/** Asynchronously check if the backend port is free and if it is, set the new value
|
||||
*
|
||||
*/
|
||||
async setBackendPort(newPort) {
|
||||
const success = await this.instance.setBackendPort(newPort);
|
||||
var successEls = this.element.querySelector(".backend-port").querySelectorAll(".success");
|
||||
for (let i = 0; i < successEls.length; i++) {
|
||||
successEls[i].classList.toggle("hide", !success);
|
||||
}
|
||||
var errorEls = this.element.querySelector(".backend-port").querySelectorAll(".error");
|
||||
for (let i = 0; i < errorEls.length; i++) {
|
||||
errorEls[i].classList.toggle("hide", success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConnectionsPage;
|
||||
@ -1,65 +1,114 @@
|
||||
var regedit = require('regedit')
|
||||
const shellFoldersKey = 'HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders'
|
||||
const saveGamesKey = '{4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4}'
|
||||
const { getManager } = require('./managerfactory')
|
||||
var regedit = require('regedit').promisified;
|
||||
var fs = require('fs')
|
||||
var path = require('path')
|
||||
const { checkPort, fetchWithTimeout } = require('./net')
|
||||
const { checkPort, fetchWithTimeout, getFreePort } = require('./net')
|
||||
const dircompare = require('dir-compare');
|
||||
const { spawn } = require('child_process');
|
||||
const find = require('find-process');
|
||||
const { uninstallInstance } = require('./filesystem')
|
||||
const { showErrorPopup, showConfirmPopup } = require('./popup')
|
||||
const { installHooks, installMod, installJSON, applyConfiguration, installShortCuts, deleteMod, deleteHooks, deleteJSON, deleteShortCuts } = require('./filesystem')
|
||||
const { showErrorPopup, showConfirmPopup, showWaitLoadingPopup, setPopupLoadingProgress } = require('./popup')
|
||||
const { logger } = require("./filesystem")
|
||||
const { hidePopup } = require('./popup');
|
||||
const { sleep } = require('./utils');
|
||||
|
||||
const shellFoldersKey = 'HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders'
|
||||
const saveGamesKey = '{4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4}'
|
||||
|
||||
class DCSInstance {
|
||||
static instances = null;
|
||||
|
||||
/** Static asynchronous method to retrieve all DCS instances. Only runs at startup
|
||||
/** Static asynchronous method to retrieve all DCS instances. Only runs at startup, later calls will serve the cached result
|
||||
*
|
||||
* @returns The list of DCS instances
|
||||
*/
|
||||
static async getInstances(force = false) {
|
||||
if (this.instances === null || force)
|
||||
DCSInstance.instances = this.findInstances();
|
||||
return DCSInstance.instances;
|
||||
}
|
||||
|
||||
/** Static asynchronous method to reload all DCS instances. It will not detect any new instance, but it will determine the
|
||||
* installation status of the existing instances.
|
||||
*
|
||||
*/
|
||||
static async getInstances() {
|
||||
if (this.instances === null) {
|
||||
this.instances = await this.findInstances();
|
||||
static async reloadInstances() {
|
||||
var instances = await this.getInstances();
|
||||
for (let instance of instances) {
|
||||
await instance.checkInstallation();
|
||||
}
|
||||
return this.instances;
|
||||
}
|
||||
|
||||
/** Static asynchronous method to find all existing DCS instances
|
||||
*
|
||||
* @returns The list of found DCS instances
|
||||
*/
|
||||
static async findInstances() {
|
||||
let promise = new Promise((res, rej) => {
|
||||
/* Get the Saved Games folder from the registry */
|
||||
regedit.list(shellFoldersKey, function (err, result) {
|
||||
if (err) {
|
||||
rej(err);
|
||||
/* Get the Saved Games folder from the registry */
|
||||
getManager().setLoadingProgress("Finding DCS instances...");
|
||||
|
||||
var result = await regedit.list(shellFoldersKey);
|
||||
/* Check that the registry read was successfull */
|
||||
if (result[shellFoldersKey] !== undefined && result[shellFoldersKey]["exists"] && result[shellFoldersKey]['values'][saveGamesKey] !== undefined && result[shellFoldersKey]['values'][saveGamesKey]['value'] !== undefined) {
|
||||
/* Read all the folders in Saved Games */
|
||||
const searchpath = result[shellFoldersKey]['values'][saveGamesKey]['value'];
|
||||
var folders = fs.readdirSync(searchpath).map((folder) => {return path.join(searchpath, folder);});
|
||||
var instances = [];
|
||||
folders = folders.concat(getManager().getAdditionalDCSInstances());
|
||||
|
||||
/* A DCS Instance is created if either the appsettings.lua or serversettings.lua file is detected */
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
const folder = folders[i];
|
||||
if (fs.existsSync(path.join(folder, "Config", "appsettings.lua")) || fs.existsSync(path.join(folder, "Config", "serversettings.lua")) || getManager().getAdditionalDCSInstances().includes(folder)) {
|
||||
logger.log(`Found instance in ${folder}, checking for Olympus`)
|
||||
var newInstance = new DCSInstance(path.join(folder));
|
||||
|
||||
/* Check if Olympus is already installed */
|
||||
getManager().setLoadingProgress(`Found instance in ${folder}, checking for Olympus...`, (i + 1) / folders.length * 100);
|
||||
await newInstance.checkInstallation();
|
||||
instances.push(newInstance);
|
||||
}
|
||||
else {
|
||||
/* Check that the registry read was successfull */
|
||||
if (result[shellFoldersKey] !== undefined && result[shellFoldersKey]["exists"] && result[shellFoldersKey]['values'][saveGamesKey] !== undefined && result[shellFoldersKey]['values'][saveGamesKey]['value'] !== undefined) {
|
||||
/* Read all the folders in Saved Games */
|
||||
const searchpath = result[shellFoldersKey]['values'][saveGamesKey]['value'];
|
||||
const folders = fs.readdirSync(searchpath);
|
||||
var instances = [];
|
||||
}
|
||||
} else {
|
||||
logger.error("An error occured while trying to fetch the location of the DCS instances.")
|
||||
showErrorPopup(`<div class='main-message'>An error occured while trying to fetch the location of the DCS instances. </div><div class='sub-message'> You can find more info in ${getManager().getLogLocation()} </div>`);
|
||||
}
|
||||
getManager().setLoadingProgress(`All DCS instances found!`, 100);
|
||||
|
||||
/* A DCS Instance is created if either the appsettings.lua or serversettings.lua file is detected */
|
||||
folders.forEach((folder) => {
|
||||
if (fs.existsSync(path.join(searchpath, folder, "Config", "appsettings.lua")) ||
|
||||
fs.existsSync(path.join(searchpath, folder, "Config", "serversettings.lua"))) {
|
||||
instances.push(new DCSInstance(path.join(searchpath, folder)));
|
||||
}
|
||||
})
|
||||
return instances;
|
||||
}
|
||||
|
||||
res(instances);
|
||||
} else {
|
||||
logger.error("An error occured while trying to fetch the location of the DCS instances.")
|
||||
rej("An error occured while trying to fetch the location of the DCS instances.");
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
/** Asynchronously fixes/updates all the instances by deleting the existing installation and the copying over the clean files
|
||||
*
|
||||
*/
|
||||
static async fixInstances() {
|
||||
showWaitLoadingPopup("Please wait while your instances are being fixed.")
|
||||
const instancesToFix = (await DCSInstance.getInstances()).filter((instance) => { return instance.installed && instance.error; });
|
||||
setPopupLoadingProgress(`Fixing Olympus instances`, 0);
|
||||
|
||||
return promise;
|
||||
for (let i = 0; i < instancesToFix.length; i++) {
|
||||
const instance = instancesToFix[i];
|
||||
logger.log(`Fixing Olympus in ${instance.folder}`)
|
||||
|
||||
setPopupLoadingProgress(`Deleting mod folder in ${instance.folder}...`, (i * 4 + 1) / (instancesToFix.length * 4) * 100);
|
||||
await sleep(100);
|
||||
await deleteMod(instance.folder, instance.name);
|
||||
|
||||
setPopupLoadingProgress(`Deleting hook scripts in ${instance.folder}...`, (i * 4 + 2) / (instancesToFix.length * 4) * 100);
|
||||
await sleep(100);
|
||||
await deleteHooks(instance.folder);
|
||||
|
||||
setPopupLoadingProgress(`Installing mod folder in ${instance.folder}...`, (i * 4 + 3) / (instancesToFix.length * 4) * 100);
|
||||
await sleep(100);
|
||||
await installMod(instance.folder, instance.name);
|
||||
|
||||
setPopupLoadingProgress(`Installing hook scripts in ${instance.folder}...`, (i * 4 + 4) / (instancesToFix.length * 4) * 100);
|
||||
await sleep(100);
|
||||
await installHooks(instance.folder);
|
||||
}
|
||||
|
||||
setPopupLoadingProgress(`All instances fixed!`, 100);
|
||||
await sleep(100);
|
||||
}
|
||||
|
||||
folder = "";
|
||||
@ -78,38 +127,73 @@ class DCSInstance {
|
||||
missionTime = "";
|
||||
load = 0;
|
||||
fps = 0;
|
||||
installationType = 'singleplayer';
|
||||
connectionsType = 'auto';
|
||||
gameMasterPasswordEdited = false;
|
||||
blueCommanderPasswordEdited = false;
|
||||
redCommanderPasswordEdited = false;
|
||||
|
||||
constructor(folder) {
|
||||
this.folder = folder;
|
||||
this.name = path.basename(folder);
|
||||
|
||||
/* Periodically "ping" Olympus to check if either the client or the backend are active */
|
||||
window.setInterval(async () => {
|
||||
await this.getData();
|
||||
getManager().updateInstances();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/** Asynchronously checks if Olympus is installed in a DCS instance and compares the contents of package with the installation
|
||||
*
|
||||
* @returns true if the instance has any error or is outdated
|
||||
*/
|
||||
async checkInstallation() {
|
||||
/* Reset values */
|
||||
this.installed = false;
|
||||
this.error = false;
|
||||
this.installationType = 'singleplayer';
|
||||
this.connectionsType = 'auto';
|
||||
|
||||
/* Check if the olympus.json file is detected. If true, Olympus is considered to be installed */
|
||||
if (fs.existsSync(path.join(folder, "Config", "olympus.json"))) {
|
||||
if (fs.existsSync(path.join(this.folder, "Config", "olympus.json"))) {
|
||||
|
||||
getManager().setLoadingProgress(`Olympus installed in ${this.folder}`);
|
||||
try {
|
||||
/* Read the olympus.json */
|
||||
var config = JSON.parse(fs.readFileSync(path.join(folder, "Config", "olympus.json")));
|
||||
var config = JSON.parse(fs.readFileSync(path.join(this.folder, "Config", "olympus.json")));
|
||||
this.clientPort = config["client"]["port"];
|
||||
this.backendPort = config["server"]["port"];
|
||||
this.backendAddress = config["server"]["address"];
|
||||
this.gameMasterPasswordHash = config["authentication"]["gameMasterPassword"];
|
||||
|
||||
this.gameMasterPasswordEdited = false;
|
||||
this.blueCommanderPasswordEdited = false;
|
||||
this.redCommanderPasswordEdited = false;
|
||||
|
||||
} catch (err) {
|
||||
showErrorPopup(`<div class='main-message'>A critical error has occurred while reading your Olympus configuration file. </div><div class='sub-message'> Please, manually reinstall olympus in ${this.folder}. </div>`)
|
||||
logger.error(err)
|
||||
}
|
||||
|
||||
/* Compare the contents of the installed Olympus instance and the one in the root folder. Exclude the databases folder, which users can edit.
|
||||
If there is any difference, the instance is flagged as either corrupted or outdated */
|
||||
this.installed = true;
|
||||
const options = {
|
||||
const options = {
|
||||
compareContent: true,
|
||||
excludeFilter: "databases, mods.lua"
|
||||
};
|
||||
};
|
||||
var err1 = true;
|
||||
var err2 = true;
|
||||
var res1;
|
||||
var res2;
|
||||
try {
|
||||
res1 = dircompare.compareSync(path.join("..", "mod"), path.join(folder, "Mods", "Services", "Olympus"), options);
|
||||
res2 = dircompare.compareSync(path.join("..", "scripts", "OlympusHook.lua"), path.join(folder, "Scripts", "Hooks", "OlympusHook.lua"), options);
|
||||
logger.log(`Comparing Mods content in ${this.folder}`)
|
||||
getManager().setLoadingProgress(`Comparing Mods content in ${this.folder}`);
|
||||
res1 = await dircompare.compare(path.join("..", "mod"), path.join(this.folder, "Mods", "Services", "Olympus"), options);
|
||||
logger.log(`Comparing Scripts content in ${this.folder}`)
|
||||
getManager().setLoadingProgress(`Comparing Scripts content in ${this.folder}`);
|
||||
res2 = await dircompare.compareSync(path.join("..", "scripts", "OlympusHook.lua"), path.join(this.folder, "Scripts", "Hooks", "OlympusHook.lua"), options);
|
||||
err1 = res1.differences !== 0;
|
||||
err2 = res2.differences !== 0;
|
||||
} catch (e) {
|
||||
@ -118,71 +202,39 @@ class DCSInstance {
|
||||
|
||||
if (err1 || err2) {
|
||||
this.error = true;
|
||||
getManager().setLoadingProgress(`Differences found in ${this.folder}`);
|
||||
logger.log("Differences found!")
|
||||
} else {
|
||||
getManager().setLoadingProgress(`No differences found in ${this.folder}`);
|
||||
}
|
||||
} else {
|
||||
this.installed = false;
|
||||
this.error = false;
|
||||
}
|
||||
|
||||
/* Periodically "ping" Olympus to check if either the client or the backend are active */
|
||||
window.setInterval(async () => {
|
||||
await this.getData();
|
||||
|
||||
var page = document.getElementById("manager-instances");
|
||||
if (page) {
|
||||
var instanceDivs = page.querySelectorAll(`.option`);
|
||||
for (let i = 0; i < instanceDivs.length; i++) {
|
||||
if (instanceDivs[i].dataset.folder == this.folder) {
|
||||
var instanceDiv = instanceDivs[i];
|
||||
if (instanceDiv.querySelector(".webserver.online") !== null) {
|
||||
instanceDiv.querySelector(".webserver.online").classList.toggle("hide", !this.webserverOnline)
|
||||
instanceDiv.querySelector(".webserver.offline").classList.toggle("hide", this.webserverOnline)
|
||||
instanceDiv.querySelector(".backend.online").classList.toggle("hide", !this.backendOnline)
|
||||
instanceDiv.querySelector(".backend.offline").classList.toggle("hide", this.backendOnline)
|
||||
|
||||
if (this.backendOnline) {
|
||||
instanceDiv.querySelector(".fps .data").innerText = this.fps;
|
||||
instanceDiv.querySelector(".load .data").innerText = this.load;
|
||||
}
|
||||
|
||||
instanceDiv.querySelector(".button.start").classList.toggle("hide", this.webserverOnline)
|
||||
instanceDiv.querySelector(".button.uninstall").classList.toggle("hide", this.webserverOnline)
|
||||
instanceDiv.querySelector(".button.edit").classList.toggle("hide", this.webserverOnline)
|
||||
instanceDiv.querySelector(".button.open-browser").classList.toggle("hide", !this.webserverOnline)
|
||||
instanceDiv.querySelector(".button.stop").classList.toggle("hide", !this.webserverOnline)
|
||||
|
||||
if (this.webserverOnline)
|
||||
instanceDiv.querySelector(".button.start").classList.remove("loading")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
return this.error;
|
||||
}
|
||||
|
||||
/** Asynchronously check if the client port is free and if it is, set the new value
|
||||
/** Set the client port
|
||||
*
|
||||
* @param {Number} newPort The new client port to set
|
||||
*/
|
||||
async setClientPort(newPort) {
|
||||
if (await this.checkClientPort(newPort)) {
|
||||
logger.log(`Instance ${this.folder} client port set to ${newPort}`)
|
||||
this.clientPort = newPort;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
setClientPort(newPort) {
|
||||
logger.log(`Instance ${this.folder} client port set to ${newPort}`)
|
||||
this.clientPort = newPort;
|
||||
}
|
||||
|
||||
/** Asynchronously check if the client port is free and if it is, set the new value
|
||||
/** Set the backend port
|
||||
*
|
||||
* @param {Number} newPort The new backend port to set
|
||||
*/
|
||||
async setBackendPort(newPort) {
|
||||
if (await this.checkBackendPort(newPort)) {
|
||||
logger.log(`Instance ${this.folder} client port set to ${newPort}`)
|
||||
this.backendPort = newPort;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
setBackendPort(newPort) {
|
||||
logger.log(`Instance ${this.folder} backend port set to ${newPort}`)
|
||||
this.backendPort = newPort;
|
||||
}
|
||||
|
||||
/** Set backend address
|
||||
*
|
||||
* @param {String} newAddress The new backend address to set
|
||||
*/
|
||||
setBackendAddress(newAddress) {
|
||||
this.backendAddress = newAddress;
|
||||
@ -190,91 +242,151 @@ class DCSInstance {
|
||||
|
||||
/** Set Game Master password
|
||||
*
|
||||
* @param {String} newPassword The new Game Master password to set
|
||||
*/
|
||||
setGameMasterPassword(newPassword) {
|
||||
this.gameMasterPassword = newPassword;
|
||||
this.gameMasterPasswordEdited = true;
|
||||
}
|
||||
|
||||
/** Set Blue Commander password
|
||||
*
|
||||
* @param {String} newAddress The new Blue Commander password to set
|
||||
*/
|
||||
setBlueCommanderPassword(newPassword) {
|
||||
this.blueCommanderPassword = newPassword;
|
||||
this.blueCommanderPasswordEdited = true;
|
||||
}
|
||||
|
||||
/** Set Red Commander password
|
||||
*
|
||||
* @param {String} newAddress The new Red Commander password to set
|
||||
*/
|
||||
setRedCommanderPassword(newPassword) {
|
||||
this.redCommanderPassword = newPassword;
|
||||
this.redCommanderPasswordEdited = true;
|
||||
}
|
||||
|
||||
/** Check if the client port is free
|
||||
/** Checks if any password has been edited by the user
|
||||
*
|
||||
* @returns true if any password was edited
|
||||
*/
|
||||
arePasswordsEdited() {
|
||||
return (getManager().getActiveInstance().gameMasterPasswordEdited || getManager().getActiveInstance().blueCommanderPasswordEdited || getManager().getActiveInstance().redCommanderPasswordEdited);
|
||||
}
|
||||
|
||||
/** Checks if all the passwords have been set by the user
|
||||
*
|
||||
* @returns true if all the password have been set
|
||||
*/
|
||||
arePasswordsSet() {
|
||||
return !(getManager().getActiveInstance().gameMasterPassword === '' || getManager().getActiveInstance().blueCommanderPassword === '' || getManager().getActiveInstance().redCommanderPassword === '');
|
||||
}
|
||||
|
||||
/** Checks if all the passwords are different
|
||||
*
|
||||
* @returns true if all the passwords are different
|
||||
*/
|
||||
arePasswordsDifferent() {
|
||||
return !(getManager().getActiveInstance().gameMasterPassword === getManager().getActiveInstance().blueCommanderPassword || getManager().getActiveInstance().gameMasterPassword === getManager().getActiveInstance().redCommanderPassword || getManager().getActiveInstance().blueCommanderPassword === getManager().getActiveInstance().redCommanderPassword);
|
||||
}
|
||||
|
||||
/** Asynchronously check if the client port is free
|
||||
*
|
||||
* @param {Number | undefined} port The port to check. If not set, the current clientPort will be checked
|
||||
* @returns true if the client port is free
|
||||
*/
|
||||
async checkClientPort(port) {
|
||||
var promise = new Promise((res, rej) => {
|
||||
checkPort(port, async (portFree) => {
|
||||
if (portFree) {
|
||||
portFree = !(await DCSInstance.getInstances()).some((instance) => {
|
||||
if (instance !== this && instance.installed) {
|
||||
if (instance.clientPort === port || instance.backendPort === port) {
|
||||
logger.log(`Port ${port} already selected by other instance`);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (instance.backendPort === port) {
|
||||
logger.log(`Port ${port} equal to backend port`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
port = port ?? this.clientPort;
|
||||
|
||||
logger.log(`Checking client port ${port}`);
|
||||
var portFree = await checkPort(port);
|
||||
if (portFree) {
|
||||
portFree = !(await DCSInstance.getInstances()).some((instance) => {
|
||||
if (instance !== this && instance.installed) {
|
||||
if (instance.clientPort === port || instance.backendPort === port) {
|
||||
logger.log(`Client port ${port} already selected by other instance`);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (instance.backendPort === port) {
|
||||
logger.log(`Client port ${port} equal to backend port`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.log(`Port ${port} currently in use`);
|
||||
}
|
||||
res(portFree);
|
||||
return false;
|
||||
})
|
||||
})
|
||||
return promise;
|
||||
}
|
||||
else {
|
||||
logger.log(`Client port ${port} currently in use`);
|
||||
}
|
||||
return portFree;
|
||||
}
|
||||
|
||||
/** Check if the backend port is free
|
||||
/** Asynchronously check if the backend port is free
|
||||
*
|
||||
* @param {Number | undefined} port The port to check. If not set, the current backendPort will be checked
|
||||
* @returns true if the backend port is free
|
||||
*/
|
||||
async checkBackendPort(port) {
|
||||
var promise = new Promise((res, rej) => {
|
||||
checkPort(port, async (portFree) => {
|
||||
if (portFree) {
|
||||
portFree = !(await DCSInstance.getInstances()).some((instance) => {
|
||||
if (instance !== this && instance.installed) {
|
||||
if (instance.clientPort === port || instance.backendPort === port) {
|
||||
logger.log(`Port ${port} already selected by other instance`);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (instance.clientPort === port) {
|
||||
logger.log(`Port ${port} equal to client port`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
port = port ?? this.backendPort;
|
||||
|
||||
logger.log(`Checking backend port ${port}`);
|
||||
var portFree = await checkPort(port);
|
||||
if (portFree) {
|
||||
portFree = !(await DCSInstance.getInstances()).some((instance) => {
|
||||
if (instance !== this && instance.installed) {
|
||||
if (instance.clientPort === port || instance.backendPort === port) {
|
||||
logger.log(`Backend port ${port} already selected by other instance`);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
logger.log(`Port ${port} currently in use`);
|
||||
if (instance.clientPort === port) {
|
||||
logger.log(`Backend port ${port} equal to client port`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
res(portFree);
|
||||
return false;
|
||||
})
|
||||
})
|
||||
return promise;
|
||||
} else {
|
||||
logger.log(`Backend port ${port} currently in use`);
|
||||
}
|
||||
return portFree;
|
||||
}
|
||||
|
||||
/** Asynchronously find free client and backend ports. If the old ports are free, it will keep them.
|
||||
*
|
||||
*/
|
||||
async findFreePorts() {
|
||||
logger.log(`Looking for free ports`);
|
||||
if (await this.checkClientPort() && await this.checkBackendPort()) {
|
||||
logger.log("Old ports are free, keeping them")
|
||||
} else {
|
||||
logger.log(`Finding new free ports`);
|
||||
|
||||
const instances = await DCSInstance.getInstances();
|
||||
const firstPort = instances.map((instance) => { return instance.clientPort; }).concat(instances.map((instance) => { return instance.backendPort; })).sort().at(-1) + 1;
|
||||
|
||||
var clientPort = await getFreePort(firstPort);
|
||||
if (clientPort === false)
|
||||
rej("Unable to find a free client port");
|
||||
logger.log(`Found free client port ${clientPort}`);
|
||||
|
||||
var backendPort = await getFreePort(clientPort + 1);
|
||||
if (backendPort === false)
|
||||
rej("Unable to find a free backend port");
|
||||
logger.log(`Found free backend port ${backendPort}`);
|
||||
|
||||
this.clientPort = clientPort;
|
||||
this.backendPort = backendPort;
|
||||
}
|
||||
}
|
||||
|
||||
/** Asynchronously interrogate the webserver and the backend to check if they are active and to retrieve data.
|
||||
*
|
||||
*/
|
||||
async getData() {
|
||||
if (this.installed && !this.error) {
|
||||
if (this.installed) {
|
||||
fetchWithTimeout(`http://localhost:${this.clientPort}`, { timeout: 250 })
|
||||
.then(async (response) => {
|
||||
this.webserverOnline = (await response.text()).includes("Olympus");
|
||||
@ -343,7 +455,9 @@ class DCSInstance {
|
||||
sub.unref();
|
||||
}
|
||||
|
||||
/* Stop any node process running on the server port. This will stop either the server or the client depending on what is running */
|
||||
/** Stop any node process running on the server port. This will stop either the server or the client depending on what is running
|
||||
*
|
||||
*/
|
||||
stop() {
|
||||
find('port', this.clientPort)
|
||||
.then((list) => {
|
||||
@ -368,20 +482,137 @@ class DCSInstance {
|
||||
})
|
||||
}
|
||||
|
||||
/* Uninstall this instance */
|
||||
uninstall() {
|
||||
showConfirmPopup("Are you sure you want to completely remove this Olympus installation?", () =>
|
||||
uninstallInstance(this.folder, this.name).then(
|
||||
() => {
|
||||
location.reload();
|
||||
},
|
||||
(err) => {
|
||||
logger.error(err)
|
||||
showErrorPopup(`An error has occurred while uninstalling the Olympus instance. Make sure Olympus and DCS are not running. <br><br> You can find more info in ${path.join(__dirname, "..", "manager.log")}`, () => {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
));
|
||||
/** Edit this instance
|
||||
*
|
||||
*/
|
||||
async edit() {
|
||||
showWaitLoadingPopup(`<span>Please wait while Olympus is being edited in <i>${this.name}</i></span>`);
|
||||
try {
|
||||
setPopupLoadingProgress("Applying configuration...", 0);
|
||||
await sleep(500);
|
||||
await applyConfiguration(getManager().getActiveInstance().folder, getManager().getActiveInstance());
|
||||
|
||||
setPopupLoadingProgress("Editing completed!", 100);
|
||||
await sleep(1500);
|
||||
logger.log(`Editing completed successfully`);
|
||||
hidePopup();
|
||||
|
||||
getManager().getMode() === "basic"? getManager().settingsPage.show(): getManager().instancesPage.show();
|
||||
} catch (err) {
|
||||
logger.log(`An error occurred during editing: ${err}`);
|
||||
getManager().getActiveInstance().error = true;
|
||||
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${getManager().getLogLocation()} for more info. </div>`)
|
||||
getManager().getMode() === "basic"? getManager().settingsPage.show(): getManager().instancesPage.show();
|
||||
}
|
||||
}
|
||||
|
||||
/** Install this instance
|
||||
*
|
||||
*/
|
||||
async install() {
|
||||
showWaitLoadingPopup(`<span>Please wait while Olympus is being installed in <i>${this.name}</i></span>`);
|
||||
try {
|
||||
getManager().activePage.hide();
|
||||
setPopupLoadingProgress("Installing hook scripts...", 0);
|
||||
await sleep(100);
|
||||
await installHooks(getManager().getActiveInstance().folder);
|
||||
|
||||
setPopupLoadingProgress("Installing mod folder...", 20);
|
||||
await sleep(100);
|
||||
await installMod(getManager().getActiveInstance().folder, getManager().getActiveInstance().name);
|
||||
|
||||
setPopupLoadingProgress("Installing JSON file...", 40);
|
||||
await sleep(100);
|
||||
await installJSON(getManager().getActiveInstance().folder);
|
||||
|
||||
setPopupLoadingProgress("Applying configuration...", 60);
|
||||
await sleep(100);
|
||||
await applyConfiguration(getManager().getActiveInstance().folder, getManager().getActiveInstance());
|
||||
|
||||
setPopupLoadingProgress("Creating shortcuts...", 80);
|
||||
await sleep(100);
|
||||
await installShortCuts(getManager().getActiveInstance().folder, getManager().getActiveInstance().name);
|
||||
|
||||
setPopupLoadingProgress("Installation completed!", 100);
|
||||
await sleep(500);
|
||||
logger.log(`Installation completed successfully`);
|
||||
hidePopup();
|
||||
if (getManager().getMode() === 'basic') {
|
||||
getManager().resultPage.show();
|
||||
getManager().resultPage.getElement().querySelector(".result-summary.success").classList.remove("hide");
|
||||
getManager().resultPage.getElement().querySelector(".result-summary.error").classList.add("hide");
|
||||
getManager().resultPage.getElement().querySelector(".instructions-group").classList.remove("hide");
|
||||
} else {
|
||||
await getManager().reload();
|
||||
getManager().instancesPage.show();
|
||||
}
|
||||
} catch (err) {
|
||||
logger.log(`An error occurred during installation: ${err}`);
|
||||
hidePopup();
|
||||
if (getManager().getMode() === 'basic') {
|
||||
getManager().resultPage.show();
|
||||
getManager().resultPage.getElement().querySelector(".result-summary.success").classList.add("hide");
|
||||
getManager().resultPage.getElement().querySelector(".result-summary.error").classList.remove("hide");
|
||||
} else {
|
||||
await getManager().reload();
|
||||
getManager().instancesPage.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Uninstall this instance
|
||||
*
|
||||
*/
|
||||
async uninstall() {
|
||||
showConfirmPopup(`<div class='main-message'> Are you sure you want to remove Olympus from ${this.name}? </div> <div class='sub-message'>This will only remove Olympus for this particular DCS instance.</div>`, async () => {
|
||||
try {
|
||||
getManager().activePage.hide();
|
||||
logger.log(`Uninstalling Olympus from ${this.folder}`)
|
||||
await sleep(300);
|
||||
showWaitLoadingPopup(`<span>Please wait while Olympus is being removed from <i>${this.name}</i></span>`);
|
||||
setPopupLoadingProgress("Deleting mod folder...", 0);
|
||||
await sleep(100);
|
||||
await deleteMod(this.folder, this.name);
|
||||
|
||||
setPopupLoadingProgress("Deleting hook scripts...", 25);
|
||||
await sleep(100);
|
||||
await deleteHooks(this.folder);
|
||||
|
||||
setPopupLoadingProgress("Deleting JSON...", 50);
|
||||
await sleep(100);
|
||||
await deleteJSON(this.folder);
|
||||
|
||||
setPopupLoadingProgress("Deleting shortcuts...", 75);
|
||||
await sleep(100);
|
||||
await deleteShortCuts(this.folder, this.name);
|
||||
|
||||
await sleep(500);
|
||||
setPopupLoadingProgress("Instance removed!", 100);
|
||||
logger.log(`Olympus removed from ${this.folder}`)
|
||||
|
||||
hidePopup();
|
||||
await getManager().reload();
|
||||
if (getManager().getMode() === 'basic')
|
||||
getManager().settingsPage.show();
|
||||
else
|
||||
getManager().instancesPage.show();
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
|
||||
/* Nested popup calls need to wait for animation to complete */
|
||||
await sleep(300);
|
||||
showErrorPopup(`<div class='main-message'>An error has occurred while uninstalling the Olympus instance. </div><div class='sub-message'> Make sure Olympus and DCS are not running. </div><div class='sub-message'>You can find more info in ${path.join(__dirname, "..", "manager.log")} </div>`, () => {
|
||||
if (getManager().getMode() === 'basic')
|
||||
getManager().settingsPage.show();
|
||||
else
|
||||
getManager().instancesPage.show();
|
||||
});
|
||||
}
|
||||
}, () => {
|
||||
getManager().setState('IDLE');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,315 +1,234 @@
|
||||
const sha256 = require('sha256')
|
||||
const createShortcut = require('create-desktop-shortcuts');
|
||||
const fs = require('fs');
|
||||
const fsp = require('fs').promises;
|
||||
const path = require('path');
|
||||
const { showWaitPopup } = require('./popup');
|
||||
const { Console } = require('console');
|
||||
const homeDir = require('os').homedir();
|
||||
|
||||
var output = fs.createWriteStream('./manager.log', {flags: 'a'});
|
||||
var output = fs.createWriteStream('./manager.log', { flags: 'a' });
|
||||
var logger = new Console(output, output);
|
||||
const date = new Date();
|
||||
output.write(` ======================= New log starting at ${date.toString()} =======================\n`);
|
||||
|
||||
/** Conveniency function to asynchronously delete a single file, with error catching
|
||||
*
|
||||
* @param {String} filePath The path to the file to delete
|
||||
*/
|
||||
async function deleteFile(filePath) {
|
||||
logger.log(`Deleting ${filePath}`);
|
||||
var promise = new Promise((res, rej) => {
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.rm(filePath, (err) => {
|
||||
if (err) {
|
||||
logger.error(`Error removing ${filePath}: ${err}`)
|
||||
rej(err);
|
||||
}
|
||||
else {
|
||||
logger.log(`Removed ${filePath}`)
|
||||
res(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
res(true);
|
||||
}
|
||||
})
|
||||
return promise;
|
||||
if (await exists(filePath) && await fsp.rm(filePath))
|
||||
logger.log(`Removed ${filePath}`);
|
||||
else
|
||||
logger.log(`${filePath} does not exist, nothing to do`);
|
||||
}
|
||||
|
||||
/** Given a list of Olympus instances, it fixes/updates them by deleting the existing installation and the copying over the clean files
|
||||
/** Conveniency function to asynchronously check if a file or folder exists
|
||||
*
|
||||
* @param {*} location Path to the folder or file to check for existance
|
||||
* @returns true if file exists, false if it doesn't
|
||||
*/
|
||||
async function fixInstances(instances) {
|
||||
var promise = new Promise((res, rej) => {
|
||||
var instancePromises = instances.map((instance) => {
|
||||
var instancePromise = new Promise((instanceRes, instanceErr) => {
|
||||
logger.log(`Fixing Olympus in ${instance.folder}`)
|
||||
deleteMod(instance.folder, instance.name)
|
||||
.then(() => deleteHooks(instance.folder), (err) => { return Promise.reject(err); })
|
||||
.then(() => installMod(instance.folder, instance.name), (err) => { return Promise.reject(err); })
|
||||
.then(() => installHooks(instance.folder), (err) => { return Promise.reject(err); })
|
||||
.then(() => installShortCuts(instance.folder, instance.name), (err) => { return Promise.reject(err); })
|
||||
.then(() => instanceRes(true), (err) => { instanceErr(err) })
|
||||
})
|
||||
return instancePromise;
|
||||
});
|
||||
Promise.all(instancePromises).then(() => res(true), (err) => { rej(err) });
|
||||
})
|
||||
return promise;
|
||||
async function exists(location) {
|
||||
try {
|
||||
await fsp.stat(location);
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return false
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Uninstalls a specific instance given its folder
|
||||
*
|
||||
*/
|
||||
async function uninstallInstance(folder, name) {
|
||||
logger.log(`Uninstalling Olympus from ${folder}`)
|
||||
showWaitPopup("Please wait while the Olympus installation is being uninstalled.")
|
||||
var promise = new Promise((res, rej) => {
|
||||
deleteMod(folder, name)
|
||||
.then(() => deleteHooks(folder), (err) => { return Promise.reject(err); })
|
||||
.then(() => deleteJSON(folder), (err) => { return Promise.reject(err); })
|
||||
.then(() => deleteShortCuts(folder, name), (err) => { return Promise.reject(err); })
|
||||
.then(() => res(true), (err) => { rej(err) });
|
||||
})
|
||||
return promise;
|
||||
}
|
||||
|
||||
/** Installs the Hooks script
|
||||
/** Asynchronously installs the Hooks script
|
||||
*
|
||||
* @param {String} folder The base Saved Games folder where the hooks scripts should be installed
|
||||
*/
|
||||
async function installHooks(folder) {
|
||||
logger.log(`Installing hooks in ${folder}`)
|
||||
var promise = new Promise((res, rej) => {
|
||||
fs.cp(path.join("..", "scripts", "OlympusHook.lua"), path.join(folder, "Scripts", "Hooks", "OlympusHook.lua"), (err) => {
|
||||
if (err) {
|
||||
logger.log(`Error installing hooks in ${folder}: ${err}`)
|
||||
rej(err);
|
||||
}
|
||||
else {
|
||||
logger.log(`Hooks succesfully installed in ${folder}`)
|
||||
res(true);
|
||||
}
|
||||
});
|
||||
})
|
||||
return promise;
|
||||
await fsp.cp(path.join("..", "scripts", "OlympusHook.lua"), path.join(folder, "Scripts", "Hooks", "OlympusHook.lua"));
|
||||
logger.log(`Hooks succesfully installed in ${folder}`)
|
||||
}
|
||||
|
||||
/** Installs the Mod folder
|
||||
/** Asynchronously installs the Mod folder
|
||||
*
|
||||
* @param {String} folder The base Saved Games folder where the mod folder should be installed
|
||||
* @param {String} name The name of the current DCS Instance, used to create backups of user created files
|
||||
*/
|
||||
|
||||
async function installMod(folder, name) {
|
||||
logger.log(`Installing mod in ${folder}`)
|
||||
var promise = new Promise((res, rej) => {
|
||||
fs.cp(path.join("..", "mod"), path.join(folder, "Mods", "Services", "Olympus"), { recursive: true }, (err) => {
|
||||
if (err) {
|
||||
logger.log(`Error installing mod in ${folder}: ${err}`)
|
||||
rej(err);
|
||||
}
|
||||
else {
|
||||
logger.log(`Mod succesfully installed in ${folder}`)
|
||||
|
||||
/* Check if backup user-editable files exist. If true copy them over */
|
||||
try {
|
||||
logger.log(__dirname)
|
||||
logger.log(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"));
|
||||
if (fs.existsSync(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"))) {
|
||||
logger.log("Backup databases found, copying over");
|
||||
fs.cpSync(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"), path.join(folder, "Mods", "Services", "Olympus", "databases"), {recursive: true});
|
||||
}
|
||||
await fsp.cp(path.join("..", "mod"), path.join(folder, "Mods", "Services", "Olympus"), { recursive: true });
|
||||
logger.log(`Mod succesfully installed in ${folder}`)
|
||||
|
||||
if (fs.existsSync(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "scripts", "mods.lua"))) {
|
||||
logger.log("Backup mods.lua found, copying over");
|
||||
fs.cpSync(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "scripts", "mods.lua"), path.join(folder, "Mods", "Services", "Olympus", "scripts", "mods.lua"));
|
||||
}
|
||||
} catch (err) {
|
||||
logger.log(`Error installing mod in ${folder}: ${err}`)
|
||||
rej(err);
|
||||
}
|
||||
/* Check if backup user-editable files exist. If true copy them over */
|
||||
logger.log(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"));
|
||||
if (await exists(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"))) {
|
||||
logger.log("Backup databases found, copying over");
|
||||
await fsp.cp(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"), path.join(folder, "Mods", "Services", "Olympus", "databases"), { recursive: true });
|
||||
}
|
||||
|
||||
res(true);
|
||||
}
|
||||
});
|
||||
})
|
||||
return promise;
|
||||
if (exists(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "scripts", "mods.lua"))) {
|
||||
logger.log("Backup mods.lua found, copying over");
|
||||
fsp.cp(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "scripts", "mods.lua"), path.join(folder, "Mods", "Services", "Olympus", "scripts", "mods.lua"));
|
||||
}
|
||||
}
|
||||
|
||||
/** Installs the olympus.json file
|
||||
/** Asynchronously installs the olympus.json file
|
||||
*
|
||||
* @param {String} folder The base Saved Games folder where the config json should be installed
|
||||
*/
|
||||
async function installJSON(folder) {
|
||||
logger.log(`Installing config in ${folder}`)
|
||||
var promise = new Promise((res, rej) => {
|
||||
fs.cp(path.join("..", "olympus.json"), path.join(folder, "Config", "olympus.json"), (err) => {
|
||||
if (err) {
|
||||
logger.log(`Error installing config in ${folder}: ${err}`)
|
||||
rej(err);
|
||||
}
|
||||
else {
|
||||
logger.log(`Config succesfully installed in ${folder}`)
|
||||
res(true);
|
||||
}
|
||||
});
|
||||
})
|
||||
return promise;
|
||||
await fsp.cp(path.join("..", "olympus.json"), path.join(folder, "Config", "olympus.json"));
|
||||
logger.log(`Config succesfully installed in ${folder}`)
|
||||
}
|
||||
|
||||
/** Creates shortcuts both in the DCS Saved Games folder and on the desktop
|
||||
/** Asynchronously creates shortcuts both in the DCS Saved Games folder and on the desktop
|
||||
*
|
||||
* @param {String} folder The base Saved Games folder where the shortcuts should be installed
|
||||
* @param {String} name The name of the current DCS Instance, used to create the shortcut names
|
||||
*/
|
||||
async function installShortCuts(folder, name) {
|
||||
logger.log(`Installing shortcuts for Olympus in ${folder}`);
|
||||
var promise = new Promise((res, rej) => {
|
||||
var res1 = createShortcut({
|
||||
windows: {
|
||||
filePath: path.resolve(__dirname, '..', '..', 'client', 'client.vbs'),
|
||||
outputPath: folder,
|
||||
name: `DCS Olympus Client (${name})`,
|
||||
arguments: `"${path.join(folder, "Config", "olympus.json")}"`,
|
||||
icon: path.resolve(__dirname, '..', '..', 'img', 'olympus.ico'),
|
||||
workingDirectory: path.resolve(__dirname, '..', '..', 'client')
|
||||
}
|
||||
});
|
||||
|
||||
var res2 = createShortcut({
|
||||
windows: {
|
||||
filePath: path.resolve(__dirname, '..', '..', 'client', 'server.vbs'),
|
||||
outputPath: folder,
|
||||
name: `DCS Olympus Server (${name})`,
|
||||
arguments: `"${path.join(folder, "Config", "olympus.json")}"`,
|
||||
icon: path.resolve(__dirname, '..', '..', 'img', 'olympus_server.ico'),
|
||||
workingDirectory: path.resolve(__dirname, '..', '..', 'client')
|
||||
}
|
||||
});
|
||||
|
||||
var res3 = createShortcut({
|
||||
windows: {
|
||||
filePath: path.resolve(__dirname, '..', '..', 'client', 'client.vbs'),
|
||||
name: `DCS Olympus Client (${name})`,
|
||||
arguments: `"${path.join(folder, "Config", "olympus.json")}"`,
|
||||
icon: path.resolve(__dirname, '..', '..', 'img', 'olympus.ico'),
|
||||
workingDirectory: path.resolve(__dirname, '..', '..', 'client')
|
||||
}
|
||||
});
|
||||
|
||||
var res4 = createShortcut({
|
||||
windows: {
|
||||
filePath: path.resolve(__dirname, '..', '..', 'client', 'server.vbs'),
|
||||
name: `DCS Olympus Server (${name})`,
|
||||
arguments: `"${path.join(folder, "Config", "olympus.json")}"`,
|
||||
icon: path.resolve(__dirname, '..', '..', 'img', 'olympus_server.ico'),
|
||||
workingDirectory: path.resolve(__dirname, '..', '..', 'client')
|
||||
}
|
||||
});
|
||||
|
||||
if (res1 && res2 && res3 && res4) {
|
||||
res(true);
|
||||
} else {
|
||||
rej("An error occurred while creating the shortcuts")
|
||||
var res1 = createShortcut({
|
||||
windows: {
|
||||
filePath: path.resolve(__dirname, '..', '..', 'client', 'client.vbs'),
|
||||
outputPath: folder,
|
||||
name: `DCS Olympus Client (${name})`,
|
||||
arguments: `"${path.join(folder, "Config", "olympus.json")}"`,
|
||||
icon: path.resolve(__dirname, '..', '..', 'img', 'olympus.ico'),
|
||||
workingDirectory: path.resolve(__dirname, '..', '..', 'client')
|
||||
}
|
||||
});
|
||||
return promise;
|
||||
|
||||
var res2 = createShortcut({
|
||||
windows: {
|
||||
filePath: path.resolve(__dirname, '..', '..', 'client', 'server.vbs'),
|
||||
outputPath: folder,
|
||||
name: `DCS Olympus Server (${name})`,
|
||||
arguments: `"${path.join(folder, "Config", "olympus.json")}"`,
|
||||
icon: path.resolve(__dirname, '..', '..', 'img', 'olympus_server.ico'),
|
||||
workingDirectory: path.resolve(__dirname, '..', '..', 'client')
|
||||
}
|
||||
});
|
||||
|
||||
var res3 = createShortcut({
|
||||
windows: {
|
||||
filePath: path.resolve(__dirname, '..', '..', 'client', 'client.vbs'),
|
||||
name: `DCS Olympus Client (${name})`,
|
||||
arguments: `"${path.join(folder, "Config", "olympus.json")}"`,
|
||||
icon: path.resolve(__dirname, '..', '..', 'img', 'olympus.ico'),
|
||||
workingDirectory: path.resolve(__dirname, '..', '..', 'client')
|
||||
}
|
||||
});
|
||||
|
||||
var res4 = createShortcut({
|
||||
windows: {
|
||||
filePath: path.resolve(__dirname, '..', '..', 'client', 'server.vbs'),
|
||||
name: `DCS Olympus Server (${name})`,
|
||||
arguments: `"${path.join(folder, "Config", "olympus.json")}"`,
|
||||
icon: path.resolve(__dirname, '..', '..', 'img', 'olympus_server.ico'),
|
||||
workingDirectory: path.resolve(__dirname, '..', '..', 'client')
|
||||
}
|
||||
});
|
||||
|
||||
// TODO actually check if the shortcuts where created
|
||||
if (!res1 || !res2 || !res3 || !res4)
|
||||
throw "An error occurred while creating the shortcuts";
|
||||
}
|
||||
|
||||
/** Writes the configuration of an instance to the olympus.json file
|
||||
/** Asynchronously writes the configuration of an instance to the olympus.json file
|
||||
*
|
||||
* @param {String} folder The base Saved Games folder where Olympus should is installed
|
||||
* @param {DCSInstance} instance The DCSInstance of which we want to apply the configuration
|
||||
*/
|
||||
async function applyConfiguration(folder, instance) {
|
||||
logger.log(`Applying configuration to Olympus in ${folder}`);
|
||||
var promise = new Promise((res, rej) => {
|
||||
if (fs.existsSync(path.join(folder, "Config", "olympus.json"))) {
|
||||
var config = JSON.parse(fs.readFileSync(path.join(folder, "Config", "olympus.json")));
|
||||
|
||||
config["client"]["port"] = instance.clientPort;
|
||||
config["server"]["port"] = instance.backendPort;
|
||||
config["server"]["address"] = instance.backendAddress;
|
||||
config["authentication"]["gameMasterPassword"] = sha256(instance.gameMasterPassword);
|
||||
config["authentication"]["blueCommanderPassword"] = sha256(instance.blueCommanderPassword);
|
||||
config["authentication"]["redCommanderPassword"] = sha256(instance.redCommanderPassword);
|
||||
if (await exists(path.join(folder, "Config", "olympus.json"))) {
|
||||
var config = JSON.parse(await fsp.readFile(path.join(folder, "Config", "olympus.json")));
|
||||
|
||||
fs.writeFile(path.join(folder, "Config", "olympus.json"), JSON.stringify(config, null, 4), (err) => {
|
||||
if (err) {
|
||||
logger.log(`Error applying config in ${folder}: ${err}`)
|
||||
rej(err);
|
||||
}
|
||||
else {
|
||||
logger.log(`Config succesfully applied in ${folder}`)
|
||||
res(true);
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
rej("File does not exist")
|
||||
/* Automatically find free ports */
|
||||
if (instance.connectionsType === 'auto') {
|
||||
await instance.findFreePorts();
|
||||
}
|
||||
res(true);
|
||||
});
|
||||
return promise;
|
||||
|
||||
/* Apply the configuration */
|
||||
config["client"]["port"] = instance.clientPort;
|
||||
config["server"]["port"] = instance.backendPort;
|
||||
config["server"]["address"] = instance.backendAddress;
|
||||
config["authentication"]["gameMasterPassword"] = sha256(instance.gameMasterPassword);
|
||||
config["authentication"]["blueCommanderPassword"] = sha256(instance.blueCommanderPassword);
|
||||
config["authentication"]["redCommanderPassword"] = sha256(instance.redCommanderPassword);
|
||||
|
||||
await fsp.writeFile(path.join(folder, "Config", "olympus.json"), JSON.stringify(config, null, 4));
|
||||
logger.log(`Config succesfully applied in ${folder}`)
|
||||
} else {
|
||||
throw "File does not exist";
|
||||
}
|
||||
}
|
||||
|
||||
/** Deletes the Hooks script
|
||||
/** Asynchronously deletes the Hooks script
|
||||
*
|
||||
* @param {String} folder The base Saved Games folder where Olympus is installed
|
||||
*/
|
||||
async function deleteHooks(folder) {
|
||||
logger.log(`Deleting hooks from ${folder}`);
|
||||
return deleteFile(path.join(folder, "Scripts", "Hooks", "OlympusHook.lua"));
|
||||
await deleteFile(path.join(folder, "Scripts", "Hooks", "OlympusHook.lua"));
|
||||
}
|
||||
|
||||
/** Deletes the Mod folder
|
||||
/** Asynchronously deletes the Mod folder
|
||||
*
|
||||
* @param {String} folder The base Saved Games folder where Olympus is installed
|
||||
*/
|
||||
async function deleteMod(folder, name) {
|
||||
logger.log(`Deleting mod from ${folder}`);
|
||||
var promise = new Promise((res, rej) => {
|
||||
if (fs.existsSync(path.join(folder, "Mods", "Services", "Olympus"))) {
|
||||
/* Make a copy of the user-editable files */
|
||||
if (fs.existsSync(path.join(folder, "Mods", "Services", "Olympus", "databases")))
|
||||
fs.cpSync(path.join(folder, "Mods", "Services", "Olympus", "databases"), path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"), {recursive: true});
|
||||
else
|
||||
logger.warn(`No database folder found in ${folder}, skipping backup...`)
|
||||
|
||||
if (fs.existsSync(path.join(folder, "Mods", "Services", "Olympus", "scripts", "mods.lua")))
|
||||
fs.cpSync(path.join(folder, "Mods", "Services", "Olympus", "scripts", "mods.lua"), path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "scripts", "mods.lua"));
|
||||
else
|
||||
logger.warn(`No mods.lua found in ${folder}, skipping backup...`)
|
||||
if (await exists(path.join(folder, "Mods", "Services", "Olympus"))) {
|
||||
/* Make a copy of the user-editable files */
|
||||
if (await exists(path.join(folder, "Mods", "Services", "Olympus", "databases")))
|
||||
await fsp.cp(path.join(folder, "Mods", "Services", "Olympus", "databases"), path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"), { recursive: true });
|
||||
else
|
||||
logger.warn(`No database folder found in ${folder}, skipping backup...`)
|
||||
|
||||
/* Remove the mod folder */
|
||||
fs.rmdir(path.join(folder, "Mods", "Services", "Olympus"), { recursive: true, force: true }, (err) => {
|
||||
if (err) {
|
||||
logger.log(`Error removing mod from ${folder}: ${err}`)
|
||||
rej(err);
|
||||
}
|
||||
else {
|
||||
logger.log(`Mod succesfully removed from ${folder}`)
|
||||
res(true);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
res(true);
|
||||
};
|
||||
})
|
||||
return promise;
|
||||
if (await exists(path.join(folder, "Mods", "Services", "Olympus", "scripts", "mods.lua")))
|
||||
await fsp.cp(path.join(folder, "Mods", "Services", "Olympus", "scripts", "mods.lua"), path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "scripts", "mods.lua"));
|
||||
else
|
||||
logger.warn(`No mods.lua found in ${folder}, skipping backup...`)
|
||||
|
||||
/* Remove the mod folder */
|
||||
await fsp.rmdir(path.join(folder, "Mods", "Services", "Olympus"), { recursive: true, force: true })
|
||||
logger.log(`Mod succesfully removed from ${folder}`)
|
||||
} else {
|
||||
logger.warn(`Mod does not exist in ${folder}, nothing to do`)
|
||||
}
|
||||
}
|
||||
|
||||
/** Deletes the olympus.json configuration file
|
||||
/** Asynchronously deletes the olympus.json configuration file
|
||||
*
|
||||
* @param {String} folder The base Saved Games folder where Olympus is installed
|
||||
*/
|
||||
async function deleteJSON(folder) {
|
||||
logger.log(`Deleting JSON from ${folder}`);
|
||||
return deleteFile(path.join(folder, "Config", "olympus.json"));
|
||||
}
|
||||
|
||||
/** Deletes the shortcuts
|
||||
/** Asynchronously deletes the shortcuts
|
||||
*
|
||||
* @param {String} folder The base Saved Games folder where Olympus is installed
|
||||
* @param {String} name The name of the DCS Instance, used to find the correct shortcuts
|
||||
*/
|
||||
async function deleteShortCuts(folder, name) {
|
||||
logger.log(`Deleting ShortCuts from ${folder}`);
|
||||
var promise = new Promise((res, rej) => {
|
||||
deleteFile(path.join(folder, `DCS Olympus Server (${name}).lnk`))
|
||||
.then(deleteFile(path.join(folder, `DCS Olympus Client (${name}).lnk`)), (err) => { return Promise.reject(err); })
|
||||
.then(deleteFile(path.join(homeDir, "Desktop", `DCS Olympus Server (${name}).lnk`)), (err) => { return Promise.reject(err); })
|
||||
.then(deleteFile(path.join(homeDir, "Desktop", `DCS Olympus Client (${name}).lnk`)), (err) => { return Promise.reject(err); })
|
||||
.then(() => { res(true) }, (err) => { rej(err) })
|
||||
});
|
||||
return promise;
|
||||
logger.log(`Deleting ShortCuts from ${folder} and desktop`);
|
||||
await deleteFile(path.join(folder, `DCS Olympus Server (${name}).lnk`))
|
||||
await deleteFile(path.join(folder, `DCS Olympus Client (${name}).lnk`))
|
||||
await deleteFile(path.join(homeDir, "Desktop", `DCS Olympus Server (${name}).lnk`))
|
||||
await deleteFile(path.join(homeDir, "Desktop", `DCS Olympus Client (${name}).lnk`))
|
||||
logger.log(`ShortCuts deleted from ${folder} and desktop`);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@ -318,11 +237,9 @@ module.exports = {
|
||||
installHooks: installHooks,
|
||||
installMod: installMod,
|
||||
installShortCuts, installShortCuts,
|
||||
fixInstances: fixInstances,
|
||||
deleteHooks: deleteHooks,
|
||||
deleteJSON: deleteJSON,
|
||||
deleteMod: deleteMod,
|
||||
deleteShortCuts: deleteShortCuts,
|
||||
uninstallInstance: uninstallInstance,
|
||||
logger: logger
|
||||
}
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
const DCSInstance = require("./dcsinstance");
|
||||
const ManagerPage = require("./managerpage");
|
||||
const ejs = require('ejs')
|
||||
const { logger } = require("./filesystem")
|
||||
|
||||
class InstallationsPage extends ManagerPage {
|
||||
onCancelClicked;
|
||||
setSelectedInstance;
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
render(str) {
|
||||
this.element.innerHTML = str;
|
||||
|
||||
var options = this.element.querySelectorAll(".option");
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
options[i].onclick = (e) => {this.onOptionClicked(e);}
|
||||
}
|
||||
|
||||
this.element.querySelector(".cancel").addEventListener("click", (e) => this.onCancelClicked(e));
|
||||
|
||||
super.render();
|
||||
}
|
||||
|
||||
async onOptionClicked(e) {
|
||||
this.setSelectedInstance((await DCSInstance.getInstances()).find((instance) => {return instance.folder === e.target.dataset.folder}));
|
||||
}
|
||||
|
||||
show() {
|
||||
ejs.renderFile("./ejs/installations.ejs", this.options, {}, (err, str) => {
|
||||
if (!err) {
|
||||
this.render(str);
|
||||
} else {
|
||||
logger.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
super.show();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InstallationsPage;
|
||||
@ -1,108 +0,0 @@
|
||||
const DCSInstance = require("./dcsinstance");
|
||||
const ManagerPage = require("./managerpage");
|
||||
const ejs = require('ejs');
|
||||
const { showErrorPopup } = require("./popup");
|
||||
const { exec } = require("child_process");
|
||||
const { logger } = require("./filesystem")
|
||||
|
||||
class InstancesPage extends ManagerPage {
|
||||
onCancelClicked;
|
||||
setSelectedInstance;
|
||||
startInstance;
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
render(str) {
|
||||
this.element.innerHTML = str;
|
||||
|
||||
var editButtons = this.element.querySelectorAll(".button.edit");
|
||||
for (let i = 0; i < editButtons.length; i++) {
|
||||
editButtons[i].onclick = (e) => {this.onEditClicked(e);}
|
||||
}
|
||||
|
||||
var uninstallButtons = this.element.querySelectorAll(".button.uninstall");
|
||||
for (let i = 0; i < uninstallButtons.length; i++) {
|
||||
uninstallButtons[i].onclick = (e) => {this.onUninstallClicked(e);}
|
||||
}
|
||||
|
||||
var startServerButtons = this.element.querySelectorAll(".button.start-server");
|
||||
for (let i = 0; i < startServerButtons.length; i++) {
|
||||
startServerButtons[i].onclick = (e) => {this.onStartServerClicked(e);}
|
||||
}
|
||||
|
||||
var startClientButtons = this.element.querySelectorAll(".button.start-client");
|
||||
for (let i = 0; i < startClientButtons.length; i++) {
|
||||
startClientButtons[i].onclick = (e) => {this.onStartClientClicked(e);}
|
||||
}
|
||||
|
||||
var openBrowserButtons = this.element.querySelectorAll(".button.open-browser");
|
||||
for (let i = 0; i < openBrowserButtons.length; i++) {
|
||||
openBrowserButtons[i].onclick = (e) => {this.onOpenBrowserClicked(e);}
|
||||
}
|
||||
|
||||
var stopButtons = this.element.querySelectorAll(".button.stop");
|
||||
for (let i = 0; i < stopButtons.length; i++) {
|
||||
stopButtons[i].onclick = (e) => {this.onStopClicked(e);}
|
||||
}
|
||||
|
||||
this.element.querySelector(".cancel").addEventListener("click", (e) => this.onCancelClicked(e));
|
||||
|
||||
super.render();
|
||||
}
|
||||
|
||||
async onEditClicked(e) {
|
||||
this.getClickedInstance(e).then((instance) => {
|
||||
instance.webserverOnline || instance.backendOnline? showErrorPopup("Error, the selected Olympus instance is currently active, please stop Olympus before editing it!") :
|
||||
this.setSelectedInstance(instance);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async onStartServerClicked(e) {
|
||||
e.target.closest(".collapse").classList.add("loading");
|
||||
this.getClickedInstance(e).then((instance) => instance.startServer());
|
||||
}
|
||||
|
||||
async onStartClientClicked(e) {
|
||||
e.target.closest(".collapse").classList.add("loading");
|
||||
this.getClickedInstance(e).then(instance => instance.startClient());
|
||||
}
|
||||
|
||||
async onOpenBrowserClicked(e) {
|
||||
this.getClickedInstance(e).then((instance) => exec(`start http://localhost:${instance.clientPort}`));
|
||||
}
|
||||
|
||||
async onStopClicked(e) {
|
||||
this.getClickedInstance(e).then((instance) => instance.stop());
|
||||
}
|
||||
|
||||
async onUninstallClicked(e) {
|
||||
this.getClickedInstance(e).then((instance) => {
|
||||
instance.webserverOnline || instance.backendOnline? showErrorPopup("Error, the selected Olympus instance is currently active, please stop Olympus before uninstalling it!") : instance.uninstall();
|
||||
});
|
||||
}
|
||||
|
||||
async getClickedInstance(e) {
|
||||
return DCSInstance.getInstances().then((instances) => {
|
||||
return instances.find((instance) => {
|
||||
return instance.folder === e.target.closest('.option').dataset.folder
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
show() {
|
||||
ejs.renderFile("./ejs/instances.ejs", this.options, {}, (err, str) => {
|
||||
if (!err) {
|
||||
this.render(str);
|
||||
} else {
|
||||
logger.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
super.show();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InstancesPage;
|
||||
@ -1,264 +1,767 @@
|
||||
const MenuPage = require("./menu");
|
||||
const InstallationsPage = require('./installations');
|
||||
const ConnectionsPage = require('./connections');
|
||||
const PasswordsPage = require('./passwords');
|
||||
const ResultPage = require('./result');
|
||||
const InstancesPage = require('./instances');
|
||||
const path = require("path")
|
||||
const fs = require("fs");
|
||||
|
||||
const DCSInstance = require('./dcsinstance');
|
||||
const { showErrorPopup, showWaitPopup } = require('./popup');
|
||||
const { fixInstances } = require('./filesystem');
|
||||
const { showErrorPopup, showWaitPopup, showConfirmPopup } = require('./popup');
|
||||
const { logger } = require("./filesystem")
|
||||
const path = require("path")
|
||||
|
||||
const ManagerPage = require("./managerpage");
|
||||
const WizardPage = require("./wizardpage");
|
||||
const { fetchWithTimeout } = require("./net");
|
||||
const { exec } = require("child_process");
|
||||
const { sleep } = require("./utils");
|
||||
|
||||
class Manager {
|
||||
simplified = true;
|
||||
options = {
|
||||
activeInstance: undefined,
|
||||
additionalDCSInstances: [],
|
||||
configLoaded: false,
|
||||
instances: [],
|
||||
IP: undefined,
|
||||
logLocation: path.join(__dirname, "..", "manager.log"),
|
||||
mode: 'basic',
|
||||
state: 'IDLE'
|
||||
};
|
||||
|
||||
/* Manager pages */
|
||||
activePage = null;
|
||||
welcomePage = null;
|
||||
settingsPage = null;
|
||||
folderPage = null;
|
||||
typePage = null;
|
||||
connectionsTypePage = null;
|
||||
connectionsPage = null;
|
||||
passwordsPage = null;
|
||||
resultPage = null;
|
||||
instancesPage = null;
|
||||
expertSettingsPage = null;
|
||||
|
||||
constructor() {
|
||||
/* Simple framework to define callbacks to events directly in the .ejs files. When an event happens, e.g. a button is clicked, the signal function is called with the function
|
||||
to call and an optional object to pass. An event will then be created, defined in index.html, and will be listened here. Using an eval call, the appropriate member function
|
||||
will then be called */
|
||||
document.addEventListener("signal", (ev) => {
|
||||
const callback = ev.detail.callback;
|
||||
const params = JSON.stringify(ev.detail.params);
|
||||
try {
|
||||
eval(`this.${callback}(${params})`)
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
window.olympus = {
|
||||
manager: this
|
||||
};
|
||||
}
|
||||
|
||||
/** Asynchronously start the manager
|
||||
*
|
||||
*/
|
||||
async start() {
|
||||
/* Get the list of DCS instances */
|
||||
var instances = await DCSInstance.getInstances();
|
||||
|
||||
/* If there is only 1 DCS Instance and Olympus is not installed in it, go straight to the installation page (since there is nothing else to do) */
|
||||
this.simplified = instances.length === 1 && !instances[0].installed;
|
||||
/* Check if the options file exists */
|
||||
if (fs.existsSync("options.json")) {
|
||||
/* Load the options from the json file */
|
||||
try {
|
||||
this.options = { ...this.options, ...JSON.parse(fs.readFileSync("options.json")) };
|
||||
this.setConfigLoaded(true);
|
||||
} catch (e) {
|
||||
logger.error(`An error occurred while reading the options.json file: ${e}`);
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`)
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("loader").classList.add("hide");
|
||||
if (!this.getConfigLoaded()) {
|
||||
this.hideLoadingPage();
|
||||
|
||||
/* Check if there are corrupted or outdate instances */
|
||||
if (instances.some((instance) => {
|
||||
return instance.installed && instance.error;
|
||||
})) {
|
||||
/* Ask the user for confirmation */
|
||||
showErrorPopup("One or more Olympus instances are corrupted or need updating. Press Close to fix this.", async () => {
|
||||
showWaitPopup("Please wait while your instances are being fixed.")
|
||||
fixInstances(instances.filter((instance) => {
|
||||
return instance.installed && instance.error;
|
||||
})).then(
|
||||
() => { location.reload() },
|
||||
(err) => {
|
||||
/* Show page to select basic vs expert mode */
|
||||
this.welcomePage = new ManagerPage(this, "./ejs/welcome.ejs");
|
||||
this.welcomePage.show();
|
||||
}
|
||||
else {
|
||||
document.getElementById("header").classList.remove("hide");
|
||||
|
||||
/* Initialize mode switching */
|
||||
if (this.getMode() === "basic") {
|
||||
document.getElementById("switch-mode").innerText = "Expert mode";
|
||||
document.getElementById("switch-mode").onclick = () => { this.switchMode("expert"); }
|
||||
}
|
||||
else {
|
||||
document.getElementById("switch-mode").innerText = "Basic mode";
|
||||
document.getElementById("switch-mode").onclick = () => { this.switchMode("basic"); }
|
||||
}
|
||||
|
||||
/* Get the list of DCS instances */
|
||||
this.setLoadingProgress("Retrieving DCS instances...", 0);
|
||||
var instances = await DCSInstance.getInstances();
|
||||
this.setLoadingProgress(`Analysis completed, starting manager...`, 100);
|
||||
await sleep(100);
|
||||
|
||||
this.setInstances(instances);
|
||||
|
||||
/* Get my public IP */
|
||||
this.getPublicIP().then(
|
||||
(IP) => { this.setIP(IP); },
|
||||
(err) => {
|
||||
logger.log(err)
|
||||
this.setIP(undefined);
|
||||
}
|
||||
)
|
||||
|
||||
/* Check if there are corrupted or outdated instances */
|
||||
if (this.getInstances().some((instance) => {
|
||||
return instance.installed && instance.error;
|
||||
})) {
|
||||
/* Ask the user for confirmation */
|
||||
showConfirmPopup("<div class='main-message'> One or more of your Olympus instances are not up to date! </div><div class='sub-message'> If you have just updated Olympus this is normal.<br><br> Press <b>Accept</b> and the Manager will update your instances for you. <br> Press <b>Close</b> to update your instances manually using the Installation Wizard</div>", async () => {
|
||||
try {
|
||||
/* Nested popup calls need to wait for animation to complete */
|
||||
await sleep(300);
|
||||
|
||||
await DCSInstance.fixInstances();
|
||||
location.reload();
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
showErrorPopup(`An error occurred while trying to fix your installations. Please reinstall Olympus manually. <br><br> You can find more info in ${path.join(__dirname, "..", "manager.log")}`);
|
||||
|
||||
/* Nested popup calls need to wait for animation to complete */
|
||||
await sleep(300);
|
||||
showErrorPopup(`<div class='main-message'>An error occurred while trying to fix your installations. Please reinstall Olympus manually. </div><div class='sub-message'> You can find more info in ${this.options.logLocation} </div>`);
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/* Check which buttons should be enabled */
|
||||
const installEnabled = true;
|
||||
const manageEnabled = instances.some((instance) => { return instance.installed; });
|
||||
|
||||
/* Menu */
|
||||
var menuPage = new MenuPage();
|
||||
menuPage.options = {
|
||||
...menuPage.options,
|
||||
installEnabled: installEnabled,
|
||||
manageEnabled: manageEnabled
|
||||
}
|
||||
/* When the install button is clicked go the installation page */
|
||||
menuPage.onInstallClicked = (e) => {
|
||||
menuPage.hide();
|
||||
installationsPage.show();
|
||||
}
|
||||
/* When the manage button is clicked go to the instances page in "manage mode" (i.e. manage = true) */
|
||||
menuPage.onManageClicked = (e) => {
|
||||
menuPage.hide();
|
||||
instancesPage.show();
|
||||
}
|
||||
|
||||
/* Installations */
|
||||
var installationsPage = new InstallationsPage();
|
||||
installationsPage.options = {
|
||||
...installationsPage.options,
|
||||
instances: instances
|
||||
}
|
||||
installationsPage.setSelectedInstance = (activeInstance) => {
|
||||
/* Set the active options for the pages */
|
||||
const options = {
|
||||
instance: activeInstance,
|
||||
simplified: this.simplified,
|
||||
install: true
|
||||
}
|
||||
connectionsPage.options = {
|
||||
...connectionsPage.options,
|
||||
...options
|
||||
}
|
||||
passwordsPage.options = {
|
||||
...passwordsPage.options,
|
||||
...options
|
||||
}
|
||||
resultPage.options = {
|
||||
...resultPage.options,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
/* Show the connections page */
|
||||
installationsPage.hide();
|
||||
connectionsPage.show();
|
||||
/* Hide the loading page */
|
||||
this.hideLoadingPage();
|
||||
|
||||
connectionsPage.onBackClicked = (e) => {
|
||||
/* Show the installation page */
|
||||
connectionsPage.hide();
|
||||
installationsPage.show();
|
||||
}
|
||||
}
|
||||
installationsPage.onCancelClicked = (e) => {
|
||||
/* Go back to the main menu */
|
||||
installationsPage.hide();
|
||||
menuPage.show();
|
||||
}
|
||||
/* Create all the HTML pages */
|
||||
this.menuPage = new ManagerPage(this, "./ejs/menu.ejs");
|
||||
this.folderPage = new WizardPage(this, "./ejs/folder.ejs");
|
||||
this.settingsPage = new ManagerPage(this, "./ejs/settings.ejs");
|
||||
this.typePage = new WizardPage(this, "./ejs/type.ejs");
|
||||
this.connectionsTypePage = new WizardPage(this, "./ejs/connectionsType.ejs");
|
||||
this.connectionsPage = new WizardPage(this, "./ejs/connections.ejs");
|
||||
this.passwordsPage = new WizardPage(this, "./ejs/passwords.ejs");
|
||||
this.resultPage = new ManagerPage(this, "./ejs/result.ejs");
|
||||
this.instancesPage = new ManagerPage(this, "./ejs/instances.ejs");
|
||||
this.expertSettingsPage = new WizardPage(this, "./ejs/expertsettings.ejs");
|
||||
|
||||
/* Instances */
|
||||
var instancesPage = new InstancesPage();
|
||||
instancesPage.options = {
|
||||
...instancesPage.options,
|
||||
instances: instances.filter((instance) => { return instance.installed; })
|
||||
}
|
||||
instancesPage.setSelectedInstance = (activeInstance) => {
|
||||
/* Set the active options for the pages */
|
||||
const options = {
|
||||
instance: activeInstance,
|
||||
simplified: this.simplified,
|
||||
install: false
|
||||
}
|
||||
connectionsPage.options = {
|
||||
...connectionsPage.options,
|
||||
...options
|
||||
}
|
||||
passwordsPage.options = {
|
||||
...passwordsPage.options,
|
||||
...options
|
||||
}
|
||||
resultPage.options = {
|
||||
...resultPage.options,
|
||||
...options
|
||||
}
|
||||
|
||||
/* Show the connections page */
|
||||
instancesPage.hide();
|
||||
connectionsPage.show();
|
||||
|
||||
connectionsPage.onBackClicked = (e) => {
|
||||
/* Show the instances page */
|
||||
connectionsPage.hide();
|
||||
instancesPage.show();
|
||||
}
|
||||
}
|
||||
instancesPage.onCancelClicked = (e) => {
|
||||
/* Go back to the main menu */
|
||||
instancesPage.hide();
|
||||
menuPage.show();
|
||||
}
|
||||
|
||||
/* Connections */
|
||||
var connectionsPage = new ConnectionsPage();
|
||||
connectionsPage.onNextClicked = async (e) => {
|
||||
let activeInstance = connectionsPage.options.instance;
|
||||
if (activeInstance) {
|
||||
/* Check that the selected ports are free before proceeding */
|
||||
if (await activeInstance.checkClientPort(activeInstance.clientPort) && await activeInstance.checkBackendPort(activeInstance.backendPort)) {
|
||||
connectionsPage.hide();
|
||||
passwordsPage.show();
|
||||
} else {
|
||||
showErrorPopup("Please make sure the selected ports are not already in use.")
|
||||
/* Force the setting of the ports whenever the page is shown */
|
||||
this.connectionsPage.options.onShow = () => {
|
||||
if (this.getActiveInstance()) {
|
||||
this.setPort('client', this.getActiveInstance().clientPort);
|
||||
this.setPort('backend', this.getActiveInstance().backendPort);
|
||||
}
|
||||
} else {
|
||||
showErrorPopup(`An error has occurred, please restart the Olympus Manager. <br><br> You can find more info in ${path.join(__dirname, "..", "manager.log")}`)
|
||||
}
|
||||
}
|
||||
connectionsPage.onCancelClicked = (e) => {
|
||||
/* Go back to the main menu */
|
||||
connectionsPage.hide();
|
||||
menuPage.show();
|
||||
}
|
||||
|
||||
/* Passwords */
|
||||
var passwordsPage = new PasswordsPage();
|
||||
passwordsPage.onBackClicked = (e) => {
|
||||
/* Go back to the connections page */
|
||||
let activeInstance = connectionsPage.options.instance;
|
||||
if (activeInstance) {
|
||||
passwordsPage.hide();
|
||||
connectionsPage.show();
|
||||
} else {
|
||||
showErrorPopup(`An error has occurred, please restart the Olympus Manager. <br><br> You can find more info in ${path.join(__dirname, "..", "manager.log")}`)
|
||||
}
|
||||
}
|
||||
passwordsPage.onNextClicked = (e) => {
|
||||
let activeInstance = connectionsPage.options.instance;
|
||||
if (activeInstance) {
|
||||
/* Check that all the passwords have been set */
|
||||
if (activeInstance.gameMasterPassword === "" || activeInstance.blueCommanderPassword === "" || activeInstance.redCommanderPassword === "") {
|
||||
showErrorPopup("Please fill all the password inputs.")
|
||||
this.expertSettingsPage.options.onShow = () => {
|
||||
if (this.getActiveInstance()) {
|
||||
this.setPort('client', this.getActiveInstance().clientPort);
|
||||
this.setPort('backend', this.getActiveInstance().backendPort);
|
||||
}
|
||||
else if (activeInstance.gameMasterPassword === activeInstance.blueCommanderPassword || activeInstance.blueCommanderPassword === activeInstance.redCommanderPassword || activeInstance.gameMasterPassword === activeInstance.redCommanderPassword) {
|
||||
showErrorPopup("All the passwords must be different from each other.")
|
||||
} else {
|
||||
passwordsPage.hide();
|
||||
resultPage.show();
|
||||
resultPage.startInstallation();
|
||||
}
|
||||
} else {
|
||||
showErrorPopup(`An error has occurred, please restart the Olympus Manager. <br><br> You can find more info in ${path.join(__dirname, "..", "manager.log")}`)
|
||||
}
|
||||
|
||||
/* Always force the IDLE state when reaching the menu page */
|
||||
this.menuPage.options.onShow = async () => {
|
||||
await this.setState('IDLE');
|
||||
}
|
||||
|
||||
}
|
||||
passwordsPage.onCancelClicked = (e) => {
|
||||
/* Go back to the main menu */
|
||||
passwordsPage.hide();
|
||||
menuPage.show();
|
||||
}
|
||||
/* Update the instances when showing the dashboard */
|
||||
this.instancesPage.options.onShow = () => {
|
||||
this.updateInstances();
|
||||
}
|
||||
|
||||
/* Result */
|
||||
var resultPage = new ResultPage({logLocation: path.join(__dirname, "..", "manager.log")});
|
||||
resultPage.onBackClicked = (e) => {
|
||||
/* Reload the page to apply changes */
|
||||
resultPage.hide();
|
||||
/* Reset default radio buttons */
|
||||
this.typePage.options.onShow = () => {
|
||||
if (this.getActiveInstance())
|
||||
this.getActiveInstance().installationType = 'singleplayer';
|
||||
else {
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`);
|
||||
}
|
||||
}
|
||||
|
||||
this.connectionsTypePage.options.onShow = () => {
|
||||
if (this.getActiveInstance())
|
||||
this.getActiveInstance().connectionsType = 'auto';
|
||||
else {
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`);
|
||||
}
|
||||
}
|
||||
|
||||
/* Reload the instances when we get to the folder page */
|
||||
this.folderPage.options.onShow = async () => {
|
||||
if (this.getInstances().length > 0)
|
||||
this.setActiveInstance(this.getInstances()[0]);
|
||||
await DCSInstance.reloadInstances();
|
||||
}
|
||||
|
||||
if (this.getMode() === "basic") {
|
||||
/* In basic mode no dashboard is shown */
|
||||
this.menuPage.show();
|
||||
} else {
|
||||
/* In Expert mode we go directly to the dashboard */
|
||||
this.instancesPage.show();
|
||||
this.updateInstances();
|
||||
}
|
||||
|
||||
/* Send an event on manager started */
|
||||
document.dispatchEvent(new CustomEvent("managerStarted"));
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates the options file. This is done only the very first time you start Olympus.
|
||||
*
|
||||
* @param {String} mode The mode, either Basic or Expert
|
||||
*/
|
||||
async createOptionsFile(mode) {
|
||||
try {
|
||||
fs.writeFileSync("options.json", JSON.stringify({ mode: mode, additionalDCSInstances: [] }, null, 2));
|
||||
location.reload();
|
||||
} catch (err) {
|
||||
logger.log(err);
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`)
|
||||
}
|
||||
resultPage.onCancelClicked = (e) => {
|
||||
/* Reload the page to apply changes */
|
||||
resultPage.hide();
|
||||
location.reload();
|
||||
}
|
||||
|
||||
/** Switch to a different mode of operation
|
||||
*
|
||||
* @param {String} newMode The mode to switch to
|
||||
*/
|
||||
async switchMode(newMode) {
|
||||
/* Change the mode in the options.json and reload the page */
|
||||
var options = JSON.parse(fs.readFileSync("options.json"));
|
||||
options.mode = newMode;
|
||||
fs.writeFileSync("options.json", JSON.stringify(options, null, 2));
|
||||
location.reload();
|
||||
}
|
||||
|
||||
/************************************************/
|
||||
/* CALLBACKS */
|
||||
/************************************************/
|
||||
/** Switch to basic mode
|
||||
*
|
||||
*/
|
||||
async onBasicClicked() {
|
||||
this.createOptionsFile("basic");
|
||||
}
|
||||
|
||||
/** Switch to expert mode
|
||||
*
|
||||
*/
|
||||
async onExpertClicked() {
|
||||
this.createOptionsFile("expert");
|
||||
}
|
||||
|
||||
/** When the install button is clicked go the installation page
|
||||
*
|
||||
*/
|
||||
async onInstallMenuClicked() {
|
||||
await this.setState('INSTALL');
|
||||
|
||||
if (this.getInstances().length == 0) {
|
||||
// TODO: show error
|
||||
}
|
||||
|
||||
/* Create all the HTML pages */
|
||||
document.body.appendChild(menuPage.getElement());
|
||||
document.body.appendChild(installationsPage.getElement());
|
||||
document.body.appendChild(instancesPage.getElement());
|
||||
document.body.appendChild(connectionsPage.getElement());
|
||||
document.body.appendChild(passwordsPage.getElement());
|
||||
document.body.appendChild(resultPage.getElement());
|
||||
if (this.getInstances().length === 1) {
|
||||
this.setActiveInstance(this.getInstances()[0]);
|
||||
|
||||
/* In simplified mode we directly show the connections page */
|
||||
if (this.simplified) {
|
||||
const options = {
|
||||
instance: instances[0],
|
||||
simplified: this.simplified,
|
||||
install: true
|
||||
/* Show the type selection page */
|
||||
if (!this.getActiveInstance().installed) {
|
||||
this.activePage.hide()
|
||||
this.typePage.show();
|
||||
} else {
|
||||
if (this.getActiveInstance().webserverOnline || this.getActiveInstance().backendOnline) {
|
||||
showErrorPopup("<div class='main-message'>The selected Olympus instance is currently active </div><div class='sub-message'> Please stop DCS and Olympus Server/Client before editing it! </div>");
|
||||
} else {
|
||||
showConfirmPopup("<div class='main-message'> Olympus is already installed in this instance! </div> <div class='sub-message'>If you click Accept, it will be installed again and all changes, e.g. custom databases or mods support, will be lost. Are you sure you want to continue?</div>",
|
||||
() => {
|
||||
this.activePage.hide();
|
||||
this.typePage.show();
|
||||
},
|
||||
async () => {
|
||||
await this.setState('IDLE');
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
connectionsPage.options = {
|
||||
...connectionsPage.options,
|
||||
...options
|
||||
}
|
||||
passwordsPage.options = {
|
||||
...passwordsPage.options,
|
||||
...options
|
||||
}
|
||||
resultPage.options = {
|
||||
...resultPage.options,
|
||||
...options
|
||||
}
|
||||
/* Show the connections page directly */
|
||||
instancesPage.hide();
|
||||
connectionsPage.show();
|
||||
} else {
|
||||
/* Show the main menu */
|
||||
menuPage.show();
|
||||
/* Show the folder selection page */
|
||||
this.activePage.hide()
|
||||
this.folderPage.show();
|
||||
}
|
||||
}
|
||||
|
||||
/** When the edit button is clicked go to the settings page
|
||||
*
|
||||
*/
|
||||
async onEditMenuClicked() {
|
||||
this.activePage.hide();
|
||||
await this.setState('IDLE');
|
||||
this.settingsPage.show();
|
||||
}
|
||||
|
||||
/** When a folder is selected, find what instance was clicked to set as active
|
||||
*
|
||||
* @param {String} name The name of the instance
|
||||
*/
|
||||
|
||||
async onFolderClicked(name) {
|
||||
var instance = await this.getClickedInstance(name);
|
||||
|
||||
var instanceDivs = this.folderPage.getElement().querySelectorAll(".button.radio");
|
||||
for (let i = 0; i < instanceDivs.length; i++) {
|
||||
instanceDivs[i].classList.toggle('selected', instanceDivs[i].dataset.folder === instance.folder);
|
||||
if (instanceDivs[i].dataset.folder === instance.folder)
|
||||
this.setActiveInstance(instance);
|
||||
}
|
||||
}
|
||||
|
||||
/* When the installation type is selected */
|
||||
async onInstallTypeClicked(type) {
|
||||
this.typePage.getElement().querySelector(`.singleplayer`).classList.toggle("selected", type === 'singleplayer');
|
||||
this.typePage.getElement().querySelector(`.multiplayer`).classList.toggle("selected", type === 'multiplayer');
|
||||
if (this.getActiveInstance())
|
||||
this.getActiveInstance().installationType = type;
|
||||
else {
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`);
|
||||
}
|
||||
}
|
||||
|
||||
/* When the connections type is selected */
|
||||
async onConnectionsTypeClicked(type) {
|
||||
this.connectionsTypePage.getElement().querySelector(`.auto`).classList.toggle("selected", type === 'auto');
|
||||
this.connectionsTypePage.getElement().querySelector(`.manual`).classList.toggle("selected", type === 'manual');
|
||||
if (this.getActiveInstance())
|
||||
this.getActiveInstance().connectionsType = type;
|
||||
else {
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`);
|
||||
}
|
||||
}
|
||||
|
||||
/* When the next button of a wizard page is clicked */
|
||||
async onNextClicked() {
|
||||
/* Choose which page to show depending on the active page */
|
||||
/* Folder selection page */
|
||||
if (this.activePage == this.folderPage) {
|
||||
if (this.getActiveInstance().installed) {
|
||||
if (this.getActiveInstance().webserverOnline || this.getActiveInstance().backendOnline) {
|
||||
showErrorPopup("<div class='main-message'>The selected Olympus instance is currently active </div><div class='sub-message'> Please stop DCS and Olympus Server/Client before editing it! </div>");
|
||||
} else {
|
||||
showConfirmPopup("<div class='main-message'> Olympus is already installed in this instance! </div> <div class='sub-message'>If you click Accept, it will be installed again and all changes, e.g. custom databases or mods support, will be lost. Are you sure you want to continue?</div>",
|
||||
() => {
|
||||
this.activePage.hide();
|
||||
this.typePage.show();
|
||||
},
|
||||
async () => {
|
||||
await this.setState('IDLE');
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
this.activePage.hide();
|
||||
this.typePage.show();
|
||||
}
|
||||
/* Installation type page */
|
||||
} else if (this.activePage == this.typePage) {
|
||||
this.activePage.hide();
|
||||
this.connectionsTypePage.show();
|
||||
/* Connection type page */
|
||||
} else if (this.activePage == this.connectionsTypePage) {
|
||||
if (this.getActiveInstance()) {
|
||||
if (this.getActiveInstance().connectionsType === 'auto') {
|
||||
this.activePage.hide();
|
||||
this.passwordsPage.show();
|
||||
}
|
||||
else {
|
||||
this.activePage.hide();
|
||||
this.connectionsPage.show();
|
||||
(this.getMode() === 'basic'? this.connectionsPage: this.expertSettingsPage).getElement().querySelector(".backend-address .checkbox").classList.toggle("checked", this.getActiveInstance().backendAddress === '*')
|
||||
}
|
||||
} else {
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`)
|
||||
}
|
||||
/* Connection page */
|
||||
} else if (this.activePage == this.connectionsPage) {
|
||||
if (await this.checkPorts()) {
|
||||
this.activePage.hide();
|
||||
this.passwordsPage.show();
|
||||
}
|
||||
/* Passwords page */
|
||||
} else if (this.activePage == this.passwordsPage) {
|
||||
if (await this.checkPasswords()) {
|
||||
this.activePage.hide();
|
||||
this.getState() === 'INSTALL' ? this.getActiveInstance().install() : this.getActiveInstance().edit();
|
||||
}
|
||||
/* Expert settings page */
|
||||
} else if (this.activePage == this.expertSettingsPage) {
|
||||
if (await this.checkPorts() && await this.checkPasswords()) {
|
||||
this.activePage.hide();
|
||||
this.getState() === 'INSTALL' ? this.getActiveInstance().install() : this.getActiveInstance().edit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* When the back button of a wizard page is clicked */
|
||||
async onBackClicked() {
|
||||
this.activePage.hide();
|
||||
|
||||
/* If we have backed to the menu, instances or settings page, reset the active instance */
|
||||
if ([this.instancesPage, this.settingsPage].includes(this.activePage.previousPage)) {
|
||||
await this.setState('IDLE');
|
||||
}
|
||||
|
||||
this.activePage.previousPage.show(true); // Don't change the previous page (or we get stuck in a loop)
|
||||
this.updateInstances();
|
||||
}
|
||||
|
||||
async onCancelClicked() {
|
||||
this.activePage.hide();
|
||||
await this.setState('IDLE');
|
||||
if (this.getMode() === "basic")
|
||||
this.menuPage.show(true);
|
||||
else
|
||||
this.instancesPage.show(true);
|
||||
this.updateInstances();
|
||||
}
|
||||
|
||||
async onGameMasterPasswordChanged(value) {
|
||||
for (let input of this.activePage.getElement().querySelectorAll("input[type='password']")) {
|
||||
input.placeholder = "";
|
||||
}
|
||||
|
||||
if (this.getActiveInstance())
|
||||
this.getActiveInstance().setGameMasterPassword(value);
|
||||
else
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`);
|
||||
}
|
||||
|
||||
async onBlueCommanderPasswordChanged(value) {
|
||||
for (let input of this.activePage.getElement().querySelectorAll("input[type='password']")) {
|
||||
input.placeholder = "";
|
||||
}
|
||||
|
||||
if (this.getActiveInstance())
|
||||
this.getActiveInstance().setBlueCommanderPassword(value);
|
||||
else
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`);
|
||||
}
|
||||
|
||||
async onRedCommanderPasswordChanged(value) {
|
||||
for (let input of this.activePage.getElement().querySelectorAll("input[type='password']")) {
|
||||
input.placeholder = "";
|
||||
}
|
||||
|
||||
if (this.getActiveInstance())
|
||||
this.getActiveInstance().setRedCommanderPassword(value);
|
||||
else
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`);
|
||||
}
|
||||
|
||||
/* When the client port input value is changed */
|
||||
async onClientPortChanged(value) {
|
||||
this.setPort('client', Number(value));
|
||||
}
|
||||
|
||||
/* When the backend port input value is changed */
|
||||
async onBackendPortChanged(value) {
|
||||
this.setPort('backend', Number(value));
|
||||
}
|
||||
|
||||
/* When the "Enable API connection" checkbox is clicked */
|
||||
async onEnableAPIClicked() {
|
||||
if (this.getActiveInstance()) {
|
||||
if (this.getActiveInstance().backendAddress === 'localhost') {
|
||||
this.getActiveInstance().backendAddress = '*';
|
||||
} else {
|
||||
this.getActiveInstance().backendAddress = 'localhost';
|
||||
}
|
||||
if (this.getMode() === 'basic') {
|
||||
this.connectionsPage.getElement().querySelector(".note.warning").classList.toggle("hide", this.getActiveInstance().backendAddress !== '*')
|
||||
this.connectionsPage.getElement().querySelector(".backend-address .checkbox").classList.toggle("checked", this.getActiveInstance().backendAddress === '*')
|
||||
} else {
|
||||
this.expertSettingsPage.getElement().querySelector(".backend-address .checkbox").classList.toggle("checked", this.getActiveInstance().backendAddress === '*')
|
||||
}
|
||||
} else {
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`)
|
||||
}
|
||||
}
|
||||
|
||||
/* When the "Return to manager" button is pressed */
|
||||
async onReturnClicked() {
|
||||
await this.reload();
|
||||
this.activePage.hide();
|
||||
this.menuPage.show();
|
||||
}
|
||||
|
||||
/* When the "Close manager" button is pressed */
|
||||
async onCloseManagerClicked() {
|
||||
document.querySelector('.close').click();
|
||||
}
|
||||
|
||||
async checkPorts() {
|
||||
var clientPortFree = await this.getActiveInstance().checkClientPort();
|
||||
var backendPortFree = await this.getActiveInstance().checkBackendPort();
|
||||
if (clientPortFree && backendPortFree) {
|
||||
return true;
|
||||
} else {
|
||||
showErrorPopup(`<div class='main-message'> Please, make sure both the client and backend ports are free!</div><div class='sub-message'>If ports are already in use, Olympus will not be able to communicated correctly.</div>`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async checkPasswords() {
|
||||
if (this.getActiveInstance()) {
|
||||
if (this.getActiveInstance().installed && !this.getActiveInstance().arePasswordsEdited()) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
if (!this.getActiveInstance().arePasswordsSet()) {
|
||||
showErrorPopup(`<div class='main-message'>Please, make sure all passwords are set!</div><div class='sub-message'>The role users will fulfill depends on the password they enter at login. </div>`);
|
||||
return false;
|
||||
} else if (!this.getActiveInstance().arePasswordsDifferent()) {
|
||||
showErrorPopup(`<div class='main-message'>Please, set different passwords! </div><div class='sub-message'>The role users will fulfill depends on the password they enter at login. </div>`);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async onStartServerClicked(name) {
|
||||
var div = await this.getClickedInstanceDiv(name);
|
||||
div.querySelector(".collapse").classList.add("loading")
|
||||
var instance = await this.getClickedInstance(name);
|
||||
instance.startServer();
|
||||
}
|
||||
|
||||
async onStartClientClicked(name) {
|
||||
var div = await this.getClickedInstanceDiv(name);
|
||||
div.querySelector(".collapse").classList.add("loading")
|
||||
var instance = await this.getClickedInstance(name);
|
||||
instance.startClient();
|
||||
}
|
||||
|
||||
async onOpenBrowserClicked(name) {
|
||||
var instance = await this.getClickedInstance(name);
|
||||
exec(`start http://localhost:${instance.clientPort}`)
|
||||
}
|
||||
|
||||
async onStopClicked(name) {
|
||||
var instance = await this.getClickedInstance(name);
|
||||
instance.stop();
|
||||
}
|
||||
|
||||
async onEditClicked(name) {
|
||||
var instance = await this.getClickedInstance(name);
|
||||
if (instance.webserverOnline || instance.backendOnline) {
|
||||
showErrorPopup("<div class='main-message'>The selected Olympus instance is currently active </div><div class='sub-message'> Please stop DCS and Olympus Server/Client before editing it! </div>")
|
||||
} else {
|
||||
this.setActiveInstance(instance);
|
||||
await this.setState('EDIT');
|
||||
this.activePage.hide();
|
||||
(this.getMode() === 'basic'? this.typePage: this.expertSettingsPage).show();
|
||||
}
|
||||
}
|
||||
|
||||
async onInstallClicked(name) {
|
||||
var instance = await this.getClickedInstance(name);
|
||||
this.setActiveInstance(instance);
|
||||
await this.setState('INSTALL');
|
||||
this.activePage.hide();
|
||||
(this.getMode() === 'basic'? this.typePage: this.expertSettingsPage).show();
|
||||
}
|
||||
|
||||
async onUninstallClicked(name) {
|
||||
var instance = await this.getClickedInstance(name);
|
||||
this.setActiveInstance(instance);
|
||||
await this.setState('UNINSTALL');
|
||||
if (instance.webserverOnline || instance.backendOnline)
|
||||
showErrorPopup("<div class='main-message'>The selected Olympus instance is currently active </div><div class='sub-message'> Please stop DCS and Olympus Server/Client before removing it! </div>")
|
||||
else
|
||||
await instance.uninstall();
|
||||
}
|
||||
|
||||
async onLinkClicked(url) {
|
||||
exec(`start ${url}`);
|
||||
}
|
||||
|
||||
async onTextFileClicked(path) {
|
||||
exec(`notepad "${path}"`);
|
||||
}
|
||||
|
||||
async getClickedInstance(name) {
|
||||
var instances = await DCSInstance.getInstances()
|
||||
return instances.find((instance) => { return instance.name === name; });
|
||||
}
|
||||
|
||||
async getClickedInstanceDiv(name) {
|
||||
var instance = await this.getClickedInstance(name);
|
||||
var instanceDivs = this.instancesPage.getElement().querySelectorAll(`.option`);
|
||||
for (let i = 0; i < instanceDivs.length; i++) {
|
||||
var instanceDiv = instanceDivs[i];
|
||||
if (instanceDiv.dataset.folder === instance.folder) {
|
||||
return instanceDiv;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Set the selected port to the dcs instance */
|
||||
async setPort(port, value) {
|
||||
var success;
|
||||
if (port === 'client') {
|
||||
success = await this.getActiveInstance().checkClientPort(value);
|
||||
this.getActiveInstance().setClientPort(value);
|
||||
}
|
||||
else {
|
||||
success = await this.getActiveInstance().checkBackendPort(value);
|
||||
this.getActiveInstance().setBackendPort(value);
|
||||
}
|
||||
|
||||
var successEls = (this.getMode() === 'basic'? this.connectionsPage: this.expertSettingsPage).getElement().querySelector(`.${port}-port`).querySelectorAll(".success");
|
||||
for (let i = 0; i < successEls.length; i++) {
|
||||
successEls[i].classList.toggle("hide", !success);
|
||||
}
|
||||
var errorEls = (this.getMode() === 'basic'? this.connectionsPage: this.expertSettingsPage).getElement().querySelector(`.${port}-port`).querySelectorAll(".error");
|
||||
for (let i = 0; i < errorEls.length; i++) {
|
||||
errorEls[i].classList.toggle("hide", success);
|
||||
}
|
||||
}
|
||||
|
||||
async getPublicIP() {
|
||||
const res = await fetchWithTimeout("https://ipecho.io/json", { timeout: 2500 });
|
||||
const data = await res.json();
|
||||
return data.ip;
|
||||
}
|
||||
|
||||
async updateInstances() {
|
||||
var instanceDivs = this.instancesPage.getElement().querySelectorAll(`.option`);
|
||||
for (let i = 0; i < instanceDivs.length; i++) {
|
||||
var instanceDiv = instanceDivs[i];
|
||||
var instance = this.getInstances().find((instance) => { return instance.folder === instanceDivs[i].dataset.folder; })
|
||||
if (instance) {
|
||||
instanceDiv.querySelector(".button.install").classList.toggle("hide", instance.installed);
|
||||
instanceDiv.querySelector(".button.start").classList.toggle("hide", !instance.installed);
|
||||
instanceDiv.querySelector(".button.uninstall").classList.toggle("hide", !instance.installed);
|
||||
instanceDiv.querySelector(".button.edit").classList.toggle("hide", !instance.installed);
|
||||
|
||||
if (instance.installed) {
|
||||
if (instanceDiv.querySelector(".webserver.online") !== null) {
|
||||
instanceDiv.querySelector(".webserver.online").classList.toggle("hide", !instance.webserverOnline);
|
||||
instanceDiv.querySelector(".webserver.offline").classList.toggle("hide", instance.webserverOnline);
|
||||
instanceDiv.querySelector(".backend.online").classList.toggle("hide", !instance.backendOnline);
|
||||
instanceDiv.querySelector(".backend.offline").classList.toggle("hide", instance.backendOnline);
|
||||
|
||||
if (instance.backendOnline) {
|
||||
instanceDiv.querySelector(".fps .data").innerText = instance.fps;
|
||||
instanceDiv.querySelector(".load .data").innerText = instance.load;
|
||||
}
|
||||
|
||||
instanceDiv.querySelector(".button.start").classList.toggle("hide", instance.webserverOnline);
|
||||
instanceDiv.querySelector(".button.uninstall").classList.toggle("hide", instance.webserverOnline);
|
||||
instanceDiv.querySelector(".button.edit").classList.toggle("hide", instance.webserverOnline);
|
||||
instanceDiv.querySelector(".button.open-browser").classList.toggle("hide", !instance.webserverOnline);
|
||||
instanceDiv.querySelector(".button.stop").classList.toggle("hide", !instance.webserverOnline);
|
||||
|
||||
if (instance.webserverOnline)
|
||||
instanceDiv.querySelector(".button.start").classList.remove("loading");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async reload() {
|
||||
await DCSInstance.reloadInstances();
|
||||
|
||||
this.options.installEnabled = true;
|
||||
this.options.editEnabled = this.getInstances().find(instance => instance.installed);
|
||||
}
|
||||
|
||||
async setLoadingProgress(message, percent) {
|
||||
document.querySelector("#loader .loading-message").innerHTML = message;
|
||||
if (percent) {
|
||||
var style = document.querySelector('#loader .loading-bar').style;
|
||||
style.setProperty('--percent', `${percent}%`);
|
||||
}
|
||||
}
|
||||
|
||||
async hideLoadingPage() {
|
||||
/* Hide the loading page */
|
||||
document.getElementById("loader").style.opacity = "0%";
|
||||
window.setTimeout(() => {
|
||||
document.getElementById("loader").classList.add("hide");
|
||||
}, 250);
|
||||
}
|
||||
|
||||
async setActiveInstance(newActiveInstance) {
|
||||
this.options.activeInstance = newActiveInstance;
|
||||
}
|
||||
|
||||
async setAdditionalDCSInstances(newAdditionalDCSInstances) {
|
||||
this.options.additionalDCSInstances = newAdditionalDCSInstances;
|
||||
}
|
||||
|
||||
async setConfigLoaded(newConfigLoaded) {
|
||||
this.options.configLoaded = newConfigLoaded;
|
||||
}
|
||||
|
||||
async setInstances(newInstances) {
|
||||
this.options.instances = newInstances;
|
||||
}
|
||||
|
||||
async setIP(newIP) {
|
||||
this.options.IP = newIP;
|
||||
}
|
||||
|
||||
async setLogLocation(newLogLocation) {
|
||||
this.options.logLocation = newLogLocation;
|
||||
}
|
||||
|
||||
async setState(newState) {
|
||||
this.options.state = newState;
|
||||
await DCSInstance.reloadInstances();
|
||||
if (newState === 'IDLE')
|
||||
this.setActiveInstance(undefined);
|
||||
}
|
||||
|
||||
/** Get the currently active instance, i.e. the instance that is being edited/installed/removed
|
||||
*
|
||||
* @returns The active instance
|
||||
*/
|
||||
getActiveInstance() {
|
||||
return this.options.activeInstance;
|
||||
}
|
||||
|
||||
getAdditionalDCSInstances() {
|
||||
return this.options.additionalDCSInstances
|
||||
}
|
||||
|
||||
getConfigLoaded() {
|
||||
return this.options.configLoaded;
|
||||
}
|
||||
|
||||
getInstances() {
|
||||
return this.options.instances;
|
||||
}
|
||||
|
||||
getIP() {
|
||||
return this.options.IP;
|
||||
}
|
||||
|
||||
getLogLocation() {
|
||||
return this.options.logLocation;
|
||||
}
|
||||
|
||||
getState() {
|
||||
return this.options.state;
|
||||
}
|
||||
|
||||
getMode() {
|
||||
return this.options.mode;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Manager;
|
||||
15
manager/javascripts/managerfactory.js
Normal file
@ -0,0 +1,15 @@
|
||||
/* TODO: find a better solution without using the window object to persist the manager singleton */
|
||||
|
||||
function getManager() {
|
||||
if (window.manager) {
|
||||
return window.manager;
|
||||
} else {
|
||||
const Manager = require("./manager");
|
||||
window.manager = new Manager();
|
||||
return window.manager;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getManager: getManager
|
||||
};
|
||||
@ -1,33 +1,64 @@
|
||||
class ManagerPage {
|
||||
element;
|
||||
options;
|
||||
const { logger } = require("./filesystem");
|
||||
const ejs = require('ejs')
|
||||
|
||||
constructor(options) {
|
||||
this.options = options ?? {};
|
||||
class ManagerPage {
|
||||
manager;
|
||||
ejsFile;
|
||||
element;
|
||||
options = {};
|
||||
previousPage;
|
||||
|
||||
constructor(manager, ejsFile) {
|
||||
this.manager = manager;
|
||||
this.element = document.createElement('div');
|
||||
this.element.classList.add("manager-page", "hide");
|
||||
this.ejsFile = ejsFile;
|
||||
document.body.appendChild(this.element);
|
||||
}
|
||||
|
||||
getElement() {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
show() {
|
||||
this.element.classList.remove("hide");
|
||||
show(ignorePreviousPage) {
|
||||
ejs.renderFile(this.ejsFile, {...this.options, ...this.manager.options}, {}, (err, str) => {
|
||||
if (!err) {
|
||||
this.render(str);
|
||||
} else {
|
||||
logger.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
this.previousPage = ignorePreviousPage ? this.previousPage : this.manager.activePage;
|
||||
this.manager.activePage = this;
|
||||
|
||||
if (this.options.onShow)
|
||||
this.options.onShow();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.element.classList.add("hide");
|
||||
this.element.style.opacity = "0%";
|
||||
window.setTimeout(() => {
|
||||
this.element.classList.add("hide");
|
||||
}, 250);
|
||||
}
|
||||
|
||||
render() {
|
||||
render(str) {
|
||||
this.element.innerHTML = str;
|
||||
this.element.style.opacity = "0%";
|
||||
|
||||
this.element.classList.remove("hide");
|
||||
window.setTimeout(() => {
|
||||
this.element.style.opacity = "100%";
|
||||
}, 0)
|
||||
|
||||
/* Connect all the collapsable buttons */
|
||||
let buttons = document.querySelectorAll(".button.collapse");
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
buttons[i].addEventListener("click", () => {
|
||||
buttons[i].classList.toggle("open");
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
const ManagerPage = require("./managerpage");
|
||||
const ejs = require('ejs')
|
||||
const { logger } = require("./filesystem")
|
||||
|
||||
class MenuPage extends ManagerPage {
|
||||
onInstallClicked;
|
||||
onUpdateClicked;
|
||||
onManageClicked;
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
render(str) {
|
||||
const element = this.getElement();
|
||||
element.innerHTML = str;
|
||||
|
||||
element.querySelector(".install").addEventListener("click", (e) => this.onInstallClicked(e));
|
||||
element.querySelector(".manage").addEventListener("click", (e) => this.onManageClicked(e));
|
||||
|
||||
super.render();
|
||||
}
|
||||
|
||||
show() {
|
||||
this.instance = this.options.instance;
|
||||
|
||||
ejs.renderFile("./ejs/menu.ejs", this.options, {}, (err, str) => {
|
||||
if (!err) {
|
||||
this.render(str);
|
||||
} else {
|
||||
logger.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
super.show();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MenuPage;
|
||||
@ -1,18 +1,27 @@
|
||||
const portfinder = require('portfinder')
|
||||
const { logger } = require("./filesystem")
|
||||
const portfinder = require('portfinder');
|
||||
const { logger } = require('./filesystem');
|
||||
|
||||
/** Checks if a port is already in use
|
||||
*
|
||||
*/
|
||||
function checkPort(port, callback) {
|
||||
portfinder.getPort({ port: port, stopPort: port }, (err, res) => {
|
||||
if (err !== null) {
|
||||
logger.error(`Port ${port} already in use`);
|
||||
callback(false);
|
||||
} else {
|
||||
callback(true);
|
||||
}
|
||||
});
|
||||
async function checkPort(port) {
|
||||
try{
|
||||
await portfinder.getPortPromise({ port: port, stopPort: port });
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.log(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getFreePort(startPort) {
|
||||
try{
|
||||
var port = await portfinder.getPortPromise({ port: startPort });
|
||||
return port;
|
||||
} catch (err) {
|
||||
logger.log(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Performs a fetch request, with a configurable timeout
|
||||
@ -34,6 +43,7 @@ async function fetchWithTimeout(resource, options = {}) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getFreePort: getFreePort,
|
||||
checkPort: checkPort,
|
||||
fetchWithTimeout: fetchWithTimeout
|
||||
}
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
const ManagerPage = require("./managerpage");
|
||||
const ejs = require('ejs')
|
||||
const { logger } = require("./filesystem")
|
||||
|
||||
class PasswordsPage extends ManagerPage {
|
||||
onBackClicked;
|
||||
onNextClicked;
|
||||
onCancelClicked;
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
render(str) {
|
||||
const element = this.getElement();
|
||||
element.innerHTML = str;
|
||||
|
||||
if (this.element.querySelector(".back"))
|
||||
this.element.querySelector(".back").addEventListener("click", (e) => this.onBackClicked(e));
|
||||
|
||||
if (this.element.querySelector(".next"))
|
||||
this.element.querySelector(".next").addEventListener("click", (e) => this.onNextClicked(e));
|
||||
|
||||
if (this.element.querySelector(".cancel"))
|
||||
this.element.querySelector(".cancel").addEventListener("click", (e) => this.onCancelClicked(e));
|
||||
|
||||
this.element.querySelector(".game-master").querySelector("input").addEventListener("change", async (e) => { this.instance.setGameMasterPassword(e.target.value); })
|
||||
this.element.querySelector(".blue-commander").querySelector("input").addEventListener("change", async (e) => { this.instance.setBlueCommanderPassword(e.target.value); })
|
||||
this.element.querySelector(".red-commander").querySelector("input").addEventListener("change", async (e) => { this.instance.setRedCommanderPassword(e.target.value); })
|
||||
|
||||
super.render();
|
||||
}
|
||||
|
||||
show() {
|
||||
this.instance = this.options.instance;
|
||||
|
||||
ejs.renderFile("./ejs/passwords.ejs", this.options, {}, (err, str) => {
|
||||
if (!err) {
|
||||
this.render(str);
|
||||
} else {
|
||||
logger.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
super.show();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PasswordsPage;
|
||||
@ -1,8 +1,25 @@
|
||||
// TODO: we can probably refactor this to be a bit cleaner
|
||||
|
||||
function showInfoPopup(message, onCloseCallback) {
|
||||
showPopup();
|
||||
document.getElementById("popup").querySelector(".error").classList.add("hide");
|
||||
document.getElementById("popup").querySelector(".wait").classList.add("hide");
|
||||
document.getElementById("popup").querySelector(".confirm").classList.remove("hide");
|
||||
document.getElementById("popup").querySelector(".close-popup").classList.remove("hide");
|
||||
document.getElementById("popup").querySelector(".accept-popup").classList.add("hide");
|
||||
|
||||
/* Not using event listeners to make sure we only have one callback */
|
||||
document.getElementById("popup").querySelector(".close-popup").onclick = (e) => {
|
||||
hidePopup();
|
||||
if (onCloseCallback)
|
||||
onCloseCallback();
|
||||
}
|
||||
document.getElementById("popup").querySelector(".content").innerHTML = message;
|
||||
}
|
||||
|
||||
|
||||
function showErrorPopup(message, onCloseCallback) {
|
||||
document.getElementById("grayout").classList.remove("hide");
|
||||
document.getElementById("popup").classList.remove("hide");
|
||||
showPopup();
|
||||
document.getElementById("popup").querySelector(".error").classList.remove("hide");
|
||||
document.getElementById("popup").querySelector(".wait").classList.add("hide");
|
||||
document.getElementById("popup").querySelector(".confirm").classList.add("hide");
|
||||
@ -19,8 +36,7 @@ function showErrorPopup(message, onCloseCallback) {
|
||||
}
|
||||
|
||||
function showWaitPopup(message) {
|
||||
document.getElementById("grayout").classList.remove("hide");
|
||||
document.getElementById("popup").classList.remove("hide");
|
||||
showPopup();
|
||||
document.getElementById("popup").querySelector(".error").classList.add("hide");
|
||||
document.getElementById("popup").querySelector(".wait").classList.remove("hide");
|
||||
document.getElementById("popup").querySelector(".confirm").classList.add("hide");
|
||||
@ -29,9 +45,18 @@ function showWaitPopup(message) {
|
||||
document.getElementById("popup").querySelector(".content").innerHTML = message;
|
||||
}
|
||||
|
||||
function showWaitLoadingPopup(message) {
|
||||
showPopup();
|
||||
document.getElementById("popup").querySelector(".error").classList.add("hide");
|
||||
document.getElementById("popup").querySelector(".wait").classList.remove("hide");
|
||||
document.getElementById("popup").querySelector(".confirm").classList.add("hide");
|
||||
document.getElementById("popup").querySelector(".close-popup").classList.add("hide");
|
||||
document.getElementById("popup").querySelector(".accept-popup").classList.add("hide");
|
||||
document.getElementById("popup").querySelector(".content").innerHTML = `${message}<div class="loading-bar" style="width: 100%; height: 10px;"></div><div class="loading-message" style="font-weight: normal; text-align: center;"></div>` ;
|
||||
}
|
||||
|
||||
function showConfirmPopup(message, onAcceptCallback, onCloseCallback) {
|
||||
document.getElementById("grayout").classList.remove("hide");
|
||||
document.getElementById("popup").classList.remove("hide");
|
||||
showPopup();
|
||||
document.getElementById("popup").querySelector(".error").classList.add("hide");
|
||||
document.getElementById("popup").querySelector(".wait").classList.add("hide");
|
||||
document.getElementById("popup").querySelector(".confirm").classList.remove("hide");
|
||||
@ -55,14 +80,40 @@ function showConfirmPopup(message, onAcceptCallback, onCloseCallback) {
|
||||
document.getElementById("popup").querySelector(".content").innerHTML = message;
|
||||
}
|
||||
|
||||
function showPopup() {
|
||||
document.getElementById("grayout").classList.remove("hide");
|
||||
document.getElementById("popup").classList.remove("hide");
|
||||
|
||||
window.setTimeout(() => {
|
||||
document.getElementById("grayout").style.opacity = "100%";
|
||||
document.getElementById("popup").style.opacity = "100%";
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function hidePopup() {
|
||||
document.getElementById("grayout").classList.add("hide");
|
||||
document.getElementById("popup").classList.add("hide");
|
||||
document.getElementById("grayout").style.opacity = "0%";
|
||||
document.getElementById("popup").style.opacity = "0%";
|
||||
|
||||
window.setTimeout(() => {
|
||||
document.getElementById("grayout").classList.add("hide");
|
||||
document.getElementById("popup").classList.add("hide");
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function setPopupLoadingProgress(message, percent) {
|
||||
document.querySelector("#popup .loading-message").innerHTML = message;
|
||||
if (percent) {
|
||||
var style = document.querySelector('#popup .loading-bar').style;
|
||||
style.setProperty('--percent', `${percent}%`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
showInfoPopup: showInfoPopup,
|
||||
showErrorPopup: showErrorPopup,
|
||||
showConfirmPopup: showConfirmPopup,
|
||||
showWaitPopup: showWaitPopup,
|
||||
hidePopup: hidePopup
|
||||
showWaitLoadingPopup: showWaitLoadingPopup,
|
||||
hidePopup: hidePopup,
|
||||
setPopupLoadingProgress: setPopupLoadingProgress
|
||||
}
|
||||
@ -1,5 +1,3 @@
|
||||
const Manager = require('./manager');
|
||||
|
||||
const contextBridge = require('electron').contextBridge;
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
const { exec, spawn } = require("child_process");
|
||||
@ -10,7 +8,9 @@ const https = require('follow-redirects').https;
|
||||
const fs = require('fs');
|
||||
const AdmZip = require("adm-zip");
|
||||
const { Octokit } = require('octokit');
|
||||
const { logger } = require("./filesystem")
|
||||
const { logger } = require("./filesystem");
|
||||
const { getManager } = require('./managerfactory');
|
||||
const { sleep } = require('./utils');
|
||||
|
||||
const VERSION = "{{OLYMPUS_VERSION_NUMBER}}";
|
||||
logger.log(`Running in ${__dirname}`);
|
||||
@ -35,7 +35,7 @@ function checkVersion() {
|
||||
/* If a newer version is available update Olympus in Release mode */
|
||||
if (reg1[0] > reg2[0] || (reg1[0] == reg2[0] && reg1[1] > reg2[1]) || (reg1[0] == reg2[0] && reg1[1] == reg2[1] && reg1[2] > reg2[2])) {
|
||||
logger.log(`New version available: ${res["version"]}`);
|
||||
showConfirmPopup(`You are currently running DCS Olympus ${VERSION}, but ${res["version"]} is available. Do you want to update DCS Olympus automatically? <div style="max-width: 100%; color: orange">Note: DCS and Olympus MUST be stopped before proceeding.</div>`,
|
||||
showConfirmPopup(`<div class='main-message'>You are currently running DCS Olympus ${VERSION}, but ${res["version"]} is available. </div><div class='sub-message'> Do you want to update DCS Olympus automatically? </div> <div style="max-width: 100%; color: orange">Note: DCS and Olympus MUST be stopped before proceeding.</div>`,
|
||||
() => {
|
||||
updateOlympusRelease();
|
||||
}, () => {
|
||||
@ -45,12 +45,7 @@ function checkVersion() {
|
||||
/* If the current version is newer than the latest release, the user is probably a developer. Ask for a beta update */
|
||||
else if (reg2[0] > reg1[0] || (reg2[0] == reg1[0] && reg2[1] > reg1[1]) || (reg2[0] == reg1[0] && reg2[1] == reg1[1] && reg2[2] > reg1[2])) {
|
||||
logger.log(`Beta version detected: ${res["version"]} vs ${VERSION}`);
|
||||
showConfirmPopup(`You are currently running DCS Olympus ${VERSION}, which is newer than the latest release version. Do you want to download the latest beta version? <div style="max-width: 100%; color: orange">Note: DCS and Olympus MUST be stopped before proceeding.</div>`,
|
||||
() => {
|
||||
updateOlympusBeta();
|
||||
}, () => {
|
||||
logger.log("Update canceled");
|
||||
})
|
||||
updateOlympusBeta();
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -74,27 +69,37 @@ async function updateOlympusBeta() {
|
||||
/* Select the newest artifact */
|
||||
var artifact = artifacts.find((artifact) => { return artifact.name = "development_build_not_a_release" });
|
||||
|
||||
showConfirmPopup(`Latest beta artifact has a timestamp of ${artifact.updated_at}. Do you want to continue?`, () => {
|
||||
/* Run the browser and download the artifact */ //TODO: try and directly download the file from code rather than using the browser
|
||||
exec(`start https://github.com/Pax1601/DCSOlympus/actions/runs/${artifact.workflow_run.id}/artifacts/${artifact.id}`)
|
||||
showConfirmPopup('A browser window was opened to download the beta artifact. Please wait for the download to complete, then press "Accept" and select the downloaded beta artifact.',
|
||||
() => {
|
||||
/* Ask the user to select the downloaded file */
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.click();
|
||||
input.onchange = e => {
|
||||
/* Run the update process */
|
||||
updateOlympus(e.target.files[0])
|
||||
}
|
||||
const date1 = new Date(artifact.updated_at);
|
||||
const date2 = fs.statSync(".").mtime;
|
||||
if (date1 > date2) {
|
||||
showConfirmPopup(`<div class='main-message'> Looks like you are running a beta version of Olympus!</div><div class='sub-message'> Latest beta artifact timestamp of: <b style="color: orange">${date1.toLocaleString()}</b> <br> Your installation timestamp: <b style="color: orange">${date2.toLocaleString()}</b> <br><br> Do you want to update to the newest beta version?</div>`, async () => {
|
||||
/* Nested popup calls need to wait for animation to complete */
|
||||
await sleep(300);
|
||||
|
||||
/* Run the browser and download the artifact */ //TODO: try and directly download the file from code rather than using the browser
|
||||
exec(`start https://github.com/Pax1601/DCSOlympus/actions/runs/${artifact.workflow_run.id}/artifacts/${artifact.id}`)
|
||||
showConfirmPopup(`<div class='main-message'> A browser window was opened to download the beta artifact. </div><div class='sub-message'> Please wait for the download to complete, then press "Accept" and select the downloaded beta artifact.</div>`,
|
||||
() => {
|
||||
/* Ask the user to select the downloaded file */
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.click();
|
||||
input.onchange = e => {
|
||||
/* Run the update process */
|
||||
updateOlympus(e.target.files[0])
|
||||
}
|
||||
},
|
||||
() => {
|
||||
logger.log("Update canceled");
|
||||
});
|
||||
},
|
||||
() => {
|
||||
logger.log("Update canceled");
|
||||
});
|
||||
},
|
||||
() => {
|
||||
logger.log("Update canceled");
|
||||
})
|
||||
}
|
||||
)
|
||||
} else {
|
||||
logger.log("Build is latest")
|
||||
}
|
||||
}
|
||||
|
||||
/** Update Olympus to the lastest release
|
||||
@ -116,7 +121,7 @@ async function updateOlympusRelease() {
|
||||
}
|
||||
|
||||
function updateOlympus(location) {
|
||||
showWaitPopup("Please wait while Olympus is being updated. The Manager will be closed and reopened automatically when updating is completed.")
|
||||
showWaitPopup("<div class='main-message'>Please wait while Olympus is being updated. </div><div class='sub-message'> The Manager will be closed and reopened automatically when updating is completed.</div>")
|
||||
|
||||
/* If the location is a string, it is interpreted as a download url. Else, it is interpreted as a File (on disk)*/
|
||||
if (typeof location === "string") {
|
||||
@ -201,7 +206,7 @@ function extractAndCopy(folder) {
|
||||
*
|
||||
*/
|
||||
function failUpdate() {
|
||||
showErrorPopup(`An error has occurred while updating Olympus. Please delete Olympus and update it manually. A browser window will open automatically on the download page. <br><br> You can find more info in ${path.join(__dirname, "..", "manager.log")}`, () => {
|
||||
showErrorPopup(`<div class='main-message'>An error has occurred while updating Olympus. </div><div class='sub-message'> Please delete Olympus and update it manually. A browser window will open automatically on the download page. <br><br> You can find more info in ${path.join(__dirname, "..", "manager.log")}</div>`, () => {
|
||||
exec(`start https://github.com/Pax1601/DCSOlympus/releases`, () => {
|
||||
ipcRenderer.send('window:close');
|
||||
})
|
||||
@ -221,8 +226,7 @@ const ipc = {
|
||||
/* From main to render. */
|
||||
'receive': [
|
||||
'event:maximized',
|
||||
'event:unmaximized',
|
||||
'check-version'
|
||||
'event:unmaximized'
|
||||
],
|
||||
/* From render to main and back again. */
|
||||
'sendReceive': []
|
||||
@ -257,28 +261,25 @@ contextBridge.exposeInMainWorld(
|
||||
}
|
||||
});
|
||||
|
||||
/* New instance of the manager app */
|
||||
const manager = new Manager();
|
||||
|
||||
/* On content loaded */
|
||||
window.addEventListener('DOMContentLoaded', async () => {
|
||||
/* Compute the height of the content page */
|
||||
computePagesHeight();
|
||||
document.getElementById("loader").classList.remove("hide");
|
||||
await manager.start();
|
||||
/* Compute the height of the content page to account for the pages created by the manager*/
|
||||
computePagesHeight();
|
||||
|
||||
/* Create event listeners for the hyperlinks */
|
||||
var links = document.querySelectorAll(".link");
|
||||
for (let i = 0; i < links.length; i++) {
|
||||
links[i].addEventListener("click", (e) => {
|
||||
exec("start " + e.target.dataset.link);
|
||||
})
|
||||
}
|
||||
await getManager().start();
|
||||
await checkVersion();
|
||||
})
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
/* Compute the height of the content page */
|
||||
computePagesHeight();
|
||||
})
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
/* Compute the height of the content page */
|
||||
computePagesHeight();
|
||||
})
|
||||
|
||||
document.addEventListener('managerStarted', () => {
|
||||
/* Compute the height of the content page */
|
||||
computePagesHeight();
|
||||
})
|
||||
|
||||
@ -294,8 +295,3 @@ function computePagesHeight() {
|
||||
pages[i].style.height = (window.innerHeight - (titleBar.clientHeight + header.clientHeight)) + "px";
|
||||
}
|
||||
}
|
||||
|
||||
ipcRenderer.on("check-version", () => {
|
||||
/* Check if a new version is available */
|
||||
checkVersion();
|
||||
})
|
||||
@ -1,117 +0,0 @@
|
||||
const { installMod, installHooks, installJSON, applyConfiguration, installShortCuts } = require("./filesystem");
|
||||
const ManagerPage = require("./managerpage");
|
||||
const ejs = require('ejs')
|
||||
const { logger } = require("./filesystem")
|
||||
|
||||
class ResultPage extends ManagerPage {
|
||||
onBackClicked;
|
||||
onNextClicked;
|
||||
onCancelClicked;
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
render(str) {
|
||||
const element = this.getElement();
|
||||
element.innerHTML = str;
|
||||
|
||||
this.element.querySelector(".back").addEventListener("click", (e) => this.onBackClicked(e));
|
||||
|
||||
super.render();
|
||||
}
|
||||
|
||||
show() {
|
||||
this.instance = this.options.instance;
|
||||
|
||||
ejs.renderFile("./ejs/result.ejs", this.options, {}, (err, str) => {
|
||||
if (!err) {
|
||||
this.render(str);
|
||||
} else {
|
||||
logger.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
super.show();
|
||||
}
|
||||
|
||||
/** Installation is performed by using an then chain of async functions. Installation is aborted on any error along the chain
|
||||
*
|
||||
*/
|
||||
startInstallation() {
|
||||
installHooks(this.instance.folder).then(
|
||||
() => {
|
||||
this.applyStepSuccess(".hook");
|
||||
},
|
||||
(err) => {
|
||||
this.applyStepFailure(".hook");
|
||||
return Promise.reject(err);
|
||||
}
|
||||
).then(() => installMod(this.instance.folder, this.instance.name)).then(
|
||||
() => {
|
||||
this.applyStepSuccess(".mod");
|
||||
},
|
||||
(err) => {
|
||||
this.applyStepFailure(".mod");
|
||||
return Promise.reject(err);
|
||||
}
|
||||
).then(() => installJSON(this.instance.folder)).then(
|
||||
() => {
|
||||
this.applyStepSuccess(".json");
|
||||
},
|
||||
(err) => {
|
||||
this.applyStepFailure(".json");
|
||||
return Promise.reject(err);
|
||||
}
|
||||
).then(() => applyConfiguration(this.instance.folder, this.instance)).then(
|
||||
() => {
|
||||
this.applyStepSuccess(".config");
|
||||
},
|
||||
(err) => {
|
||||
this.applyStepFailure(".config");
|
||||
return Promise.reject(err);
|
||||
}
|
||||
).then(() => installShortCuts(this.instance.folder, this.instance.name)).then(
|
||||
() => {
|
||||
this.applyStepSuccess(".shortcuts");
|
||||
},
|
||||
(err) => {
|
||||
this.applyStepFailure(".shortcuts");
|
||||
return Promise.reject(err);
|
||||
}
|
||||
).then(
|
||||
() => {
|
||||
this.element.querySelector(".summary.success").classList.remove("hide");
|
||||
this.element.querySelector(".summary.error").classList.add("hide");
|
||||
this.element.querySelector(".info.success").classList.remove("hide");
|
||||
this.element.querySelector(".info.error").classList.add("hide");
|
||||
this.element.querySelector(".result .success").classList.remove("hide");
|
||||
this.element.querySelector(".result .error").classList.add("hide");
|
||||
this.element.querySelector(".result .wait").classList.add("hide");
|
||||
},
|
||||
() => {
|
||||
this.element.querySelector(".summary.success").classList.add("hide");
|
||||
this.element.querySelector(".summary.error").classList.remove("hide");
|
||||
this.element.querySelector(".info.success").classList.add("hide");
|
||||
this.element.querySelector(".info.error").classList.remove("hide");
|
||||
this.element.querySelector(".result .success").classList.add("hide");
|
||||
this.element.querySelector(".result .error").classList.remove("hide");
|
||||
this.element.querySelector(".result .wait").classList.add("hide");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
applyStepSuccess(step) {
|
||||
this.element.querySelector(step).querySelector(".success").classList.remove("hide");
|
||||
this.element.querySelector(step).querySelector(".error").classList.add("hide");
|
||||
this.element.querySelector(step).querySelector(".wait").classList.add("hide");
|
||||
}
|
||||
|
||||
applyStepFailure(step) {
|
||||
this.element.querySelector(step).querySelector(".success").classList.add("hide");
|
||||
this.element.querySelector(step).querySelector(".error").classList.remove("hide");
|
||||
this.element.querySelector(step).querySelector(".wait").classList.add("hide");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ResultPage;
|
||||
10
manager/javascripts/utils.js
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
|
||||
async function sleep(ms) {
|
||||
await new Promise(r => setTimeout(r, ms));
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sleep: sleep
|
||||
}
|
||||
26
manager/javascripts/wizardpage.js
Normal file
@ -0,0 +1,26 @@
|
||||
const ManagerPage = require("./managerpage");
|
||||
const ejs = require('ejs')
|
||||
|
||||
class WizardPage extends ManagerPage {
|
||||
contentEjsFile;
|
||||
|
||||
constructor(manager, contentEjsFile) {
|
||||
super(manager, './ejs/wizard.ejs');
|
||||
this.contentEjsFile = contentEjsFile;
|
||||
}
|
||||
|
||||
render(str) {
|
||||
super.render(str);
|
||||
|
||||
ejs.renderFile(this.contentEjsFile, {...this.options, ...this.manager.options}, {}, (err, str) => {
|
||||
if (!err) {
|
||||
this.element.querySelector(".content").innerHTML = str;
|
||||
} else {
|
||||
logger.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WizardPage;
|
||||
@ -10,8 +10,8 @@ process.env['PATH'] = process.env['PATH'] + "%WINDIR%\\System32;"
|
||||
|
||||
function createWindow() {
|
||||
const window = new electronBrowserWindow({
|
||||
width: 1500,
|
||||
height: 850,
|
||||
width: 1200,
|
||||
height: 750,
|
||||
frame: false,
|
||||
resizable: true,
|
||||
maximizable: true,
|
||||
@ -38,7 +38,6 @@ function createWindow() {
|
||||
|
||||
electronApp.on('ready', () => {
|
||||
window = createWindow();
|
||||
window.webContents.send('check-version')
|
||||
});
|
||||
|
||||
electronApp.on('window-all-closed', () => {
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"build-release": "call ./scripts/build-release.bat"
|
||||
"build-release": "call ./scripts/build-release.bat"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
@ -26,4 +26,4 @@
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
echo D|xcopy /Y /S /E .\icons ..\package\manager\icons
|
||||
echo D|xcopy /Y /S /E .\ejs ..\package\manager\ejs
|
||||
echo D|xcopy /Y /S /E .\javascripts ..\package\manager\javascripts
|
||||
echo D|xcopy /Y /S /E .\stylesheets ..\package\manager\stylesheets
|
||||
|
||||
echo F|xcopy /Y /I .\*.* ..\package\manager
|
||||
xcopy /Y /S /E .\icons ..\package\manager\icons
|
||||
xcopy /Y /S /E .\ejs ..\package\manager\ejs
|
||||
xcopy /Y /S /E .\javascripts ..\package\manager\javascripts
|
||||
xcopy /Y /S /E .\stylesheets ..\package\manager\stylesheets
|
||||
xcopy /Y /I .\*.* ..\package\manager
|
||||
|
||||
cd ..
|
||||
call node .\scripts\node\set_version_text.js
|
||||
@ -3,17 +3,26 @@
|
||||
--background-dark: #13181f;
|
||||
--background-light: #202831;
|
||||
--background-disabled: #212A34;
|
||||
--background-note: #2C3540;
|
||||
--background-warning: #3D3322;
|
||||
--background-usage: #28313A;
|
||||
--offwhite: #F2F2F2;
|
||||
--offwhite-transparent: #F2F2F255;
|
||||
--blue: #247be2;
|
||||
--red: #FF5858;
|
||||
--green: #8bff63;
|
||||
--green: #8BFF63;
|
||||
--lightgray: #cfd9e8;
|
||||
--gray: #989898;
|
||||
--darkgray: #3d4651;
|
||||
--orange: #FF7B42;
|
||||
--very-large: 18px;
|
||||
--large: 16px;
|
||||
--big: 15px;
|
||||
--normal: 13px;
|
||||
--small: 12px;
|
||||
}
|
||||
|
||||
* {
|
||||
* {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@ -31,12 +40,15 @@ body {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/************************************************/
|
||||
/* Title bar */
|
||||
/************************************************/
|
||||
#title-bar {
|
||||
content: " ";
|
||||
display: block;
|
||||
-webkit-user-select: none;
|
||||
-webkit-app-region: drag;
|
||||
height: 20px;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
@ -48,7 +60,7 @@ body {
|
||||
#title-bar>*:first-child {
|
||||
margin-right: auto;
|
||||
color: #F2F2F2AA;
|
||||
font-size: 12px;
|
||||
font-size: var(--small);
|
||||
}
|
||||
|
||||
.title-bar-button {
|
||||
@ -85,13 +97,16 @@ body {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/************************************************/
|
||||
/* Header */
|
||||
/************************************************/
|
||||
#header {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
color: #F2F2F2;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
font-size: var(--big);
|
||||
padding: 20px 20px 20px 20px;
|
||||
column-gap: 10px;
|
||||
background-color: var(--background-dark);
|
||||
@ -100,7 +115,7 @@ body {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
#header .link{
|
||||
#header .link {
|
||||
-webkit-user-select: text;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
@ -120,6 +135,7 @@ body {
|
||||
font-weight: normal;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
font-size: var(--big);
|
||||
}
|
||||
|
||||
.link.first {
|
||||
@ -139,17 +155,40 @@ body {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
/************************************************/
|
||||
/* Loader */
|
||||
/************************************************/
|
||||
#loader {
|
||||
color: var(--offwhite);
|
||||
font-size: 20px;
|
||||
font-size: var(--large);
|
||||
font-weight: normal;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
.loading-bar {
|
||||
border: 1px solid var(--offwhite);
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-bar::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: var(--percent);
|
||||
background-color: var(--offwhite);
|
||||
height: 100%;
|
||||
transition: width 0.25s linear;
|
||||
}
|
||||
|
||||
/************************************************/
|
||||
/* Scrollbar */
|
||||
/************************************************/
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
@ -169,130 +208,52 @@ body {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.accent-red {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.accent-green {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--offwhite);
|
||||
border-bottom: 1px solid var(--offwhite);
|
||||
padding-bottom: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
color: var(--offwhite);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 10px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.instructions>span {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.instructions>span:first-child {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.instructions>span:not(:first-child) {
|
||||
font-size: 15px;
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.buttons-footer {
|
||||
display: flex;
|
||||
column-gap: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 10px 15px;
|
||||
border-radius: 5px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.next {
|
||||
color: var(--background);
|
||||
background-color: var(--offwhite);
|
||||
}
|
||||
|
||||
.back {
|
||||
color: var(--offwhite);
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--offwhite);
|
||||
}
|
||||
|
||||
.cancel {
|
||||
padding: 10px 5px;
|
||||
color: var(--offwhite);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.close-popup {
|
||||
color: var(--offwhite);
|
||||
background-color: var(--blue);
|
||||
}
|
||||
|
||||
.accept-popup {
|
||||
color: var(--background);
|
||||
background-color: var(--offwhite);
|
||||
}
|
||||
|
||||
input {
|
||||
outline: none;
|
||||
font-weight: 600;
|
||||
color: var(--background);
|
||||
font-size: 13px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none !important;
|
||||
/************************************************/
|
||||
/* Manager page */
|
||||
/************************************************/
|
||||
.manager-page {
|
||||
position: absolute;
|
||||
min-width: 1200px;
|
||||
overflow-y: auto;
|
||||
transition: opacity 0.25s linear;
|
||||
opacity: 0%;
|
||||
/* By default has 0% opacity to allow for fade transition */
|
||||
}
|
||||
|
||||
/************************************************/
|
||||
/* Popup */
|
||||
/************************************************/
|
||||
#grayout {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: black;
|
||||
opacity: 30%;
|
||||
background-color: rgba(0, 0, 0, 0.30);
|
||||
z-index: 999;
|
||||
transition: opacity 0.25s linear;
|
||||
opacity: 0%;
|
||||
/* By default has 0% opacity to allow for fade transition */
|
||||
}
|
||||
|
||||
#popup {
|
||||
width: 400px;
|
||||
width: 600px;
|
||||
height: fit-content;
|
||||
min-height: 200px;
|
||||
position: absolute;
|
||||
background-color: var(--background);
|
||||
border-radius: 5px;
|
||||
left: calc(50% - 200px);
|
||||
left: calc(50% - 300px);
|
||||
top: calc(50% - 100px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
padding: 20px 40px;
|
||||
align-items: start;
|
||||
z-index: 999;
|
||||
transition: opacity 0.25s linear;
|
||||
opacity: 0%;
|
||||
/* By default has 0% opacity to allow for fade transition */
|
||||
}
|
||||
|
||||
#popup img {
|
||||
@ -306,13 +267,16 @@ input {
|
||||
|
||||
#popup .content {
|
||||
color: var(--offwhite);
|
||||
font-size: 13px;
|
||||
font-size: var(--normal);
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 15px 0px !important;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
#popup .footer {
|
||||
@ -323,109 +287,58 @@ input {
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.manager-page {
|
||||
min-width: 1200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.manager-page>div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.step-summary {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 30%;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
.close-popup {
|
||||
color: var(--offwhite);
|
||||
border-left: 1px dashed var(--offwhite);
|
||||
height: 200px;
|
||||
row-gap: 100px;
|
||||
margin-left: 80px;
|
||||
}
|
||||
|
||||
.step-summary div {
|
||||
display: flex;
|
||||
width: 280px;
|
||||
height: 80px;
|
||||
align-items: center;
|
||||
column-gap: 15px;
|
||||
margin-left: -15px;
|
||||
margin-top: -40px;
|
||||
margin-bottom: -40px;
|
||||
font-size: 14px;
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.step-summary div:before {
|
||||
display: inline-block;
|
||||
content: "";
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--offwhite);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.step-summary div.white {
|
||||
color: var(--offwhite);
|
||||
}
|
||||
|
||||
.step-summary div.blue {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.step-summary div.white:before {
|
||||
.accept-popup {
|
||||
color: var(--background);
|
||||
background-color: var(--offwhite);
|
||||
}
|
||||
|
||||
.step-summary div.empty:before {
|
||||
background-color: var(--background);
|
||||
#popup .main-message {
|
||||
font-size: var(--large);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.step-summary div.blue:before {
|
||||
border: 1px solid var(--blue);
|
||||
background-color: var(--blue);
|
||||
#popup .sub-message {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 20px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
/************************************************/
|
||||
/* Inputs */
|
||||
/************************************************/
|
||||
input {
|
||||
outline: none;
|
||||
font-weight: 600;
|
||||
color: var(--background);
|
||||
font-size: var(--normal);
|
||||
padding: 3px 10px;
|
||||
border-radius: 5px;
|
||||
text-align: left;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.content>div {
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
color: var(--offwhite);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 5px;
|
||||
align-items: center;
|
||||
align-items: start;
|
||||
position: relative;
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.input-group>span:nth-child(1) {
|
||||
font-size: 14px;
|
||||
font-size: var(--normal);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.input-group>span:nth-child(2) {
|
||||
font-size: 13px;
|
||||
font-size: var(--normal);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@ -443,33 +356,45 @@ input {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
margin-bottom: 10px;
|
||||
.button {
|
||||
padding: 10px 15px;
|
||||
border-radius: 5px;
|
||||
font-size: var(--normal);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.buttons-footer {
|
||||
margin-top: 10px;
|
||||
.button.radio {
|
||||
border: 1px solid var(--offwhite);
|
||||
color: var(--offwhite);
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-top: 0px solid transparent !important;
|
||||
border-bottom: 1px solid var(--offwhite) !important;
|
||||
opacity: 80%;
|
||||
height: 0px !important;
|
||||
cursor: default;
|
||||
.button.radio.selected {
|
||||
background-color: var(--offwhite);
|
||||
color: var(--background);
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg)
|
||||
}
|
||||
.button.radio::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 1px solid var(--offwhite);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg)
|
||||
}
|
||||
.button.radio.selected::before {
|
||||
background-color: var(--offwhite);
|
||||
border: 4px solid var(--background);
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.button.collapse {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
@ -492,7 +417,8 @@ input {
|
||||
.button.collapse>div {
|
||||
display: none;
|
||||
position: absolute;
|
||||
transform: translate(-15px, calc(50% + 20px));
|
||||
transform: translate(-15px, calc(50% + 25px));
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.button.collapse.open>div {
|
||||
@ -522,3 +448,399 @@ input {
|
||||
border-top-left-radius: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
}
|
||||
|
||||
.buttons-footer {
|
||||
display: flex;
|
||||
column-gap: 10px;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
position: relative;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
border: 1px solid var(--offwhite);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.checkbox.checked::after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 3px;
|
||||
width: 8px;
|
||||
transform: translate(1px, -1px) rotate(-45deg);
|
||||
border-left: 2px solid var(--offwhite);
|
||||
border-bottom: 2px solid var(--offwhite);
|
||||
}
|
||||
|
||||
/************************************************/
|
||||
/* Port checks */
|
||||
/************************************************/
|
||||
|
||||
.port-input .success,
|
||||
.port-input .error {
|
||||
position: absolute;
|
||||
left: 320px;
|
||||
display: flex;
|
||||
width: 150px;
|
||||
column-gap: 8px;
|
||||
}
|
||||
|
||||
.port-input .success {
|
||||
content: url("../icons/check-solid-green.svg");
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.port-input .error img {
|
||||
content: url("../icons/triangle-exclamation-solid.svg");
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.port-input .error span {
|
||||
font-weight: 600;
|
||||
font-size: var(--small);
|
||||
color: var(--red);
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
/************************************************/
|
||||
/* Dashboard */
|
||||
/************************************************/
|
||||
|
||||
.dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 15px;
|
||||
height: 100%;
|
||||
padding: 40px 80px;
|
||||
}
|
||||
|
||||
.dashboard .scroll-container {
|
||||
overflow-y: auto;
|
||||
max-width: 100% !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dashboard .scrollable {
|
||||
display: flex;
|
||||
row-gap: 15px;
|
||||
column-gap: 15px;
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dashboard .instructions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
.dashboard .instructions .title {
|
||||
color: var(--offwhite);
|
||||
font-size: var(--very-large);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dashboard .instructions .subtitle {
|
||||
color: var(--lightgray);
|
||||
font-size: var(--normal);
|
||||
}
|
||||
|
||||
.dashboard .content {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 15px;
|
||||
}
|
||||
|
||||
.dashboard .option {
|
||||
background-color: var(--darkgray);
|
||||
width: 48%;
|
||||
color: white;
|
||||
display: flex;
|
||||
font-size: var(--normal);
|
||||
font-weight: 600;
|
||||
padding: 15px;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
border-left: 5px solid var(--blue);
|
||||
flex-direction: column;
|
||||
row-gap: 25px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dashboard .option:not(.installed) {
|
||||
background-color: var(--background-disabled);
|
||||
}
|
||||
|
||||
.dashboard .option:not(.installed) .info {
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
.dashboard .option:not(.installed) .server-data {
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
.dashboard .server-data {
|
||||
display: flex;
|
||||
column-gap: 15px;
|
||||
row-gap: 5px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dashboard .server-status {
|
||||
font-weight: 600;
|
||||
font-size: var(--normal);
|
||||
display: flex;
|
||||
column-gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dashboard .server-status::before {
|
||||
display: block;
|
||||
content: "";
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 999px;
|
||||
background-color: var(--gray);
|
||||
}
|
||||
|
||||
.dashboard .server-status.offline {
|
||||
color: var(--gray)
|
||||
}
|
||||
|
||||
.dashboard .server-status.offline::before {
|
||||
background-color: var(--gray);
|
||||
}
|
||||
|
||||
.dashboard .server-status.online {
|
||||
color: var(--green)
|
||||
}
|
||||
|
||||
.dashboard .server-status.online::before {
|
||||
background-color: var(--green);
|
||||
}
|
||||
|
||||
.dashboard .server-status.backend {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.dashboard .server-data-entry {
|
||||
display: flex;
|
||||
column-gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dashboard .server-data-entry span:nth-child(2) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dashboard .server-data-entry span:nth-child(3) {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.dashboard .instance-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dashboard .instance-info>.name {
|
||||
font-size: var(--large);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dashboard .instance-info>.folder {
|
||||
font-size: var(--normal);
|
||||
font-weight: normal;
|
||||
color: var(--lightgray);
|
||||
}
|
||||
|
||||
.dashboard .instance-info>.status {
|
||||
font-size: var(--normal);
|
||||
font-weight: 600;
|
||||
color: var(--lightgray);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
column-gap: 8px;
|
||||
}
|
||||
|
||||
.dashboard .instance-info>.status.installed {
|
||||
font-weight: 600;
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.dashboard .instance-info>.status.installed::before {
|
||||
content: url("../icons/check-solid-green.svg");
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.dashboard .instance-info>.status.error {
|
||||
font-weight: 600;
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.dashboard .instance-info>.status.error::before {
|
||||
content: url("../icons/triangle-exclamation-solid-orange.svg");
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.dashboard .instance-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.dashboard .instance-info .info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dashboard .instance-info .info>div:nth-child(1) {
|
||||
font-weight: 600;
|
||||
font-size: var(--normal);
|
||||
}
|
||||
|
||||
.dashboard .instance-info .info>div:nth-child(2) {
|
||||
font-weight: normal;
|
||||
font-size: var(--normal);
|
||||
}
|
||||
|
||||
.dashboard .instance-info .divider {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.dashboard .start,
|
||||
.dashboard .open-browser {
|
||||
margin-right: auto;
|
||||
color: var(--offwhite);
|
||||
background-color: var(--blue);
|
||||
}
|
||||
|
||||
.dashboard .start {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.dashboard .start>div {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.dashboard .edit,
|
||||
.dashboard .install,
|
||||
.dashboard .uninstall,
|
||||
.dashboard .stop {
|
||||
color: var(--offwhite);
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--offwhite);
|
||||
}
|
||||
|
||||
.dashboard .edit:hover,
|
||||
.dashboard .install:hover,
|
||||
.dashboard .uninstall:hover,
|
||||
.dashboard .stop:hover {
|
||||
color: var(--background);
|
||||
background-color: var(--offwhite);
|
||||
}
|
||||
|
||||
.dashboard .install {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.dashboard .summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 5px;
|
||||
}
|
||||
|
||||
.dashboard .logs-link {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dashboard .divider {
|
||||
border-top: 0px solid transparent !important;
|
||||
border-bottom: 1px solid var(--offwhite) !important;
|
||||
opacity: 80%;
|
||||
height: 0px !important;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/************************************************/
|
||||
/* Result summary */
|
||||
/************************************************/
|
||||
.result-summary {
|
||||
padding: 25px 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
.result-summary .title {
|
||||
font-weight: bold;
|
||||
font-size: var(--big);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.result-summary .title img {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.result-summary .description {
|
||||
font-size: var(--normal);
|
||||
}
|
||||
|
||||
.result-summary.success{
|
||||
color: var(--background-color);
|
||||
background-color: var(--green);
|
||||
}
|
||||
|
||||
.result-summary.error{
|
||||
color: var(--background-color);
|
||||
background-color: var(--red);
|
||||
}
|
||||
|
||||
/************************************************/
|
||||
/* Misc */
|
||||
/************************************************/
|
||||
.accent-red {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.accent-green {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/************************************************/
|
||||
/* Animations */
|
||||
/************************************************/
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg)
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg)
|
||||
}
|
||||
}
|
||||