Merge branch 'Pax1601:release-candidate' into 681_newest_changes_skill

This commit is contained in:
Stefan Arsic 2024-02-03 22:42:28 +01:00 committed by GitHub
commit 0ef3abbffa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 2812 additions and 2020 deletions

3
.gitignore vendored
View File

@ -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

View File

@ -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";

View File

@ -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 {

View File

@ -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>

View File

@ -6,7 +6,7 @@
{
"label": "mirror-package",
"type": "shell",
"command": "call ./scripts/mirror-package.bat",
"command": "./scripts/mirror-package.bat",
"isBackground": true
}
]

View File

@ -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>

View 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>

View 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
View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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
View 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
View 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
View 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
View 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>

View 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

View 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
View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -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

View 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

View 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

View File

@ -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>

View File

@ -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;

View File

@ -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');
});
}
}

View File

@ -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
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View 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
};

View File

@ -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");
})
}
}
}
}

View File

@ -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;

View File

@ -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
}

View File

@ -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;

View File

@ -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
}

View File

@ -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();
})

View File

@ -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;

View File

@ -0,0 +1,10 @@
async function sleep(ms) {
await new Promise(r => setTimeout(r, ms));
return true;
}
module.exports = {
sleep: sleep
}

View 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;

View File

@ -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', () => {

View File

@ -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"
}
}
}

View File

@ -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

View File

@ -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)
}
}