Merge pull request #811 from Pax1601/manager-wizard

Manager wizard
This commit is contained in:
Pax1601 2024-01-26 17:34:51 +01:00 committed by GitHub
commit ec020f7b5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 2481 additions and 1688 deletions

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[];
@ -836,6 +840,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;
@ -1600,6 +1605,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;
@ -1997,6 +2003,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

@ -1,55 +1,55 @@
<style>
#manager-connections .success,
#manager-connections .error {
#connections-page .success,
#connections-page .error {
position: absolute;
left: 420px;
left: 320px;
display: flex;
width: 150px;
column-gap: 8px;
}
#manager-connections .success {
#connections-page .success {
content: url("./icons/check-solid-green.svg");
height: 20px;
width: 20px;
}
#manager-connections .error img {
#connections-page .error img {
content: url("./icons/triangle-exclamation-solid.svg");
height: 20px;
width: 20px;
}
#manager-connections .error span {
#connections-page .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 <%= singleInstance? "3": "4" %> of <%= singleInstance? "4": "5" %>
</div>
<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">
<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>
@ -58,10 +58,12 @@
</div>
<div class="input-group backend-port">
<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 +71,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 <%= singleInstance? "2": "3" %> of <%= singleInstance? "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 <%= activeInstance.connectionsType === 'auto'? 'selected': '' %>" onclick="signal('onConnectionsTypeClicked', 'auto')">
Auto apply settings
</div>
<div class="button radio manual <%= activeInstance.connectionsType !== 'auto'? 'selected': '' %>" onclick="signal('onConnectionsTypeClicked', 'manual')">
Manually set
</div>
</div>
</div>

24
manager/ejs/folder.ejs Normal file
View File

@ -0,0 +1,24 @@
<style>
</style>
<div>
<div class="instructions">
<div class="step">
Step 1 of <%= singleInstance? "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>
</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,9 +1,4 @@
<style>
#manager-instances {
padding-left: 80px;
padding-right: 80px;
}
#manager-instances .scroll-container {
height: 100%;
overflow-y: auto;
@ -18,7 +13,7 @@
height: fit-content;
width: 100%;
flex-wrap: wrap;
padding: 15px;
padding: 15px 40px;
}
#manager-instances .option {
@ -36,8 +31,16 @@
row-gap: 25px;
}
#manager-instances>.instructions {
margin-bottom: 10px;
#manager-instances .option:not(.installed) {
background-color: var(--background-disabled);
}
#manager-instances .option:not(.installed) .info {
opacity: 50%;
}
#manager-instances .option:not(.installed) .server-data {
opacity: 50%;
}
#manager-instances .button.cancel {
@ -107,7 +110,7 @@
#manager-instances .instance-info {
display: flex;
flex-direction: column;
row-gap: 5px;
row-gap: 10px;
width: 100%;
}
@ -192,6 +195,7 @@
}
#manager-instances .edit,
#manager-instances .install,
#manager-instances .uninstall,
#manager-instances .stop {
color: var(--offwhite);
@ -200,36 +204,34 @@
}
#manager-instances .edit:hover,
#manager-instances .install:hover,
#manager-instances .uninstall:hover,
#manager-instances .stop:hover {
color: var(--background);
background-color: var(--offwhite);
}
#manager-instances .install {
margin-left: auto;
}
</style>
<div id="manager-instances">
<div id="manager-instances" style="margin-bottom: 10px;">
<div class="content">
<div class="button cancel">
<img src="./icons/chevron-left-solid.svg"/> Return to menu
</div>
<div class="instructions">
<span>
<div class="instructions" style="display: flex; flex-direction: column; row-gap: 10px; align-items: center; padding: 20px;">
<span style="color: var(--offwhite); font-size: 18px; font-weight: 600;">
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 style="color: var(--offwhite); font-size: 14px; text-align: center;">
The following DCS installations have been identified. <br>You can start an Olympus server, modify settings and uninstall below.
</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="option <%= instances[i].installed? 'installed': '' %>" 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>
@ -239,33 +241,42 @@
<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>
<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>
<span><img src="./icons/folder-open-solid.svg"> <%= instances[i].folder %></span>
<div style="display: flex; flex-direction: column; row-gap: 5px;">
<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">Start server</div>
<div class="button start-client">Start client</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>
<% } %>

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-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: 14px;
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: 13px; 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 <%= installEnabled? '': 'disabled' %>" onclick="signal('onInstallMenuClicked')">
Add Olympus
<div>
Add or update Olympus to a new DCS instance
</div>
</div>
<div class="option <%= editEnabled? '': '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 <%= singleInstance? "4": "5" %> of <%= singleInstance? "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)" value="<%= activeInstance["installed"]? 'This is a long string so that users know this is not actually their password. Hi Tony!': '' %>">
</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)" value="<%= activeInstance["installed"]? 'This is a long string so that users know this is not actually their password. Hi Tony!': '' %>">
</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)" value="<%= activeInstance["installed"]? 'This is a long string so that users know this is not actually their password. Hi Tony!': '' %>">
</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: 14px; 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,156 @@
<style>
#manager-result .content {
width: 100% !important;
}
#manager-result img.success {
content: url("./icons/check-solid-green.svg");
height: 20px;
width: 20px;
#result-page {
display: flex;
flex-direction: column;
row-gap: 30px;
padding: 60px 120px;
}
#manager-result img.error {
content: url("./icons/triangle-exclamation-solid.svg");
height: 20px;
width: 20px;
#result-page .result-summary {
padding: 25px 15px;
display: flex;
flex-direction: column;
row-gap: 10px;
}
#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 .result-summary .title {
font-weight: bold;
font-size: 15px;
display: flex;
align-items: center;
font-size: 13px;
font-weight: 600;
color: var(--offwhite);
column-gap: 10px;
}
#manager-result .result {
cursor: pointer;
background-color: var(--darkgray);
border-left: 5px solid var(--blue);
width: 100%;
height: 80px;
color: white;
#result-page .result-summary .title img {
margin-right: 10px;
}
#result-page .result-summary .description {
font-size: 13px;
}
#result-page .result-summary.success{
color: var(--background-color);
background-color: var(--green);
}
#result-page .result-summary.error{
color: var(--background-color);
background-color: var(--red);
}
#result-page .instructions-group {
display: flex;
font-size: 13px;
font-weight: 600;
padding-left: 15px;
color: var(--offwhite);
flex-direction: column;
row-gap: 15px;
}
#result-page .usage-instructions {
background-color: var(--background-usage);
border-radius: 10px;
display: flex;
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;
}
#manager-result .result>div>span:nth-child(1) {
font-size: 15px;
font-weight: 600;
}
#manager-result .result>div>span:nth-child(2) {
display: flex;
column-gap: 10px;
justify-content: center;
justify-items: center;
align-items: start;
font-size: 13px;
font-weight: normal;
}
#result-page .usage-instructions>div>img {
height: 40px;
width: 40px;
}
#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: 18px; 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: 13px; 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: 13px;">
Alternatively, you can run the <b>Olympus Server</b> instead and visit <div class="link">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">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: 13px;">
<b>To access Olympus from this PC</b>, you need to visit <div class="link">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>

266
manager/ejs/settings.ejs Normal file
View File

@ -0,0 +1,266 @@
<style>
#manager-settings .scroll-container {
height: 100%;
overflow-y: auto;
max-width: 100% !important;
width: 100%;
}
#manager-settings .scrollable {
display: flex;
row-gap: 15px;
column-gap: 15px;
height: fit-content;
width: 100%;
flex-wrap: wrap;
padding: 15px 40px;
}
#manager-settings .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-settings .option:not(.installed) {
background-color: var(--background-disabled);
}
#manager-settings .option:not(.installed) .info {
opacity: 50%;
}
#manager-settings .option:not(.installed) .server-data {
opacity: 50%;
}
#manager-settings .button.cancel {
position: absolute;
left: 110px;
top: 130px;
}
#manager-settings .server-data {
display: flex;
column-gap: 15px;
row-gap: 5px;
flex-wrap: wrap;
}
#manager-settings .server-status {
font-weight: 600;
font-size: 15;
display: flex;
column-gap: 5px;
align-items: center;
}
#manager-settings .server-status::before {
display: block;
content: "";
width: 15px;
height: 15px;
border-radius: 999px;
background-color: var(--gray);
}
#manager-settings .server-status.offline {
color: var(--gray)
}
#manager-settings .server-status.offline::before {
background-color: var(--gray);
}
#manager-settings .server-status.online {
color: var(--green)
}
#manager-settings .server-status.online::before {
background-color: var(--green);
}
#manager-settings .server-status.backend {
margin-left: auto;
}
#manager-settings .server-data-entry {
display: flex;
column-gap: 5px;
align-items: center;
}
#manager-settings .server-data-entry span:nth-child(2) {
font-weight: 600;
}
#manager-settings .server-data-entry span:nth-child(3) {
font-weight: normal;
}
#manager-settings .instance-info {
display: flex;
flex-direction: column;
row-gap: 10px;
width: 100%;
}
#manager-settings .instance-info>span:nth-child(1) {
font-size: 18px;
font-weight: 600;
}
#manager-settings .instance-info>span:nth-child(2) {
font-size: 13px;
font-weight: 600;
color: var(--lightgray);
}
#manager-settings .instance-info>span:nth-child(2).installed {
font-weight: 600;
color: var(--green);
}
#manager-settings .instance-info>span:nth-child(2).error {
font-weight: 600;
color: orange;
}
#manager-settings .instance-info>span:nth-child(3) {
font-size: 13px;
font-weight: normal;
color: var(--lightgray);
display: flex;
align-items: center;
column-gap: 8px;
}
#manager-settings .instance-info>span:nth-child(4) {
display: flex;
column-gap: 10px;
font-size: 13px;
font-weight: normal;
}
#manager-settings .instance-buttons {
display: flex;
flex-direction: row;
width: 100%;
justify-content: space-between;
column-gap: 10px;
}
#manager-settings .instance-info .info {
display: flex;
flex-direction: row;
justify-content: space-between;
}
#manager-settings .instance-info .info>div:nth-child(1) {
font-weight: 600;
font-size: 14px;
}
#manager-settings .instance-info .info>div:nth-child(2) {
font-weight: normal;
font-size: 14px;
}
#manager-settings .instance-info .divider {
margin-top: 5px;
margin-bottom: 5px;
}
#manager-settings .start, #manager-settings .open-browser {
margin-right: auto;
color: var(--offwhite);
background-color: var(--blue);
}
#manager-settings .start {
width: 160px;
}
#manager-settings .start>div {
width: 160px;
}
#manager-settings .edit,
#manager-settings .install,
#manager-settings .uninstall,
#manager-settings .stop {
color: var(--offwhite);
background-color: transparent;
border: 1px solid var(--offwhite);
}
#manager-settings .edit:hover,
#manager-settings .install:hover,
#manager-settings .uninstall:hover,
#manager-settings .stop:hover {
color: var(--background);
background-color: var(--offwhite);
}
#manager-settings .install {
margin-left: auto;
}
</style>
<div id="manager-settings" style="padding: 40px;">
<div class="cancel" style="font-size: 14px; font-weight: 600; color: var(--offwhite); display: flex; align-items: center; column-gap: 10px; cursor: pointer; text-decoration: underline; " onclick="signal('onBackClicked')">
<img src="./icons/chevron-left-solid.svg" style=" height: 14px;">Back to menu
</div>
<div class="content">
<div class="instructions" style="display: flex; flex-direction: column; row-gap: 10px; align-items: center; padding: 20px;">
<span style="color: var(--offwhite); font-size: 18px; font-weight: 600;">
View and manage installs
</span>
<span style="color: var(--offwhite); font-size: 14px; text-align: center;">
The following DCS installations have been identified. <br>You can modify settings and uninstall below.
</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><%= instances[i].name %></span>
<span><img src="./icons/folder-open-solid.svg"> <%= instances[i].folder %></span>
<div class="divider"></div>
<div style="display: flex; flex-direction: column; row-gap: 5px;">
<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 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>

25
manager/ejs/type.ejs Normal file
View File

@ -0,0 +1,25 @@
<style>
</style>
<div>
<div class="instructions">
<div class="step">
Step <%= singleInstance? "1": "2" %> of <%= singleInstance? "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 <%= activeInstance.installationType === 'singleplayer'? 'selected': '' %>" onclick="signal('onInstallTypeClicked', 'singleplayer')">
Singleplayer
</div>
<div class="button radio multiplayer <%= activeInstance.installationType !== 'singleplayer'? 'selected': '' %>" 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: 18px;
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>

93
manager/ejs/wizard.ejs Normal file
View File

@ -0,0 +1,93 @@
<style>
.wizard-page {
display: flex;
flex-direction: column;
row-gap: 30px;
padding: 60px 120px;
max-height: 100%;
}
.buttons-footer {
display: flex;
column-gap: 10px;
justify-content: start;
}
.instructions {
display: flex;
flex-direction: column;
row-gap: 15px;
color: var(--offwhite);
}
.instructions .step {
font-size: 14px;
color: var(--lightgray);
}
.instructions .description {
font-size: 14px;
color: var(--lightgray);
}
.instructions .title {
font-size: 24px;
font-weight: bold;
}
.content {
overflow-x: hidden;
overflow-y: scroll;
}
.content > div {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
row-gap: 20px;
align-items: start;
justify-content: center;
}
.wizard-inputs {
display: flex;
flex-direction: column;
row-gap: 10px;
}
.wizard-page .button.radio {
width: 300px;
}
.note {
width: 100%;
background-color: var(--background-note);
color: var(--offwhite);
border-left: 5px solid var(--offwhite);
font-size: 14px;
padding: 15px;
font-weight: 600;
}
.warning {
background-color: var(--background-warning);
border-left: 5px solid var(--orange);
}
</style>
<div class="wizard-page">
<div class="cancel" style="font-size: 14px; 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;">Cancel install
</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

@ -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" 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" />
</div>
<div id="loader" class="manager-page hide">
Loading, please wait...
<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: 14px; 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,11 @@
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);
}
</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,102 @@
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() {
if (this.instances === null) {
this.instances = await this.findInstances();
}
return this.instances;
if (this.instances === null)
DCSInstance.instances = this.findInstances();
return DCSInstance.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'];
const folders = fs.readdirSync(searchpath);
var instances = [];
/* 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(searchpath, folder, "Config", "appsettings.lua")) ||fs.existsSync(path.join(searchpath, folder, "Config", "serversettings.lua"))) {
logger.log(`Found instance in ${folder}, checking for Olympus`)
var newInstance = new DCSInstance(path.join(searchpath, 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.")
throw "An error occured while trying to fetch the location of the DCS instances.";
}
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 +115,61 @@ 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() {
/* 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"];
} catch (err) {
showErrorPopup(`A critical error has occurred while reading your Olympus configuration file. Please, manually reinstall olympus in ${this.folder}.`)
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 +178,37 @@ 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}`);
}
}
/* 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,84 +216,144 @@ 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.
@ -343,7 +429,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 +456,109 @@ class DCSInstance {
})
}
/* Uninstall this instance */
uninstall() {
showConfirmPopup("Are you sure you want to completely remove this Olympus installation?", () =>
uninstallInstance(this.folder, this.name).then(
() => {
/** 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(100);
await applyConfiguration(getManager().getActiveInstance().folder, getManager().getActiveInstance());
setPopupLoadingProgress("Editing completed!", 100);
await sleep(500);
logger.log(`Editing completed successfully`);
hidePopup();
this.options.mode === "basic"? getManager().menuPage.show(): getManager().instancesPage.show();
} catch (err) {
logger.log(`An error occurred during editing: ${err}`);
hidePopup();
showErrorPopup(`A critical error occurred, check ${this.options.logLocation} for more info.`)
}
}
/** Install this instance
*
*/
async install() {
showWaitLoadingPopup(`<span>Please wait while Olympus is being installed in <i>${this.name}</i></span>`);
try {
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();
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");
} catch (err) {
logger.log(`An error occurred during installation: ${err}`);
hidePopup();
getManager().resultPage.show();
getManager().resultPage.getElement().querySelector(".result-summary.success").classList.add("hide");
getManager().resultPage.getElement().querySelector(".result-summary.error").classList.remove("hide");
}
}
/** Uninstall this instance
*
*/
async uninstall() {
showConfirmPopup("<div style='font-size: 18px; max-width: 100%; margin-bottom: 15px;'> Are you sure you want to remove Olympus? </div> If you click Accept, the Olympus mod will be removed from your DCS installation.", async () => {
try {
logger.log(`Uninstalling Olympus from ${this.folder}`)
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}`)
location.reload();
return true;
} catch (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();
},
(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();
});
}
));
});
}
});
}
}

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,262 +1,589 @@
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 = {
logLocation: path.join(__dirname, "..", "manager.log"),
configLoaded: false
};
activePage = null;
welcomePage = null;
settingsPage = null;
folderPage = null;
typePage = null;
connectionsTypePage = null;
connectionsPage = null;
passwordsPage = null;
resultPage = null;
instancesPage = 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);
}
});
}
/** 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.options.configLoaded = true;
} catch (e) {
logger.error(`An error occurred while reading the options.json file: ${e}`);
showErrorPopup(`A critical error occurred, check ${this.options.logLocation} for more info.`)
}
}
document.getElementById("loader").classList.add("hide");
if (!this.options.configLoaded) {
/* Hide the loading page */
document.getElementById("loader").classList.add("hide");
/* 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) => {
/* 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.options.mode === "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);
DCSInstance.getInstances().then(async (instances) => {
this.setLoadingProgress(`Analysis completed, starting manager...`, 100);
await sleep(100);
this.options.instances = instances;
/* Get my public IP */
this.getPublicIP().then(
(ip) => { this.options.ip = ip; },
() => { this.options.ip = undefined; }
)
/* Check if there are corrupted or outdated instances */
if (this.options.instances.some((instance) => {
return instance.installed && instance.error;
})).then(
() => { location.reload() },
(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")}`);
})) {
/* Ask the user for confirmation */
showConfirmPopup("<div style='font-size: 18px; max-width: 100%;'>One or more of your Olympus instances are not up to date! </div> If you have just updated Olympus this is normal. Press Accept and the Manager will update your instances for you. <br> Press Close to update your instances manually using the Installation Wizard", async () => {
try {
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 ${this.options.logLocation}`);
}
})
}
this.options.installEnabled = true;
this.options.editEnabled = this.options.instances.find(instance => instance.installed);
/* Hide the loading page */
document.getElementById("loader").classList.add("hide");
this.options.singleInstance = this.options.instances.length === 1;
/* 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");
/* Force the setting of the ports whenever the page is shown */
this.connectionsPage.options.onShow = () => {
if (this.options.activeInstance) {
this.setPort('client', this.options.activeInstance.clientPort);
this.setPort('backend', this.options.activeInstance.backendPort);
}
}
if (this.options.mode === "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"));
});
}
}
/** Get the currently active instance, i.e. the instance that is being edited/installed/removed
*
* @returns The active instance
*/
getActiveInstance() {
return this.options.activeInstance;
}
/** Creates the options file. This is done only the very first time you start Olympus.
*
* @param {String} mode The mode, either Basic or Expert
*/
createOptionsFile(mode) {
try {
fs.writeFileSync("options.json", JSON.stringify({ mode: mode }));
location.reload();
} catch (e) {
showErrorPopup(`A critical error occurred, check ${this.options.logLocation} for more info.`)
}
}
/** Switch to a different mode of operation
*
* @param {String} newMode The mode to switch to
*/
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));
location.reload();
}
/************************************************/
/* CALLBACKS */
/************************************************/
/** Switch to basic mode
*
*/
onBasicClicked() {
this.createOptionsFile("basic");
}
/** Switch to expert mode
*
*/
onExpertClicked() {
this.createOptionsFile("expert");
}
/** When the install button is clicked go the installation page
*
*/
onInstallMenuClicked() {
this.options.install = true;
if (this.options.instances.length == 0) {
// TODO: show error
}
this.options.activeInstance = this.options.instances[0];
if (this.options.singleInstance) {
/* Show the type selection page */
if (!this.options.activeInstance.installed) {
this.activePage.hide()
this.typePage.show();
} else {
showConfirmPopup("<div style='font-size: 18px; max-width: 100%; margin-bottom: 8px;'> Olympus is already installed in this instance! </div> 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?",
() => {
this.activePage.hide()
this.typePage.show();
}
)
})
}
/* 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();
connectionsPage.onBackClicked = (e) => {
/* Show the installation page */
connectionsPage.hide();
installationsPage.show();
}
}
installationsPage.onCancelClicked = (e) => {
/* Go back to the main menu */
installationsPage.hide();
menuPage.show();
}
/* 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.")
}
} 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.")
}
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")}`)
}
}
passwordsPage.onCancelClicked = (e) => {
/* Go back to the main menu */
passwordsPage.hide();
menuPage.show();
}
/* Result */
var resultPage = new ResultPage({logLocation: path.join(__dirname, "..", "manager.log")});
resultPage.onBackClicked = (e) => {
/* Reload the page to apply changes */
resultPage.hide();
location.reload();
}
resultPage.onCancelClicked = (e) => {
/* Reload the page to apply changes */
resultPage.hide();
location.reload();
}
/* 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());
/* In simplified mode we directly show the connections page */
if (this.simplified) {
const options = {
instance: instances[0],
simplified: this.simplified,
install: true
}
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
*
*/
onEditMenuClicked() {
this.activePage.hide()
this.options.install = false;
if (this.options.singleInstance) {
this.options.activeInstance = this.options.instances[0];
this.connectionsTypePage.show();
} else {
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.options.activeInstance = instance;
}
}
/* When the installation type is selected */
onInstallTypeClicked(type) {
this.typePage.getElement().querySelector(`.singleplayer`).classList.toggle("selected", type === 'singleplayer');
this.typePage.getElement().querySelector(`.multiplayer`).classList.toggle("selected", type === 'multiplayer');
if (this.options.activeInstance)
this.options.activeInstance.installationType = type;
else
showErrorPopup(`A critical error occurred, check ${this.options.logLocation} for more info.`)
}
/* When the connections type is selected */
onConnectionsTypeClicked(type) {
this.connectionsTypePage.getElement().querySelector(`.auto`).classList.toggle("selected", type === 'auto');
this.connectionsTypePage.getElement().querySelector(`.manual`).classList.toggle("selected", type === 'manual');
if (this.options.activeInstance)
this.options.activeInstance.connectionsType = type;
else
showErrorPopup(`A critical error occurred, check ${this.options.logLocation} for more info.`)
}
/* When the next button of a wizard page is clicked */
onNextClicked() {
/* Choose which page to show depending on the active page */
if (this.activePage == this.folderPage) {
if (this.options.activeInstance.installed) {
showConfirmPopup("<div style='font-size: 18px; max-width: 100%; margin-bottom: 8px;'> Olympus is already installed in this instance! </div> 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?",
() => {
this.activePage.hide()
this.typePage.show();
}
)
} else {
this.activePage.hide();
this.typePage.show();
}
} else if (this.activePage == this.typePage) {
this.activePage.hide();
this.connectionsTypePage.show();
} else if (this.activePage == this.connectionsTypePage) {
if (this.options.activeInstance) {
if (this.options.activeInstance.connectionsType === 'auto') {
this.activePage.hide();
this.passwordsPage.show();
}
else {
this.activePage.hide();
this.connectionsPage.show();
this.connectionsPage.getElement().querySelector(".backend-address .checkbox").classList.toggle("checked", this.options.activeInstance.backendAddress === '*')
}
} else {
showErrorPopup(`A critical error occurred, check ${this.options.logLocation} for more info.`)
}
} else if (this.activePage == this.connectionsPage) {
this.options.activeInstance.checkClientPort().then(
(portFree) => {
if (portFree) {
return this.options.activeInstance.checkBackendPort();
} else {
return Promise.reject('Port not free');
}
}).then((portFree) => {
if (portFree) {
this.activePage.hide();
this.passwordsPage.show();
} else {
return Promise.reject('Port not free');
}
}).catch(() => {
showErrorPopup('Please, make sure both the client and backend ports are free!');
}
);
} else if (this.activePage == this.passwordsPage) {
if (this.options.activeInstance) {
if (this.options.activeInstance.installed && !this.options.activeInstance.arePasswordsEdited()) {
this.activePage.hide();
this.options.install? this.options.activeInstance.install(): this.options.activeInstance.edit();
}
else {
if (!this.options.activeInstance.arePasswordsSet()) {
showErrorPopup('Please, make sure all passwords are set!');
} else if (!this.options.activeInstance.arePasswordsDifferent()) {
showErrorPopup('Please, set different passwords for the Game Master, Blue Commander, and Red Commander roles!');
} else {
this.activePage.hide();
this.options.install? this.options.activeInstance.install(): this.options.activeInstance.edit();
}
}
} else {
showErrorPopup(`A critical error occurred, check ${this.options.logLocation} for more info.`)
}
}
}
/* When the back button of a wizard page is clicked */
onBackClicked() {
this.activePage.hide();
this.activePage.previousPage.show(true); // Don't change the previous page
this.updateInstances();
}
onCancelClicked() {
location.reload();
}
onGameMasterPasswordChanged(value) {
if (this.options.activeInstance) {
this.options.activeInstance.setGameMasterPassword(value);
if (!this.options.activeInstance.blueCommanderPasswordEdited)
this.passwordsPage.getElement().querySelector(".blue-commander input").value = "";
if (!this.options.activeInstance.redCommanderPasswordEdited)
this.passwordsPage.getElement().querySelector(".red-commander input").value = "";
}
else
showErrorPopup(`A critical error occurred, check ${this.options.logLocation} for more info.`)
}
onBlueCommanderPasswordChanged(value) {
if (this.options.activeInstance) {
this.options.activeInstance.setBlueCommanderPassword(value);
if (!this.options.activeInstance.gameMasterPasswordEdited)
this.passwordsPage.getElement().querySelector(".game-master input").value = "";
if (!this.options.activeInstance.redCommanderPasswordEdited)
this.passwordsPage.getElement().querySelector(".red-commander input").value = "";
}
else
showErrorPopup(`A critical error occurred, check ${this.options.logLocation} for more info.`)
}
onRedCommanderPasswordChanged(value) {
if (this.options.activeInstance) {
this.options.activeInstance.setRedCommanderPassword(value);
if (!this.options.activeInstance.gameMasterPasswordEdited)
this.passwordsPage.getElement().querySelector(".game-master input").value = "";
if (!this.options.activeInstance.blueCommanderPasswordEdited)
this.passwordsPage.getElement().querySelector(".blue-commander input").value = "";
}
else
showErrorPopup(`A critical error occurred, check ${this.options.logLocation} for more info.`)
}
/* When the client port input value is changed */
onClientPortChanged(value) {
this.setPort('client', Number(value));
}
/* When the backend port input value is changed */
onBackendPortChanged(value) {
this.setPort('backend', Number(value));
}
/* When the "Enable API connection" checkbox is clicked */
onEnableAPIClicked() {
if (this.options.activeInstance) {
if (this.options.activeInstance.backendAddress === 'localhost') {
this.options.activeInstance.backendAddress = '*';
} else {
this.options.activeInstance.backendAddress = 'localhost';
}
this.connectionsPage.getElement().querySelector(".note.warning").classList.toggle("hide", this.options.activeInstance.backendAddress !== '*')
this.connectionsPage.getElement().querySelector(".backend-address .checkbox").classList.toggle("checked", this.options.activeInstance.backendAddress === '*')
} else {
showErrorPopup(`A critical error occurred, check ${this.options.logLocation} for more info.`)
}
}
/* When the "Return to manager" button is pressed */
onReturnClicked() {
location.reload();
}
/* When the "Close manager" button is pressed */
onCloseManagerClicked() {
document.querySelector('.close').click();
}
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("Error, the selected Olympus instance is currently active, please stop Olympus before editing it!")
} else {
this.options.activeInstance = instance;
this.options.install = false;
this.activePage.hide();
this.connectionsTypePage.show();
}
}
async onInstallClicked(name) {
var instance = await this.getClickedInstance(name);
this.options.activeInstance = instance;
this.options.install = true;
this.options.singleInstance = false;
this.activePage.hide();
this.typePage.show();
}
async onUninstallClicked(name) {
var instance = await this.getClickedInstance(name);
if (instance.webserverOnline || instance.backendOnline)
showErrorPopup("Error, the selected Olympus instance is currently active, please stop Olympus before uninstalling it!")
else
await instance.uninstall();
}
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.options.activeInstance.checkClientPort(value);
this.options.activeInstance.setClientPort(value);
}
else {
success = await this.options.activeInstance.checkBackendPort(value);
this.options.activeInstance.setBackendPort(value);
}
var successEls = this.connectionsPage.getElement().querySelector(`.${port}-port`).querySelectorAll(".success");
for (let i = 0; i < successEls.length; i++) {
successEls[i].classList.toggle("hide", !success);
}
var errorEls = this.connectionsPage.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;
}
updateInstances() {
var instanceDivs = this.instancesPage.getElement().querySelectorAll(`.option`);
for (let i = 0; i < instanceDivs.length; i++) {
var instanceDiv = instanceDivs[i];
var instance = this.options.instances.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 (this.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 (this.webserverOnline)
instanceDiv.querySelector(".button.start").classList.remove("loading");
}
}
}
}
}
reload() {
this.activePage.show();
}
setLoadingProgress(message, percent) {
document.querySelector("#loader .loading-message").innerHTML = message;
if (percent) {
var style = document.querySelector('#loader .loading-bar').style;
style.setProperty('--percent', `${percent}%`);
}
}
}

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,57 @@
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() {
show(ignorePreviousPage) {
ejs.renderFile(this.ejsFile, {...this.options, ...this.manager.options}, {}, (err, str) => {
if (!err) {
this.render(str);
} else {
logger.error(err);
}
});
this.element.classList.remove("hide");
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");
}
render() {
render(str) {
this.element.innerHTML = str;
/* 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

@ -4,7 +4,7 @@ const { logger } = require("./filesystem")
/** Checks if a port is already in use
*
*/
function checkPort(port, callback) {
function checkPortSync(port, callback) {
portfinder.getPort({ port: port, stopPort: port }, (err, res) => {
if (err !== null) {
logger.error(`Port ${port} already in use`);
@ -15,6 +15,14 @@ function checkPort(port, callback) {
});
}
function checkPort(port) {
return portfinder.getPortPromise({ port: port, stopPort: port });
}
function getFreePort(startPort) {
return portfinder.getPortPromise({ port: startPort });
}
/** Performs a fetch request, with a configurable timeout
*
*/
@ -34,6 +42,8 @@ async function fetchWithTimeout(resource, options = {}) {
}
module.exports = {
getFreePort: getFreePort,
checkPort: checkPort,
checkPortSync: checkPortSync,
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,5 +1,24 @@
// TODO: we can probably refactor this to be a bit cleaner
function showInfoPopup(message, onCloseCallback) {
document.getElementById("grayout").classList.remove("hide");
document.getElementById("popup").classList.remove("hide");
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");
@ -29,6 +48,17 @@ function showWaitPopup(message) {
document.getElementById("popup").querySelector(".content").innerHTML = message;
}
function showWaitLoadingPopup(message) {
document.getElementById("grayout").classList.remove("hide");
document.getElementById("popup").classList.remove("hide");
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");
@ -60,9 +90,20 @@ function hidePopup() {
document.getElementById("popup").classList.add("hide");
}
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,8 @@ 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 VERSION = "{{OLYMPUS_VERSION_NUMBER}}";
logger.log(`Running in ${__dirname}`);
@ -45,12 +44,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 +68,34 @@ 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(path.join("package.json")).birthtime;
if (date1 > date2) {
showConfirmPopup(`<span>Latest beta artifact has a timestamp of <i style="color: orange">${artifact.updated_at}</i>, while your installation was created on <i style="color: orange">${date2.toISOString()}</i>. Do you want to update to the newest beta version?</span>`, () => {
/* 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])
}
},
() => {
logger.log("Update canceled");
});
},
() => {
logger.log("Update canceled");
});
},
() => {
logger.log("Update canceled");
})
}
)
} else {
logger.log("Build is latest")
}
}
/** Update Olympus to the lastest release
@ -257,28 +258,35 @@ 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();
await getManager().start();
/* 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);
})
if (links[i].dataset.link) {
links[i].addEventListener("click", (e) => {
if (e.target.dataset.link)
exec("start " + e.target.dataset.link);
})
}
}
})
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();
})
@ -298,4 +306,4 @@ function computePagesHeight() {
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,

317
manager/manager.log Normal file
View File

@ -0,0 +1,317 @@
======================= New log starting at Thu Jan 11 2024 16:07:31 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
Development build detected, skipping version checks...
======================= New log starting at Thu Jan 11 2024 16:11:52 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
======================= New log starting at Thu Jan 11 2024 16:11:54 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
======================= New log starting at Thu Jan 11 2024 16:11:56 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
======================= New log starting at Thu Jan 11 2024 16:13:03 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
======================= New log starting at Thu Jan 11 2024 16:13:31 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
======================= New log starting at Thu Jan 11 2024 16:13:41 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
======================= New log starting at Thu Jan 11 2024 16:13:48 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
======================= New log starting at Thu Jan 11 2024 16:23:11 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
ReferenceError: ./ejs/connections.ejs:32
30| <div id="manager-connections">
31| <div class="step-summary">
>> 32| <div class="blue <%= !install || basic? 'hide': '' %>">User path</div>
33| <div class="white">Ports and address</div>
34| <div class="empty">Passwords</div>
35| <div class="empty"> <%= install? 'Install': 'Update' %></div>
basic is not defined
at eval ("./ejs/connections.ejs":12:38)
at connections (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:703:17)
at tryHandleCache (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:274:36)
at exports.renderFile (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:491:10)
at ConnectionsPage.show (D:\Documents\DCSOlympus\manager\javascripts\connections.js:41:13)
at Manager.start (D:\Documents\DCSOlympus\manager\javascripts\manager.js:70:42)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async D:\Documents\DCSOlympus\manager\javascripts\preload.js:268:5 {
path: './ejs/connections.ejs'
}
======================= New log starting at Thu Jan 11 2024 16:24:03 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
ReferenceError: ./ejs/connections.ejs:32
30| <div id="manager-connections">
31| <div class="step-summary">
>> 32| <div class="blue <%= !install || basic? 'hide': '' %>">User path</div>
33| <div class="white">Ports and address</div>
34| <div class="empty">Passwords</div>
35| <div class="empty"> <%= install? 'Install': 'Update' %></div>
basic is not defined
at eval ("./ejs/connections.ejs":12:38)
at connections (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:703:17)
at tryHandleCache (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:274:36)
at exports.renderFile (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:491:10)
at ConnectionsPage.show (D:\Documents\DCSOlympus\manager\javascripts\connections.js:41:13)
at Manager.start (D:\Documents\DCSOlympus\manager\javascripts\manager.js:70:42)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async D:\Documents\DCSOlympus\manager\javascripts\preload.js:268:5 {
path: './ejs/connections.ejs'
}
======================= New log starting at Thu Jan 11 2024 16:24:25 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
ReferenceError: ./ejs/connections.ejs:32
30| <div id="manager-connections">
31| <div class="step-summary">
>> 32| <div class="blue <%= !install || basic? 'hide': '' %>">User path</div>
33| <div class="white">Ports and address</div>
34| <div class="empty">Passwords</div>
35| <div class="empty"> <%= install? 'Install': 'Update' %></div>
basic is not defined
at eval ("./ejs/connections.ejs":12:38)
at connections (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:703:17)
at tryHandleCache (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:274:36)
at exports.renderFile (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:491:10)
at ConnectionsPage.show (D:\Documents\DCSOlympus\manager\javascripts\connections.js:41:13)
at Manager.start (D:\Documents\DCSOlympus\manager\javascripts\manager.js:71:42)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async D:\Documents\DCSOlympus\manager\javascripts\preload.js:268:5 {
path: './ejs/connections.ejs'
}
======================= New log starting at Thu Jan 11 2024 16:24:45 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
ReferenceError: ./ejs/connections.ejs:32
30| <div id="manager-connections">
31| <div class="step-summary">
>> 32| <div class="blue <%= !install || basic? 'hide': '' %>">User path</div>
33| <div class="white">Ports and address</div>
34| <div class="empty">Passwords</div>
35| <div class="empty"> <%= install? 'Install': 'Update' %></div>
basic is not defined
at eval ("./ejs/connections.ejs":12:38)
at connections (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:703:17)
at tryHandleCache (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:274:36)
at exports.renderFile (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:491:10)
at ConnectionsPage.show (D:\Documents\DCSOlympus\manager\javascripts\connections.js:41:13)
at Manager.start (D:\Documents\DCSOlympus\manager\javascripts\manager.js:71:42)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async D:\Documents\DCSOlympus\manager\javascripts\preload.js:268:5 {
path: './ejs/connections.ejs'
}
======================= New log starting at Thu Jan 11 2024 16:25:20 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
ReferenceError: ./ejs/connections.ejs:32
30| <div id="manager-connections">
31| <div class="step-summary">
>> 32| <div class="blue <%= !install || basic? 'hide': '' %>">User path</div>
33| <div class="white">Ports and address</div>
34| <div class="empty">Passwords</div>
35| <div class="empty"> <%= install? 'Install': 'Update' %></div>
basic is not defined
at eval ("./ejs/connections.ejs":12:38)
at connections (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:703:17)
at tryHandleCache (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:274:36)
at exports.renderFile (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:491:10)
at ConnectionsPage.show (D:\Documents\DCSOlympus\manager\javascripts\connections.js:41:13)
at Manager.start (D:\Documents\DCSOlympus\manager\javascripts\manager.js:78:42)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async D:\Documents\DCSOlympus\manager\javascripts\preload.js:268:5 {
path: './ejs/connections.ejs'
}
======================= New log starting at Thu Jan 11 2024 16:28:55 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
ReferenceError: ./ejs/connections.ejs:52
50| </span>
51| <div>
>> 52| <input type="number" min="1024" max="65535" value="<%= instance["clientPort"] %>">
53| <img class="success hide">
54| <div class="error hide">
55| <img> <span>Port already in use</span>
instance is not defined
at eval ("./ejs/connections.ejs":18:26)
at connections (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:703:17)
at tryHandleCache (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:274:36)
at exports.renderFile (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:491:10)
at ConnectionsPage.show (D:\Documents\DCSOlympus\manager\javascripts\connections.js:41:13)
at Manager.start (D:\Documents\DCSOlympus\manager\javascripts\manager.js:78:42)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async D:\Documents\DCSOlympus\manager\javascripts\preload.js:268:5 {
path: './ejs/connections.ejs'
}
======================= New log starting at Thu Jan 11 2024 16:29:02 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
ReferenceError: ./ejs/connections.ejs:52
50| </span>
51| <div>
>> 52| <input type="number" min="1024" max="65535" value="<%= instance["clientPort"] %>">
53| <img class="success hide">
54| <div class="error hide">
55| <img> <span>Port already in use</span>
instance is not defined
at eval ("./ejs/connections.ejs":18:26)
at connections (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:703:17)
at tryHandleCache (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:274:36)
at exports.renderFile (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:491:10)
at ConnectionsPage.show (D:\Documents\DCSOlympus\manager\javascripts\connections.js:41:13)
at Manager.start (D:\Documents\DCSOlympus\manager\javascripts\manager.js:78:42)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async D:\Documents\DCSOlympus\manager\javascripts\preload.js:268:5 {
path: './ejs/connections.ejs'
}
======================= New log starting at Thu Jan 11 2024 16:31:06 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
ReferenceError: ./ejs/connections.ejs:52
50| </span>
51| <div>
>> 52| <input type="number" min="1024" max="65535" value="<%= instance["clientPort"] %>">
53| <img class="success hide">
54| <div class="error hide">
55| <img> <span>Port already in use</span>
instance is not defined
at eval ("./ejs/connections.ejs":18:26)
at connections (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:703:17)
at tryHandleCache (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:274:36)
at exports.renderFile (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:491:10)
at ConnectionsPage.show (D:\Documents\DCSOlympus\manager\javascripts\connections.js:41:13)
at Manager.start (D:\Documents\DCSOlympus\manager\javascripts\manager.js:77:42)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async D:\Documents\DCSOlympus\manager\javascripts\preload.js:268:5 {
path: './ejs/connections.ejs'
}
======================= New log starting at Thu Jan 11 2024 16:37:31 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
ReferenceError: ./ejs/connections.ejs:52
50| </span>
51| <div>
>> 52| <input type="number" min="1024" max="65535" value="<%= instance["clientPort"] %>">
53| <img class="success hide">
54| <div class="error hide">
55| <img> <span>Port already in use</span>
instance is not defined
at eval ("./ejs/connections.ejs":18:26)
at connections (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:703:17)
at tryHandleCache (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:274:36)
at exports.renderFile (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:491:10)
at ConnectionsPage.show (D:\Documents\DCSOlympus\manager\javascripts\connections.js:38:13)
at Manager.start (D:\Documents\DCSOlympus\manager\javascripts\manager.js:77:42)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async D:\Documents\DCSOlympus\manager\javascripts\preload.js:268:5 {
path: './ejs/connections.ejs'
}
======================= New log starting at Thu Jan 11 2024 16:38:57 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
======================= New log starting at Thu Jan 11 2024 16:39:52 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
======================= New log starting at Thu Jan 11 2024 16:41:14 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
======================= New log starting at Thu Jan 11 2024 16:41:25 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
======================= New log starting at Thu Jan 11 2024 16:41:48 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
======================= New log starting at Thu Jan 11 2024 16:42:27 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
======================= New log starting at Thu Jan 11 2024 16:42:52 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
======================= New log starting at Thu Jan 11 2024 16:43:06 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
======================= New log starting at Thu Jan 11 2024 16:43:24 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
Instance C:\Users\dpassoni\Saved Games\dcs.openbeta client port set to 3000
Instance C:\Users\dpassoni\Saved Games\dcs.openbeta client port set to 3001
======================= New log starting at Thu Jan 11 2024 16:54:16 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
======================= New log starting at Thu Jan 11 2024 16:54:21 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
Instance C:\Users\dpassoni\Saved Games\dcs.openbeta client port set to 3000
Instance C:\Users\dpassoni\Saved Games\dcs.openbeta client port set to 3001
ReferenceError: ./ejs/passwords.ejs:6
4| <div id="manager-passwords">
5| <div class="step-summary">
>> 6| <div class="blue <%= !install || singleInstance? 'hide': '' %>">User path</div>
7| <div class="blue">Ports and address</div>
8| <div class="white">Passwords</div>
9| <div class="empty"> <%= install? 'Install': 'Update' %></div>
install is not defined
at eval ("./ejs/passwords.ejs":12:27)
at passwords (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:703:17)
at tryHandleCache (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:274:36)
at exports.renderFile (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:491:10)
at PasswordsPage.show (D:\Documents\DCSOlympus\manager\javascripts\passwords.js:35:13)
at ConnectionsPage.onNextClicked (D:\Documents\DCSOlympus\manager\javascripts\connections.js:58:36)
at HTMLDivElement.<anonymous> (D:\Documents\DCSOlympus\manager\javascripts\connections.js:21:87) {
path: './ejs/passwords.ejs'
}
======================= New log starting at Thu Jan 11 2024 16:54:49 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
Instance C:\Users\dpassoni\Saved Games\dcs.openbeta client port set to 3000
Instance C:\Users\dpassoni\Saved Games\dcs.openbeta client port set to 3001
ReferenceError: ./ejs/passwords.ejs:44
42| </div>
43| </div>
>> 44| <% if (!basic) { %>
45| <div class="button cancel">
46| <%= install? "Cancel installation": "Cancel editing" %>
47| </div>
basic is not defined
at eval ("./ejs/passwords.ejs":18:8)
at passwords (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:703:17)
at tryHandleCache (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:274:36)
at exports.renderFile (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:491:10)
at PasswordsPage.show (D:\Documents\DCSOlympus\manager\javascripts\passwords.js:35:13)
at ConnectionsPage.onNextClicked (D:\Documents\DCSOlympus\manager\javascripts\connections.js:58:36)
at HTMLDivElement.<anonymous> (D:\Documents\DCSOlympus\manager\javascripts\connections.js:21:87) {
path: './ejs/passwords.ejs'
}
======================= New log starting at Thu Jan 11 2024 16:55:57 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
Instance C:\Users\dpassoni\Saved Games\dcs.openbeta client port set to 3000
Instance C:\Users\dpassoni\Saved Games\dcs.openbeta client port set to 3001
======================= New log starting at Fri Jan 12 2024 08:51:22 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
Development build detected, skipping version checks...
Instance C:\Users\dpassoni\Saved Games\dcs.openbeta client port set to 3000
Instance C:\Users\dpassoni\Saved Games\dcs.openbeta client port set to 3001
======================= New log starting at Fri Jan 12 2024 08:53:17 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
Instance C:\Users\dpassoni\Saved Games\dcs.openbeta client port set to 3000
Instance C:\Users\dpassoni\Saved Games\dcs.openbeta client port set to 3001
======================= New log starting at Fri Jan 12 2024 08:53:42 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
Instance C:\Users\dpassoni\Saved Games\dcs.openbeta client port set to 3000
Instance C:\Users\dpassoni\Saved Games\dcs.openbeta client port set to 3001
ReferenceError: ./ejs/result.ejs:132
130| <div>
131| <span>
>> 132| <%= instance.name %>
133| </span>
134| <span><img src="./icons/folder-open-solid.svg">
135| <%= instance.folder %>
instance is not defined
at eval ("./ejs/result.ejs":27:26)
at result (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:703:17)
at tryHandleCache (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:274:36)
at exports.renderFile (D:\Documents\DCSOlympus\manager\node_modules\ejs\lib\ejs.js:491:10)
at ResultPage.show (D:\Documents\DCSOlympus\manager\javascripts\result.js:25:13)
at PasswordsPage.onNextClicked (D:\Documents\DCSOlympus\manager\javascripts\passwords.js:40:33)
at HTMLDivElement.<anonymous> (D:\Documents\DCSOlympus\manager\javascripts\managerpage.js:39:87) {
path: './ejs/result.ejs'
}
======================= New log starting at Fri Jan 12 2024 08:54:41 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
Instance C:\Users\dpassoni\Saved Games\dcs.openbeta client port set to 3000
Instance C:\Users\dpassoni\Saved Games\dcs.openbeta client port set to 3001
======================= New log starting at Fri Jan 12 2024 08:56:11 GMT+0100 (Central European Standard Time) =======================
Running in D:\Documents\DCSOlympus\manager\javascripts
Instance C:\Users\dpassoni\Saved Games\dcs.openbeta client port set to 3000
Instance C:\Users\dpassoni\Saved Games\dcs.openbeta client port set to 3001
Installing hooks in C:\Users\dpassoni\Saved Games\dcs.openbeta
Error installing hooks in C:\Users\dpassoni\Saved Games\dcs.openbeta: Error: ENOENT: no such file or directory, lstat 'D:\Documents\DCSOlympus\scripts\OlympusHook.lua'

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,14 +3,18 @@
--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;
}
* {
@ -36,7 +40,7 @@ body {
display: block;
-webkit-user-select: none;
-webkit-app-region: drag;
height: 20px;
height: 30px;
width: 100%;
display: flex;
justify-content: end;
@ -141,13 +145,30 @@ body {
#loader {
color: var(--offwhite);
font-size: 20px;
font-size: 16px;
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.1s linear;
}
::-webkit-scrollbar {
@ -186,34 +207,6 @@ body {
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;
@ -225,21 +218,30 @@ body {
column-gap: 10px;
}
.next {
color: var(--background);
background-color: var(--offwhite);
}
.back {
color: var(--offwhite);
background-color: var(--background);
.button.radio {
border: 1px solid var(--offwhite);
color: var(--offwhite);
}
.cancel {
padding: 10px 5px;
color: var(--offwhite);
text-decoration: underline;
.button.radio.selected {
background-color: var(--offwhite);
color: var(--background);
}
.button.radio::before {
content: "";
display: block;
width: 10px;
height: 10px;
border: 1px solid var(--offwhite);
border-radius: 999px;
}
.button.radio.selected::before {
background-color: var(--offwhite);
border: 4px solid var(--background);
width: 4px;
height: 4px;
}
.close-popup {
@ -259,7 +261,7 @@ input {
font-size: 13px;
padding: 3px 10px;
border-radius: 5px;
text-align: center;
text-align: left;
width: 300px;
}
@ -273,13 +275,13 @@ input {
left: 0px;
width: 100%;
height: 100%;
background-color: black;
opacity: 30%;
background-color: white;
opacity: 15%;
z-index: 999;
}
#popup {
width: 400px;
width: 450px;
height: fit-content;
min-height: 200px;
position: absolute;
@ -290,7 +292,7 @@ input {
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 20px;
padding: 20px 40px;
align-items: start;
z-index: 999;
}
@ -313,6 +315,9 @@ input {
padding: 15px 0px !important;
word-wrap: break-word;
overflow-wrap: anywhere;
display: flex;
flex-direction: column;
row-gap: 10px;
}
#popup .footer {
@ -328,93 +333,12 @@ input {
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;
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;
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 {
background-color: var(--offwhite);
}
.step-summary div.empty:before {
background-color: var(--background);
}
.step-summary div.blue:before {
border: 1px solid var(--blue);
background-color: var(--blue);
}
.content {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
row-gap: 20px;
align-items: center;
justify-content: center;
padding: 20px;
}
.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;
}
@ -443,14 +367,6 @@ input {
flex-wrap: wrap;
}
.instructions {
margin-bottom: 10px;
}
.buttons-footer {
margin-top: 10px;
}
.divider {
border-top: 0px solid transparent !important;
border-bottom: 1px solid var(--offwhite) !important;
@ -470,6 +386,7 @@ input {
}
.button.collapse {
position: relative;
display: flex;
justify-content: space-between;
}
@ -492,7 +409,7 @@ input {
.button.collapse>div {
display: none;
position: absolute;
transform: translate(-15px, calc(50% + 20px));
transform: translate(-15px, calc(50% + 25px));
}
.button.collapse.open>div {
@ -522,3 +439,22 @@ input {
border-top-left-radius: 0px;
border-top-right-radius: 0px;
}
.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);
}