Modified manager to new design

This commit is contained in:
Pax1601 2023-12-29 11:28:28 +01:00
parent 7450c9e506
commit 7391006a2f
38 changed files with 1779 additions and 1395 deletions

View File

@ -0,0 +1,89 @@
<style>
#manager-connections .success,
#manager-connections .error {
position: absolute;
left: 420px;
display: flex;
width: 150px;
column-gap: 8px;
}
#manager-connections .success {
content: url("./icons/check-solid-green.svg");
height: 20px;
width: 20px;
}
#manager-connections .error img {
content: url("./icons/triangle-exclamation-solid.svg");
height: 20px;
width: 20px;
}
#manager-connections .error span {
font-weight: 600;
font-size: 12px;
color: var(--red);
height: fit-content;
}
</style>
<div id="manager-connections">
<div class="step-summary">
<div class="blue <%= !install? '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>
<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">
</span>
<div>
<input type="number" min="1024" max="65535" value="<%= instance["clientPort"] %>">
<img class="success hide">
<div class="error hide">
<img> <span>Port already in use</span>
</div>
</div>
</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.">
</span>
<div>
<input type="number" min="1024" max="65535" value="<%= instance["backendPort"] %>">
<img class="success hide">
<div class="error hide">
<img> <span>Port already in use</span>
</div>
</div>
</div>
<div class="input-group backend-address">
<span>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>
<input type="text" value="<%= instance["backendAddress"] %>">
</div>
<div class="buttons-footer">
<div class="button back">
Back
</div>
<div class="button next">
Next
</div>
</div>
<div class="button cancel">
Cancel installation
</div>
</div>
</div>

View File

@ -0,0 +1,112 @@
<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 {
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;
}
</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 already installed'): 'Olympus not installed yet' %>
</span>
</div>
<% } %>
</div>
</div>
<div class="button cancel">
Cancel installation
</div>
</div>
</div>

View File

@ -1,68 +0,0 @@
<div class="instance-div">
<div class="folder-name">
<div class="icon folder"></div>
<span><%= folder %></span>
</div>
<div class="version">
Detected .dll version: <span class="<%= version === "n/a"? '' : (version === newVersion? 'accent-green': 'accent-red blink')%>"><%= version %></span> <br>
Available .dll version: <span class="accent-green"><%= newVersion %></span>
</div>
<div class="instance-content">
<table class="input-table">
<tr>
<td>
<div>
<div class="label">Client port <div class="icon info" title="This is the port that will allow users to connect to Olympus and must be forwarded in your firewall."></div></div>
<input id="client-port" type='number' value="<%= typeof client !== 'undefined' ? client["port"]: "" %>" min="1023" max="65535" <%= !installed? "disabled": "" %> tabindex="<%= index %>">
<span id="client-port-error" class="error"></span>
</div>
</td>
<td>
<div>
<div class="label">Game Master password <div class="icon info" title="This is the password you need to input to connect with the Game Master role."></div></div>
<input id="game-master-password" type="password" <%= !installed? "disabled": "" %> tabindex="<%= index + 3 %>">
<span id="game-master-password-error" class="error"></span>
</div>
</td>
</tr>
<tr>
<td>
<div>
<div class="label">Backend port <div class="icon info" title="This is the backend port used by Olympus to connect to DCS. You don't need to forward it in your firewall anymore."></div></div>
<input id="backend-port" type='number' value="<%= typeof server !== 'undefined' ? server["port"]: "" %>" min="1023" max="65535" <%= !installed? "disabled": "" %> tabindex="<%= index + 1 %>">
<span id="backend-port-error" class="error"></span>
</div>
</td>
<td>
<div>
<div class="label">Blue Commander password <div class="icon info" title="This is the password you need to input to connect with the Blue Commander role."></div></div>
<input id="blue-commander-password" type="password" <%= !installed? "disabled": "" %> tabindex="<%= index + 4 %>">
<span id="blue-commander-password-error" class="error"></span>
</div>
</td>
</tr>
<tr>
<td>
<div>
<div class="label">Backend address <div class="icon info" title="This is the address that DCS will listen on. Unless you want to send direct API commands, leave this to localhost even for dedicated server installations."></div></div>
<input id="backend-address" type='text' value="<%= typeof server !== 'undefined' ? server["address"]: "" %>" min="1023" max="65535" <%= !installed? "disabled": "" %> tabindex="<%= index + 2 %>">
<span id="backend-address-error" class="error"></span>
</div>
</td>
<td>
<div>
<div class="label">Red Commander password <div class="icon info" title="This is the password you need to input to connect with the Red Commander role."></div></div>
<input id="red-commander-password" type="password" <%= !installed? "disabled": "" %> tabindex="<%= index + 5 %>">
<span id="red-commander-password-error" class="error"></span>
</div>
</td>
</tr>
</table>
<div class="action-buttons">
<button class="button add <%= installed? "hide": "" %>" tabindex="<%= index + 6 %>" title="Clicking on this will install all the necessary files in your DCS instance. Remember to close DCS before doing this!">Install Olympus to instance</button>
<button class="button apply <%= !installed? "hide": "" %>" tabindex="<%= index + 7 %>" title="Clicking on this will apply your changes to the configuration. Remember to restart any running mission!">Apply changes</button>
<button class="button remove <%= !installed? "hide": "" %>" tabindex="<%= index + 8 %>" title="Clicking on this will remove Olympus from your DCS folder.">Remove Olympus</button>
<button class="button update <%= !(version !== "n/a" && version !== newVersion)? "hide": "" %>" tabindex="<%= index + 9 %>" title="Clicking on this will update Olympus in your DCS folder.">Update</button>
</div>
</div>
</div>

174
manager/ejs/instances.ejs Normal file
View File

@ -0,0 +1,174 @@
<style>
#manager-instances {
padding-left: 80px;
padding-right: 80px;
}
#manager-instances .scroll-container {
height: 100%;
overflow-y: auto;
max-width: 100% !important;
width: 100%;
}
#manager-instances .scrollable {
display: flex;
row-gap: 15px;
column-gap: 15px;
height: fit-content;
width: 100%;
flex-wrap: wrap;
padding: 15px;
}
#manager-instances .option {
background-color: var(--darkgray);
width: 48%;
color: white;
display: flex;
font-size: 13px;
font-weight: 600;
padding: 15px;
align-items: center;
border-radius: 5px;
border-left: 5px solid var(--blue);
flex-direction: column;
row-gap: 25px;
}
#manager-instances>.instructions {
margin-bottom: 10px;
}
#manager-instances .button.cancel {
position: absolute;
left: 110px;
top: 180px;
}
#manager-instances .server-data {
display: flex;
column-gap: 15px;
row-gap: 5px;
flex-wrap: wrap;
}
#manager-instances .server-status {
font-weight: 600;
font-size: 15;
display: flex;
column-gap: 5px;
align-items: center;
}
#manager-instances .server-status::before {
display: block;
content: "";
width: 15px;
height: 15px;
border-radius: 999px;
background-color: var(--gray);
}
#manager-instances .server-status.offline {
color: var(--gray)
}
#manager-instances .server-status.offline::before {
background-color: var(--gray);
}
#manager-instances .server-status.online {
color: var(--green)
}
#manager-instances .server-status.online::before {
background-color: var(--green);
}
#manager-instances .server-status.backend {
margin-left: auto;
}
#manager-instances .server-data-entry {
display: flex;
column-gap: 5px;
align-items: center;
}
#manager-instances .server-data-entry span:nth-child(2) {
font-weight: 600;
}
#manager-instances .server-data-entry span:nth-child(3) {
font-weight: normal;
}
</style>
<div id="manager-instances">
<div class="content">
<div class="button cancel">
Return to menu
</div>
<div class="instructions">
<span>
<%= manage? "View and manage instances": "Update Olympus settings" %>
</span>
<span>
The following versions of Olympus have been detected.
</span>
<span>
<%= manage? "You can inspect and manage you Olympus instances by selecting the options below.": "You can edit your settings or remove Olympus by selecting the options 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="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>
<% if (!manage) { %>
<span><img src="./icons/folder-open-solid.svg"> <%= instances[i].folder %></span>
<% } else { %>
<div class="server-data">
<div class="server-status webserver online hide">ACTIVE</div>
<div class="server-status webserver offline">OFFLINE</div>
<div class="server-status backend online hide">CONNECTED</div>
<div class="server-status backend offline">DISCONNECTED</div>
<div class="server-data-entry fps"><img src="./icons/display-solid.svg"><span>FPS: </span><span class="data">0</span></div>
<div class="server-data-entry load"><img src="./icons/server-solid.svg"><span>Load: </span><span class="data">0</span></div>
<div class="server-data-entry uptime"></div>
</div>
<% } %>
<div class="divider"></div>
<div class="info">
<div>Client port</div>
<div> <%= instances[i].clientPort %> </div>
</div>
<div class="info">
<div>Backend port</div>
<div> <%= instances[i].backendPort %> </div>
</div>
<div class="info">
<div>Backend address</div>
<div> <%= instances[i].backendAddress %> </div>
</div>
</div>
<div class="instance-buttons">
<% if (!manage) { %>
<div class="button edit">Edit settings</div>
<div class="button uninstall">Uninstall</div>
<% } else { %>
<div class="button start-stop-server">Start server</div>
<!--<div class="button start-client">Start client</div>-->
<% } %>
</div>
</div>
<% } %>
</div>
</div>
</div>
</div>

View File

@ -1,110 +0,0 @@
<style>
#manager-connections {
display: flex;
flex-direction: column;
row-gap: 15px;
}
#manager-connections .input-group {
color: var(--offwhite);
display: flex;
flex-direction: column;
row-gap: 5px;
}
#manager-connections .input-group>span:nth-child(1) {
font-size: 14px;
font-weight: 600;
}
#manager-connections .input-group>span:nth-child(2) {
font-size: 13px;
font-weight: normal;
}
#manager-connections .input-group div {
display: flex;
align-items: center;
column-gap: 5px;
flex-wrap: wrap;
}
#manager-connections>.instructions {
margin-bottom: 10px;
}
#manager-connections>.buttons-footer {
margin-top: 10px;
}
#manager-connections .client-port input {
width: 150px;
}
#manager-connections .backend-port input {
width: 150px;
}
#manager-connections .success {
content: url("./icons/check-solid-green.svg");
height: 20px;
width: 20px;
}
#manager-connections .error img {
content: url("./icons/triangle-exclamation-solid.svg");
height: 20px;
width: 20px;
}
#manager-connections .error span {
font-weight: 600;
font-size: 12px;
color: var(--red);
height: fit-content;
}
</style>
<div id="manager-connections">
<div class="page-header">
Step 2: Port and address settings
</div>
<div class="input-group client-port">
<span>Client port</span>
<span>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"] %>">
<img class="success hide">
<div class="error hide">
<img> <span>Port already in use</span>
</div>
</div>
</div>
<div class="input-group backend-port">
<span>Backend port</span>
<span>This port is used to communicate with DCS. It is not necessary to allow this port through your firewall.</span>
<div>
<input type="number" min="1024" max="65535" value="<%= instance["backendPort"] %>">
<img class="success hide">
<div class="error hide">
<img> <span>Port already in use</span>
</div>
</div>
</div>
<div class="input-group backend-address">
<span>Backend address</span>
<span>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>
<input type="text" value="<%= instance["backendAddress"] %>">
</div>
<div class="buttons-footer">
<div class="button back">
Back
</div>
<div class="button next">
Next
</div>
<div class="button cancel">
Cancel
</div>
</div>
</div>

View File

@ -1,134 +0,0 @@
<style>
#manager-installations .scroll-container {
height: 440px;
overflow-y: auto;
}
#manager-installations .scrollable {
display: flex;
flex-direction: column;
row-gap: 8px;
height: fit-content;
}
#manager-installations .scrollable>.option {
cursor: pointer;
background-color: var(--darkgray);
width: 100%;
height: 100px;
color: white;
display: flex;
font-size: 13px;
font-weight: 600;
padding-left: 15px;
align-items: center;
border-radius: 5px;
border-left: 5px solid var(--blue);
}
#manager-installations .scrollable>.option * {
pointer-events: none;
}
#manager-installations .scrollable>.option.selected {
background-color: var(--blue);
}
#manager-installations .scrollable>.option .checkbox {
padding-right: 20px;
content: url("./icons/square-regular.svg");
}
#manager-installations .scrollable>.option.selected .checkbox {
content: url("./icons/check-solid.svg");
}
#manager-installations .scrollable>.option>div {
display: flex;
flex-direction: column;
row-gap: 5px;
}
#manager-installations .scrollable>.option>div>span:nth-child(1) {
font-size: 18px;
font-weight: 600;
}
#manager-installations .scrollable>.option>div>span:nth-child(2) {
font-size: 13px;
font-weight: 600;
color: var(--gray);
}
#manager-installations .scrollable>.option>div>span:nth-child(2).installed {
font-weight: 600;
color: var(--green);
}
#manager-installations .scrollable>.option>div>span:nth-child(2).error {
font-weight: 600;
color: orange;
}
#manager-installations .scrollable>.option>div>span:nth-child(3) {
display: flex;
column-gap: 10px;
justify-content: center;
font-size: 13px;
font-weight: normal;
}
#manager-installations>.instructions {
margin-bottom: 10px;
}
#manager-installations>.buttons-footer {
margin-top: 10px;
}
</style>
<div id="manager-installations">
<div class="page-header">
Step 1: Select DCS Installations
</div>
<div class="instructions">
<span>
Select the copy of DCS you want to install Olympus to.
</span>
<span>
For most people, this is your main DCS installation.
</span>
<span>
If you are running a dedicated server, you would also install Olympus to this DCS version.
</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 %>">
<img class="checkbox">
<div>
<span><%= instances[i].name %></span>
<span class="<%= instances[i].installed? (instances[i].error? 'error': 'installed'): '' %>">
<%= instances[i].installed? (instances[i].error? 'Corrupted/outdated Olympus installation': 'Olympus already installed'): 'Olympus not installed yet' %>
</span>
<span><img src="./icons/folder-open-solid.svg"> <%= instances[i].folder %></span>
</div>
</div>
<% } %>
</div>
</div>
<div class="buttons-footer">
<div class="button back">
Back
</div>
<div class="button next">
Next
</div>
<div class="button cancel">
Cancel
</div>
</div>
</div>

View File

@ -1,153 +0,0 @@
<style>
#manager-instances .scroll-container {
height: 500px;
overflow-y: auto;
}
#manager-instances .scrollable {
display: flex;
flex-direction: column;
row-gap: 8px;
height: fit-content;
}
#manager-instances .scrollable>.option {
cursor: pointer;
background-color: var(--darkgray);
width: 100%;
height: 230px;
color: white;
display: flex;
font-size: 13px;
font-weight: 600;
padding-left: 15px;
align-items: center;
border-radius: 5px;
border-left: 5px solid var(--blue);
}
#manager-instances .scrollable>.option * {
pointer-events: none;
}
#manager-instances .scrollable>.option.selected {
background-color: var(--blue);
}
#manager-instances .scrollable>.option .checkbox {
padding-right: 20px;
content: url("./icons/square-regular.svg");
}
#manager-instances .scrollable>.option.selected .checkbox {
content: url("./icons/check-solid.svg");
}
#manager-instances .scrollable>.option>div {
display: flex;
flex-direction: column;
row-gap: 5px;
}
#manager-instances .scrollable>.option>div>span:nth-child(1) {
font-size: 18px;
font-weight: 600;
}
#manager-instances .scrollable>.option>div>span:nth-child(2) {
font-size: 13px;
font-weight: 600;
color: var(--gray);
}
#manager-instances .scrollable>.option>div>span:nth-child(2).installed {
font-weight: 600;
color: var(--green);
}
#manager-instances .scrollable>.option>div>span:nth-child(2).error {
font-weight: 600;
color: orange;
}
#manager-instances .scrollable>.option>div>span:nth-child(3) {
display: flex;
column-gap: 10px;
justify-content: center;
font-size: 13px;
font-weight: normal;
}
#manager-instances>.instructions {
margin-bottom: 10px;
}
#manager-instances>.buttons-footer {
margin-top: 10px;
}
#manager-instances .info {
display: flex;
flex-direction: column;
row-gap: 5px;
}
#manager-instances .info>div:nth-child(1) {
font-weight: 600;
font-size: 14px;
}
#manager-instances .info>div:nth-child(2) {
font-weight: normal;
font-size: 14px;
}
</style>
<div id="manager-instances">
<div class="page-header">
Step 1: Select Olympus Installations
</div>
<div class="instructions">
<span>
Select the copy of Olympus you want to manage.
</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>
<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="info">
<div>Client port</div>
<div> <%= instances[i].clientPort %> </div>
</div>
<div class="info">
<div>Backend port</div>
<div> <%= instances[i].backendPort %> </div>
</div>
<div class="info">
<div>Backend address</div>
<div> <%= instances[i].backendAddress %> </div>
</div>
</div>
</div>
<% } %>
</div>
</div>
<div class="buttons-footer">
<div class="button back">
Back
</div>
<div class="button next">
Next
</div>
<div class="button cancel">
Cancel
</div>
</div>
</div>

View File

@ -1,45 +0,0 @@
<style>
#manager-menu {
display: flex;
flex-direction: column;
row-gap: 8px;
}
#manager-menu>.option {
background-color: var(--blue);
width: 100%;
height: 50px;
color: white;
display: flex;
font-size: 13px;
font-weight: 600;
padding-left: 15px;
align-items: center;
border-radius: 5px;
cursor: pointer;
}
#manager-menu>.option * {
pointer-events: none;
}
.inverted {
background-color: var(--gray) !important;
color: var(--background) !important;
}
</style>
<div id="manager-menu">
<div class="page-header">
What do you want to do?
</div>
<div class="option install">
Install Olympus
</div>
<div class="option update">
Update/remove Olympus
</div>
<div class="option inverted manage">
View and manage instances (WIP)
</div>
</div>

View File

@ -1,73 +0,0 @@
<style>
#manager-passwords {
display: flex;
flex-direction: column;
row-gap: 15px;
}
#manager-passwords .input-group {
color: var(--offwhite);
display: flex;
flex-direction: column;
row-gap: 5px;
}
#manager-passwords .input-group>span:nth-child(1) {
font-size: 14px;
font-weight: 600;
}
#manager-passwords .input-group>span:nth-child(2) {
font-size: 13px;
font-weight: normal;
}
#manager-passwords>.instructions {
margin-bottom: 10px;
}
#manager-passwords>.buttons-footer {
margin-top: 10px;
}
#manager-passwords input {
width: 250px;
}
</style>
<div id="manager-passwords">
<div class="page-header">
Step 3: Passwords
</div>
<div class="input-group">
<span>Enter your passwords to access Olympus</span>
<span>By using the passwords below, you can access different roles in Olympus.</span>
</div>
<div class="input-group game-master">
<span>Game Master Password</span>
<span>This password is used to access Olympus as Game Master with full priviledges.</span>
<input type="password" minlength="8">
</div>
<div class="input-group blue-commander">
<span>Blue Commander Password</span>
<span>This password is used to access Olympus as blue coalition Commander.</span>
<input type="password" minlength="8">
</div>
<div class="input-group red-commander">
<span>Red Commander Password</span>
<span>This password is used to access Olympus as red coalition Commander.</span>
<input type="password" minlength="8">
</div>
<div class="buttons-footer">
<div class="button back">
Back
</div>
<div class="button next">
Next
</div>
<div class="button cancel">
Cancel
</div>
</div>
</div>

View File

@ -1,160 +0,0 @@
<style>
#manager-result {
display: flex;
flex-direction: column;
row-gap: 15px;
}
#manager-result img.success {
content: url("./icons/check-solid-green.svg");
height: 20px;
width: 20px;
}
#manager-result img.error {
content: url("./icons/triangle-exclamation-solid.svg");
height: 20px;
width: 20px;
}
#manager-result img.wait {
content: url("./icons/spinner-solid.svg");
height: 20px;
width: 20px;
animation: rotate 2s linear infinite;
}
@keyframes rotate {
0% {
transform: rotate(0deg)
}
100% {
transform: rotate(360deg)
}
}
#manager-result .summary {
font-weight: 600;
font-size: 14px;
}
#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);
}
.step {
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(--blue);
width: 100%;
height: 80px;
color: white;
display: flex;
font-size: 13px;
font-weight: 600;
padding-left: 15px;
align-items: center;
border-radius: 5px;
}
#manager-result>.result>img {
margin-left: 5px;
margin-right: 20px;
}
#manager-result>.result>div {
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;
font-size: 13px;
font-weight: normal;
}
</style>
<div id="manager-result">
<div class="page-header">
Please wait while Olympus is being installed
</div>
<div class="step hook">
Installing hook scripts<img class="wait"><img class="success hide"><img class="error hide">
</div>
<div class="step mod">
Installing mod folder<img class="wait"><img class="success hide"><img class="error hide">
</div>
<div class="step json">
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">
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="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>
</div>
</div>
<div class="info success">
You may now start DCS and use Olympus either with the shortcuts or the "View and manage Olympus" entry in the
main menu
</div>
<div class="info error">
Please make sure DCS is not currently being executed
</div>
<div class="buttons-footer">
<div class="button back">
Back to main menu
</div>
<!--<div class="button cancel">
Close
</div>-->
</div>
</div>

88
manager/ejs/menu.ejs Normal file
View File

@ -0,0 +1,88 @@
<style>
#summary {
width: 70%;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
color: var(--offwhite);
padding: 80px;
}
#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 {
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: 80px;
color: var(--offwhite);
display: flex;
font-size: 20px;
font-weight: 600;
padding-left: 15px;
align-items: center;
border-radius: 5px;
cursor: pointer;
}
#manager-menu .option:hover {
color: var(--background);
background-color: var(--offwhite);
}
#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">
Install Olympus
</div>
<div class="option update">
Update/remove Olympus
</div>
<div class="option divider"></div>
<div class="option manage">
View and manage instances
</div>
</div>
</div>

48
manager/ejs/passwords.ejs Normal file
View File

@ -0,0 +1,48 @@
<style>
</style>
<div id="manager-passwords">
<div class="step-summary">
<div class="blue <%= !install? '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>
<div class="input-group game-master">
<span>Game Master Password<img src="./icons/circle-info-solid.svg" title="This password is used to access Olympus as Game Master with full privileges.">
</span>
<input type="password" minlength="8">
</div>
<div class="input-group blue-commander">
<span>Blue Commander Password<img src="./icons/circle-info-solid.svg" title="This password is used to access Olympus as blue coalition Commander.">
</span>
<input type="password" minlength="8">
</div>
<div class="input-group red-commander">
<span>Red Commander Password<img src="./icons/circle-info-solid.svg" title="This password is used to access Olympus as red coalition Commander.">
</span>
<input type="password" minlength="8">
</div>
<div class="buttons-footer">
<div class="button back">
Back
</div>
<div class="button next">
Next
</div>
</div>
<div class="button cancel">
Cancel installation
</div>
</div>
</div>

View File

@ -1,10 +0,0 @@
<div class="popup-header">
Information
</div>
<div class="popup-content">
<%= message %>
</div>
<div class="popup-footer">
<button class="button other <%= otherButton? "": " hide"%>" ><%= otherButton %></button>
<button class="button apply">Close</button>
</div>

146
manager/ejs/result.ejs Normal file
View File

@ -0,0 +1,146 @@
<style>
#manager-result .content {
width: 100% !important;
}
#manager-result img.success {
content: url("./icons/check-solid-green.svg");
height: 20px;
width: 20px;
}
#manager-result img.error {
content: url("./icons/triangle-exclamation-solid.svg");
height: 20px;
width: 20px;
}
#manager-result img.wait {
content: url("./icons/spinner-solid.svg");
height: 20px;
width: 20px;
animation: rotate 2s linear infinite;
}
#manager-result .summary {
font-weight: 600;
font-size: 24px;
}
#manager-result .summary.success {
color: var(--green);
}
#manager-result .summary.error {
color: var(--red);
}
#manager-result .info {
font-weight: 600;
font-size: 14px;
color: var(--offwhite);
}
#manager-result .step {
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;
display: flex;
font-size: 13px;
font-weight: 600;
padding-left: 15px;
align-items: center;
border-radius: 5px;
width: 500px;
}
#manager-result .result>img {
margin-left: 5px;
margin-right: 20px;
}
#manager-result .result>div {
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;
font-size: 13px;
font-weight: normal;
}
</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>
<div class="step mod <%= !install? 'hide': '' %>">
Installing mod folder<img class="wait"><img class="success hide"><img class="error hide">
</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
</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>
</div>
</div>
<div class="buttons-footer">
<div class="button back">
Back to main menu
</div>
</div>
</div>
</div>

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="#F2F2F2" d="M464 256A208 208 0 1 0 48 256a208 208 0 1 0 416 0zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zm169.8-90.7c7.9-22.3 29.1-37.3 52.8-37.3h58.3c34.9 0 63.1 28.3 63.1 63.1c0 22.6-12.1 43.5-31.7 54.8L280 264.4c-.2 13-10.9 23.6-24 23.6c-13.3 0-24-10.7-24-24V250.5c0-8.6 4.6-16.5 12.1-20.8l44.3-25.4c4.7-2.7 7.6-7.7 7.6-13.1c0-8.4-6.8-15.1-15.1-15.1H222.6c-3.4 0-6.4 2.1-7.5 5.3l-.4 1.2c-4.4 12.5-18.2 19-30.6 14.6s-19-18.2-14.6-30.6l.4-1.2zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>

After

Width:  |  Height:  |  Size: 761 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="18" viewBox="0 0 576 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 0C28.7 0 0 28.7 0 64V352c0 35.3 28.7 64 64 64H240l-10.7 32H160c-17.7 0-32 14.3-32 32s14.3 32 32 32H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H346.7L336 416H512c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64H64zM512 64V352H64V64H512z"/></svg>

After

Width:  |  Height:  |  Size: 512 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="#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>

After

Width:  |  Height:  |  Size: 659 B

View File

@ -9,40 +9,42 @@
href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;600;700;800&display=swap" />
<meta charset="UTF-8">
<title>DCS Olympus Manager v1.0.4</title>
<title>DCS Olympus Manager {{OLYMPUS_VERSION_NUMBER}}</title>
</head>
<body>
<div id="title-bar">
<div>DCS Olympus manager</div>
<button class="title-bar-button minimize"></button>
<!--<button class="title-bar-button restore hide"></button>-->
<!--<button class="title-bar-button maximize"></button>-->
<button class="title-bar-button restore hide"></button>
<button class="title-bar-button maximize"></button>
<button class="title-bar-button close"></button>
</div>
<div id="header">
<img class="main-icon" src="../img/OlympusLogoFinal_4k.png" \>
<div>
<div class="version">
<div> DCS Olympus Manager</div>
<div class="accent-green">v1.0.4</div>
<div class="accent-green">{{OLYMPUS_VERSION_NUMBER}}</div>
</div>
<div class="link first" data-link="https://github.com/Pax1601/DCSOlympus">GitHub</div>
<div class="link" data-link="https://discord.gg/pCfCykAdrw">Discord</div>
</div>
<div id="main-div">
<div id="loader" class="manager-page hide">
Loading, please wait...
</div>
<div id="grayout" class="hide"></div>
<div id="popup" class="hide">
<img src="./icons/triangle-exclamation-solid.svg">
<img src="./icons/triangle-exclamation-solid.svg" class="error">
<img src="./icons/circle-question-regular.svg" class="confirm">
<img src="./icons/spinner-solid.svg" class="wait">
<div class="content">
This is an example content
</div>
<div class="footer">
<div class="button accept-popup"> Accept </div>
<div class="button close-popup"> Close </div>
</div>
</div>
<div id="footer">
</div>
</body>
<script>
@ -50,13 +52,13 @@
window.ipcRender.send('window:minimize');
});
/*document.querySelector('.restore').addEventListener('click', () => {
document.querySelector('.restore').addEventListener('click', () => {
window.ipcRender.send('window:restore');
});
document.querySelector('.maximize').addEventListener('click', () => {
window.ipcRender.send('window:maximize');
});*/
});
document.querySelector('.close').addEventListener('click', () => {
window.ipcRender.send('window:close');

View File

@ -1,7 +1,7 @@
const ManagerPage = require("./managerpage");
const ejs = require('ejs')
class ManagerConnections extends ManagerPage {
class ConnectionsPage extends ManagerPage {
onBackClicked;
onNextClicked;
onCancelClicked;
@ -24,11 +24,10 @@ class ManagerConnections extends ManagerPage {
this.element.querySelector(".backend-address").querySelector("input").addEventListener("change", async (e) => { this.instance.setBackendAddress(e.target.value); })
}
show(instance) {
this.instance = instance;
this.options["instance"] = instance;
show() {
this.instance = this.options.instance;
ejs.renderFile("./ejs/managerconnections.ejs", this.options, {}, (err, str) => {
ejs.renderFile("./ejs/connections.ejs", this.options, {}, (err, str) => {
if (!err) {
this.render(str);
this.setClientPort(this.instance.clientPort);
@ -66,4 +65,4 @@ class ManagerConnections extends ManagerPage {
}
}
module.exports = ManagerConnections;
module.exports = ConnectionsPage;

View File

@ -4,9 +4,12 @@ const saveGamesKey = '{4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4}'
var fs = require('fs')
var path = require('path')
const vi = require('win-version-info');
const checkPort = require('./net')
const { checkPort, fetchWithTimeout } = require('./net')
const dircompare = require('dir-compare');
const { installJSON } = require('./filesystem')
const { spawn } = require('child_process');
const find = require('find-process');
const { deleteMod, uninstallInstance } = require('./filesystem')
const { showErrorPopup, showConfirmPopup } = require('./popup')
class DCSInstance {
static instances = null;
@ -29,14 +32,14 @@ class DCSInstance {
const searchpath = result[shellFoldersKey]['values'][saveGamesKey]['value'];
const folders = fs.readdirSync(searchpath);
var instances = [];
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)));
}
})
res(instances);
} else {
console.error("An error occured while trying to fetch the location of the DCS instances.")
@ -45,10 +48,10 @@ class DCSInstance {
}
})
});
return promise;
}
folder = "";
name = "";
clientPort = 3000;
@ -57,19 +60,26 @@ class DCSInstance {
gameMasterPassword = "";
blueCommanderPassword = "";
redCommanderPassword = "";
gameMasterPasswordHash = "";
installed = false;
error = false;
webserverOnline = false;
backendOnline = false;
missionTime = "";
load = 0;
fps = 0;
constructor(folder) {
this.folder = folder;
this.name = path.basename(folder);
if (fs.existsSync(path.join(folder, "Config", "olympus.json"))){
if (fs.existsSync(path.join(folder, "Config", "olympus.json"))) {
try {
var config = JSON.parse(fs.readFileSync(path.join(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) {
console.error(err)
}
@ -85,18 +95,44 @@ class DCSInstance {
res2 = dircompare.compareSync(path.join("..", "scripts", "OlympusHook.lua"), path.join(folder, "Scripts", "Hooks", "OlympusHook.lua"), options);
err1 = res1.differences !== 0;
err2 = res2.differences !== 0;
} catch(e) {
} catch (e) {
console.log(e);
}
if (err1 || err2) {
console.log(res1)
console.log(res2)
this.error = true;
}
}
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").innerTexr = this.load;
}
instanceDiv.querySelector(".start-stop-server").innerText = this.webserverOnline ? "Stop server" : "Start server";
}
}
}
}
}, 1000);
}
async setClientPort(newPort) {
if (await this.checkClientPort(newPort)) {
console.log(`Instance ${this.folder} client port set to ${newPort}`)
@ -136,7 +172,7 @@ class DCSInstance {
checkPort(port, async (portFree) => {
if (portFree) {
portFree = !(await DCSInstance.getInstances()).some((instance) => {
if (instance !== this) {
if (instance !== this && instance.installed) {
if (instance.clientPort === port || instance.backendPort === port) {
console.log(`Port ${port} already selected by other instance`);
return true;
@ -148,7 +184,7 @@ class DCSInstance {
}
}
return false;
})
})
}
else {
console.log(`Port ${port} currently in use`);
@ -164,7 +200,7 @@ class DCSInstance {
checkPort(port, async (portFree) => {
if (portFree) {
portFree = !(await DCSInstance.getInstances()).some((instance) => {
if (instance !== this) {
if (instance !== this && instance.installed) {
if (instance.clientPort === port || instance.backendPort === port) {
console.log(`Port ${port} already selected by other instance`);
return true;
@ -185,6 +221,90 @@ class DCSInstance {
})
return promise;
}
async getData() {
if (this.installed && !this.error) {
fetchWithTimeout(`http://localhost:${this.clientPort}`, { timeout: 250 })
.then(async (response) => {
this.webserverOnline = (await response.text()).includes("Olympus");
}, () => {
this.webserverOnline = false;
});
let headers = new Headers();
headers.set('Authorization', 'Basic ' + Buffer.from("manager" + ":" + this.gameMasterPasswordHash).toString('base64'));
fetchWithTimeout(`http://${this.backendAddress}:${this.backendPort}/olympus/mission`, {
method: 'GET',
headers: headers,
timeout: 250
}).then(async (response) => {
if (response.ok) {
this.backendOnline = true;
return response.text();
} else {
return Promise.reject(`Reponse error, status code: ${response.status}`);
}
}, () => {
this.backendOnline = false;
}).then((text) => {
var data = JSON.parse(text);
this.fps = data.frameRate;
this.load = data.load;
}, (err) => {
console.warn(err);
}).catch((err) => {
console.warn(err);
});
}
}
startServer() {
console.log(`Starting server for instance at ${this.folder}`)
const out = fs.openSync(`./${this.name}.log`, 'a');
const err = fs.openSync(`./${this.name}.log`, 'a');
const sub = spawn('npm.cmd', ['run', 'start', '--', '--config', path.join(this.folder, "Config", "olympus.json")], {
detached: true,
cwd: "../client",
stdio: ['ignore', out, err]
});
sub.unref();
}
stopServer() {
find('port', this.clientPort)
.then((list) => {
if (list.length !== 1) {
list.length === 0 ? console.error("No processes found on the specified port") : console.error("Too many processes found on the specified port");
} else {
if (list[0].name.includes("node.exe")) {
console.log(`Killing process ${list[0].name}`)
process.kill(list[0].pid);
}
else {
console.log(list[0])
console.error(`The process listening on the specified port has an incorrect name: ${list[0].name}`)
}
}
}, () => {
console.error("Error retrieving list of processes")
})
}
uninstall() {
showConfirmPopup("Are you sure you want to completely remove this Olympus installation?", () =>
uninstallInstance(this.folder).then(
() => {
location.reload();
},
() => {
showErrorPopup("An error has occurred while uninstalling the Olympus instance. Make sure Olympus and DCS are not running.", () => {
location.reload();
});
}
));
}
}
module.exports = DCSInstance;

View File

@ -2,38 +2,35 @@ const sha256 = require('sha256')
const createShortcut = require('create-desktop-shortcuts');
const fs = require('fs');
const path = require('path');
const { showErrorPopup, showWaitPopup } = require('./popup');
async function fixInstances(instances) {
var promise = new Promise((res, rej) => {
var instancePromises = instances.map((instance) => {
var instancePromise = new Promise((instanceRes, instanceErr) => {
installMod(instance.folder)
.then(() => installHooks(instance.folder))
.then(() => installShortCuts(instance.folder, instance.name))
console.log(`Fixing Olympus in ${instance.folder}`)
deleteMod(instance.folder)
.then(() => deleteHooks(instance.folder), (err) => { return Promise.reject(err); })
.then(() => installMod(instance.folder), (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;
});
console.log(instancePromises);
Promise.all(instancePromises).then(() => res(true), (err) => { rej(err) });
})
console.log(promise);
return promise;
}
async function installMod(folder) {
console.log(`Installing mod in ${folder}`)
async function uninstallInstance(folder) {
console.log(`Uninstalling Olympus from ${folder}`)
showWaitPopup("Please wait while the Olympus installation is being uninstalled.")
var promise = new Promise((res, rej) => {
fs.cp(path.join("..", "mod"), path.join(folder, "Mods", "Services", "Olympus"), { recursive: true }, (err) => {
if (err) {
console.log(`Error installing mod in ${folder}: ${err}`)
rej(err);
}
else {
console.log(`Mod succesfully installed in ${folder}`)
res(true);
}
});
deleteMod(folder)
.then(() => deleteHooks(folder), (err) => { return Promise.reject(err); })
.then(() => deleteJSON(folder), (err) => { return Promise.reject(err); })
.then(() => res(true), (err) => { rej(err) });
})
return promise;
}
@ -55,6 +52,23 @@ async function installHooks(folder) {
return promise;
}
async function installMod(folder) {
console.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) {
console.log(`Error installing mod in ${folder}: ${err}`)
rej(err);
}
else {
console.log(`Mod succesfully installed in ${folder}`)
res(true);
}
});
})
return promise;
}
async function installJSON(folder) {
console.log(`Installing config in ${folder}`)
var promise = new Promise((res, rej) => {
@ -138,11 +152,83 @@ async function installShortCuts(folder, name) {
return promise;
}
async function deleteHooks(folder) {
console.log(`Deleting hooks from ${folder}`);
var promise = new Promise((res, rej) => {
if (fs.existsSync(path.join(folder, "Scripts", "Hooks", "OlympusHook.lua"))) {
fs.rm(path.join(folder, "Scripts", "Hooks", "OlympusHook.lua"), (err) => {
if (err) {
console.log(`Error removing hooks from ${folder}: ${err}`)
rej(err);
}
else {
console.log(`Hooks succesfully removed from ${folder}`)
res(true);
}
});
} else {
res(true);
}
})
return promise;
}
async function deleteMod(folder) {
console.log(`Deleting mod from ${folder}`);
var promise = new Promise((res, rej) => {
if (fs.existsSync(path.join(folder, "Mods", "Services", "Olympus"))) {
fs.rmdir(path.join(folder, "Mods", "Services", "Olympus"), { recursive: true, force: true }, (err) => {
if (err) {
console.log(`Error removing mod from ${folder}: ${err}`)
rej(err);
}
else {
console.log(`Mod succesfully removed from ${folder}`)
res(true);
}
})
} else {
res(true);
};
})
return promise;
}
async function deleteJSON(folder) {
console.log(`Deleting JSON from ${folder}`);
var promise = new Promise((res, rej) => {
if (fs.existsSync(path.join(folder, "Config", "olympus.json"))) {
fs.rm(path.join(folder, "Config", "olympus.json"), (err) => {
if (err) {
console.log(`Error removing JSON from ${folder}: ${err}`)
rej(err);
}
else {
console.log(`JSON succesfully removed from ${folder}`)
res(true);
}
});
}
else {
res(true);
}
})
return promise;
}
async function deleteShortCuts() {
}
module.exports = {
applyConfiguration: applyConfiguration,
installJSON: installJSON,
installHooks: installHooks,
installMod: installMod,
installShortCuts, installShortCuts,
fixInstances: fixInstances
fixInstances: fixInstances,
deleteHooks: deleteHooks,
deleteJSON: deleteJSON,
deleteMod: deleteMod,
uninstallInstance: uninstallInstance
}

View File

@ -0,0 +1,41 @@
const DCSInstance = require("./dcsinstance");
const ManagerPage = require("./managerpage");
const ejs = require('ejs')
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));
}
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 {
console.error(err);
}
});
super.show();
}
}
module.exports = InstallationsPage;

View File

@ -0,0 +1,64 @@
const DCSInstance = require("./dcsinstance");
const ManagerPage = require("./managerpage");
const ejs = require('ejs');
const { showErrorPopup } = require("./popup");
class InstancesPage extends ManagerPage {
onCancelClicked;
setSelectedInstance;
startInstance;
constructor(options) {
super(options);
}
render(str) {
this.element.innerHTML = str;
var editButtons = this.element.querySelectorAll(".edit");
for (let i = 0; i < editButtons.length; i++) {
editButtons[i].onclick = (e) => {this.onEditClicked(e);}
}
var uninstallButtons = this.element.querySelectorAll(".uninstall");
for (let i = 0; i < uninstallButtons.length; i++) {
uninstallButtons[i].onclick = (e) => {this.onUninstallClicked(e);}
}
var startButtons = this.element.querySelectorAll(".start-stop-server");
for (let i = 0; i < startButtons.length; i++) {
startButtons[i].onclick = (e) => {this.onStartStopClicked(e);}
}
this.element.querySelector(".cancel").addEventListener("click", (e) => this.onCancelClicked(e));
}
async onEditClicked(e) {
console.log(e.target.dataset.folder)
this.setSelectedInstance((await DCSInstance.getInstances()).find((instance) => {return instance.folder === e.target.closest('.option').dataset.folder}));
}
async onStartStopClicked(e) {
var instance = (await DCSInstance.getInstances()).find((instance) => {return instance.folder === e.target.closest('.option').dataset.folder});
instance.webserverOnline? instance.stopServer(): instance.startServer();
}
async onUninstallClicked(e) {
var instance = (await DCSInstance.getInstances()).find((instance) => {return instance.folder === e.target.closest('.option').dataset.folder});
instance.webserverOnline || instance.backendOnline? showErrorPopup("Error, the selected Olympus instance is currently active, please stop Olympus before uninstalling it!") : instance.uninstall();
}
show() {
ejs.renderFile("./ejs/instances.ejs", this.options, {}, (err, str) => {
if (!err) {
this.render(str);
} else {
console.error(err);
}
});
super.show();
}
}
module.exports = InstancesPage;

View File

@ -0,0 +1,205 @@
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 DCSInstance = require('./dcsinstance');
const { showErrorPopup } = require('./popup');
const { fixInstances } = require('./filesystem');
class Manager {
constructor() {
}
async start() {
var instances = await DCSInstance.getInstances();
document.getElementById("loader").classList.add("hide");
if (instances.some((instance) => {
return instance.installed && instance.error;
})) {
showErrorPopup("One or more Olympus instances are corrupted or need updating. Press Close to fix this.", async () => {
fixInstances(instances.filter((instance) => {
return instance.installed && instance.error;
})).then(
() => { location.reload() },
() => { showErrorPopup("An error occurred while trying to fix you installations. Please reinstall Olympus manually"); }
)
})
}
/* Menu */
var menuPage = new MenuPage();
menuPage.onInstallClicked = (e) => {
menuPage.hide();
installationsPage.show();
}
menuPage.onUpdateClicked = (e) => {
menuPage.hide();
instancesPage.options = {
...instancesPage.options,
manage: false
}
instancesPage.show();
}
menuPage.onManageClicked = (e) => {
menuPage.hide();
instancesPage.options = {
...instancesPage.options,
manage: true
}
instancesPage.show();
}
/* Installations */
var installationsPage = new InstallationsPage();
installationsPage.options = {
...installationsPage.options,
instances: instances
}
installationsPage.setSelectedInstance = (activeInstance) => {
connectionsPage.options = {
...connectionsPage.options,
instance: activeInstance,
install: true
}
passwordsPage.options = {
...passwordsPage.options,
instance: activeInstance,
install: true
}
resultPage.options = {
...resultPage.options,
instance: activeInstance,
install: true
}
installationsPage.hide();
connectionsPage.show();
connectionsPage.onBackClicked = (e) => {
connectionsPage.hide();
installationsPage.show();
}
}
installationsPage.onCancelClicked = (e) => {
installationsPage.hide();
menuPage.show();
}
/* Instances */
var instancesPage = new InstancesPage();
instancesPage.options = {
...instancesPage.options,
instances: instances.filter((instance) => { return instance.installed; })
}
instancesPage.setSelectedInstance = (activeInstance) => {
connectionsPage.options = {
...connectionsPage.options,
instance: activeInstance,
install: false
}
passwordsPage.options = {
...passwordsPage.options,
instance: activeInstance,
install: false
}
resultPage.options = {
...resultPage.options,
instance: activeInstance,
install: false
}
instancesPage.hide();
connectionsPage.show();
connectionsPage.onBackClicked = (e) => {
connectionsPage.hide();
instancesPage.show();
}
}
instancesPage.onCancelClicked = (e) => {
instancesPage.hide();
menuPage.show();
}
/* Connections */
var connectionsPage = new ConnectionsPage();
connectionsPage.onNextClicked = async (e) => {
let activeInstance = connectionsPage.options.instance;
if (activeInstance) {
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.")
}
}
connectionsPage.onCancelClicked = (e) => {
connectionsPage.hide();
menuPage.show();
}
/* Passwords */
var passwordsPage = new PasswordsPage();
passwordsPage.onBackClicked = (e) => {
let activeInstance = connectionsPage.options.instance;
if (activeInstance) {
passwordsPage.hide();
connectionsPage.show();
} else {
showErrorPopup("An error has occurred, please restart the Olympus Manager.")
}
}
passwordsPage.onNextClicked = (e) => {
let activeInstance = connectionsPage.options.instance;
if (activeInstance) {
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.")
}
}
passwordsPage.onCancelClicked = (e) => {
passwordsPage.hide();
menuPage.show();
}
/* Result */
var resultPage = new ResultPage();
resultPage.onBackClicked = (e) => {
resultPage.hide();
location.reload();
}
resultPage.onCancelClicked = (e) => {
resultPage.hide();
location.reload();
}
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());
menuPage.show();
}
}
module.exports = Manager;

View File

@ -1,55 +0,0 @@
const ManagerPage = require("./managerpage");
const ejs = require('ejs')
class ManagerInstallations extends ManagerPage {
onBackClicked;
onNextClicked;
onCancelClicked;
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(".back").addEventListener("click", (e) => this.onBackClicked(e));
this.element.querySelector(".next").addEventListener("click", (e) => this.onNextClicked(e));
this.element.querySelector(".cancel").addEventListener("click", (e) => this.onCancelClicked(e));
}
onOptionClicked(e) {
var options = this.element.querySelectorAll(".option");
for (let i = 0; i < options.length; i++) {
options[i].classList.remove("selected");
}
e.target.classList.add("selected");
}
getSelectedInstance() {
return this.options.instances.find((instance) => {
const selected = this.element.querySelector(".selected");
return selected? selected.dataset.folder === instance.folder: false;
});
}
show() {
ejs.renderFile("./ejs/managerinstallations.ejs", this.options, {}, (err, str) => {
if (!err) {
this.render(str);
} else {
console.error(err);
}
});
super.show();
}
}
module.exports = ManagerInstallations;

View File

@ -1,55 +0,0 @@
const ManagerPage = require("./managerpage");
const ejs = require('ejs')
class ManagerInstances extends ManagerPage {
onBackClicked;
onNextClicked;
onCancelClicked;
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(".back").addEventListener("click", (e) => this.onBackClicked(e));
this.element.querySelector(".next").addEventListener("click", (e) => this.onNextClicked(e));
this.element.querySelector(".cancel").addEventListener("click", (e) => this.onCancelClicked(e));
}
onOptionClicked(e) {
var options = this.element.querySelectorAll(".option");
for (let i = 0; i < options.length; i++) {
options[i].classList.remove("selected");
}
e.target.classList.add("selected");
}
getSelectedInstance() {
return this.options.instances.find((instance) => {
const selected = this.element.querySelector(".selected");
return selected? selected.dataset.folder === instance.folder: false;
});
}
show() {
ejs.renderFile("./ejs/managerinstances.ejs", this.options, {}, (err, str) => {
if (!err) {
this.render(str);
} else {
console.error(err);
}
});
super.show();
}
}
module.exports = ManagerInstances;

View File

@ -1,7 +1,7 @@
const ManagerPage = require("./managerpage");
const ejs = require('ejs')
class ManagerMenu extends ManagerPage {
class MenuPage extends ManagerPage {
onInstallClicked;
onUpdateClicked;
onManageClicked;
@ -9,7 +9,7 @@ class ManagerMenu extends ManagerPage {
constructor(options) {
super(options);
ejs.renderFile("./ejs/managermenu.ejs", options, {}, (err, str) => {
ejs.renderFile("./ejs/menu.ejs", options, {}, (err, str) => {
if (!err) {
this.render(str);
} else {
@ -28,4 +28,4 @@ class ManagerMenu extends ManagerPage {
}
}
module.exports = ManagerMenu;
module.exports = MenuPage;

View File

@ -11,4 +11,22 @@ function checkPort(port, callback) {
});
}
module.exports = checkPort;
async function fetchWithTimeout(resource, options = {}) {
const { timeout = 8000 } = options;
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const response = await fetch(resource, {
...options,
signal: controller.signal
});
clearTimeout(id);
return response;
}
module.exports = {
checkPort: checkPort,
fetchWithTimeout: fetchWithTimeout
}

View File

@ -1,273 +0,0 @@
function showPopup(message, otherButton, otherButtonCallback) {
var data = {
message: message,
otherButton: otherButton
};
var popups = document.querySelectorAll(".popup");
for (let i = 0; i < popups.length; i++) {
document.body.removeChild(popups[i])
}
ejs.renderFile("./ejs/popup.ejs", data, {}, (err, str) => {
var div = document.createElement("div");
div.classList.add("popup");
div.innerHTML = str;
document.body.appendChild(div);
div.querySelector(".apply").addEventListener("click", () => {
document.body.removeChild(div);
})
div.querySelector(".other").addEventListener("click", () => {
otherButtonCallback();
})
});
}
class InstanceDiv {
element = null;
parent = null;
folder = "";
constructor(parent, folder) {
this.element = parent;
this.folder = folder;
this.render();
}
render() {
this.element = document.createElement("div");
var data = {
folder: this.folder,
installed: false,
index: instanceDivs.length * 10
};
var newVersionInfo = vi(path.join("..", "mod", "bin", "olympus.dll"));
data["newVersion"] = newVersionInfo.ProductVersion;
data["version"] = "n/a";
if (fs.existsSync(path.join(this.folder, "Config", "olympus.json"))) {
var config = JSON.parse(fs.readFileSync(path.join(this.folder, "Config", "olympus.json")));
data = {
...data,
...config
}
data["installed"] = true;
try {
data["version"] = vi(path.join(this.folder, "Mods", "Services", "Olympus", "bin", "olympus.dll")).ProductVersion;
} catch (e) {
data["version"] = "n/a";
}
}
ejs.renderFile("./ejs/instanceDiv.ejs", data, {}, (err, str) => {
this.element.innerHTML = str;
this.element.querySelector(".add").addEventListener("click", (e) => {
if (!e.srcElement.classList.contains("disabled")) {
showPopup("Please wait while Olympus is being installed");
window.setTimeout(() => {
if (installOlympus(this.folder)) {
showPopup("Olympus installed successfully. Use the provided form to set Olympus properties. All fields are mandatory. Click on \"Create desktop shortcuts\" to generate Olympus shortcuts on your desktop.", "Create desktop shortcuts", () => {
createDesktopShortcuts(this.folder);
});
} else {
showPopup("An error has occurred during installation");
}
}, 100);
}
});
this.element.querySelector(".remove").addEventListener("click", (e) => {
if (!e.srcElement.classList.contains("disabled")) {
showPopup("Please wait while Olympus is being uninstalled from DCS instance");
window.setTimeout(() => {
if (uninstallOlympus(this.folder)) {
showPopup("Olympus uninstalled successfully from DCS instance!");
} else {
showPopup("An error has occurred during uninstallation");
}
}, 100);
}
});
this.element.querySelector(".apply").addEventListener("click", (e) => {
e.srcElement.classList.remove("blink");
if (!e.srcElement.classList.contains("disabled")) {
showPopup("Please wait while the configuration is being applied");
window.setTimeout(() => {
if (applyConfiguration(this.folder, this.getFields())) {
showPopup("Olympus configuration applied successfully!");
} else {
showPopup("An error has occurred while applying the configuration");
}
}, 100)
}
});
this.element.querySelector(".update").addEventListener("click", (e) => {
if (!e.srcElement.classList.contains("disabled")) {
showPopup("Please wait while Olympus is being updated in the DCS instance");
window.setTimeout(() => {
if (updateOlympus(this.folder)) {
showPopup("Olympus updated successfully from DCS instance!");
} else {
showPopup("An error has occurred during the update");
}
}, 100);
}
});
var inputs = this.element.querySelectorAll("input");
for (let i = 0; i < inputs.length; i++) {
inputs[i].addEventListener("change", () => {
inputs[i].classList.remove("error");
instanceDivs.forEach((instanceDiv) => instanceDiv.checkFields())
})
}
});
}
getDiv() {
return this.element;
}
getFields() {
return {
clientPort: Number(this.element.querySelector("#client-port").value),
backendPort: Number(this.element.querySelector("#backend-port").value),
backendAddress: this.element.querySelector("#backend-address").value,
gameMasterPassword: this.element.querySelector("#game-master-password").value,
blueCommanderPassword: this.element.querySelector("#blue-commander-password").value,
redCommanderPassword: this.element.querySelector("#red-commander-password").value,
}
}
checkFields() {
var data = this.getFields();
/* Clear existing errors */
var inputs = this.element.querySelectorAll("input");
for (let i = 0; i < inputs.length; i++) {
inputs[i].classList.remove("error");
}
var messages = this.element.querySelectorAll(".error");
for (let i = 0; i < messages.length; i++) {
messages[i].innerText = "";
}
/* Enable the button */
this.element.querySelector(".apply").classList.remove("disabled");
if (data["clientPort"] !== 0 && data["backendPort"] !== 0) {
if (data["clientPort"] === data["backendPort"]) {
this.element.querySelector("#client-port").classList.add("error");
this.element.querySelector("#client-port-error").innerText = "Ports must be different";
this.element.querySelector("#backend-port").classList.add("error");
this.element.querySelector("#backend-port-error").innerText = "Ports must be different";
this.element.querySelector(".apply").classList.add("disabled");
}
else {
checkPort(data["clientPort"], (res) => {
var otherInstanceUsesPort = instanceDivs.find((instanceDiv) => {
if (instanceDiv != this) {
var fields = instanceDiv.getFields();
if (fields["clientPort"] === data["clientPort"] || fields["backendPort"] === data["clientPort"]) {
return true;
}
}
})
if (!res || otherInstanceUsesPort) {
this.element.querySelector("#client-port").classList.add("error");
this.element.querySelector("#client-port-error").innerText = "Port already in use";
this.element.querySelector(".apply").classList.add("disabled");
}
});
checkPort(data["backendPort"], (res) => {
var otherInstanceUsesPort = instanceDivs.find((instanceDiv) => {
if (instanceDiv != this) {
var fields = instanceDiv.getFields();
if (fields["clientPort"] === data["backendPort"] || fields["backendPort"] === data["backendPort"]) {
return true;
}
}
})
if (!res || otherInstanceUsesPort) {
this.element.querySelector("#backend-port").classList.add("error");
this.element.querySelector("#backend-port-error").innerText = "Port already in use";
this.element.querySelector(".apply").classList.add("disabled");
}
});
}
}
if (data["gameMasterPassword"] !== "" && data["blueCommanderPassword"] !== "" && data["gameMasterPassword"] === data["blueCommanderPassword"]) {
this.element.querySelector("#game-master-password").classList.add("error");
this.element.querySelector("#game-master-password-error").innerText = "Passwords must be different";
this.element.querySelector("#blue-commander-password").classList.add("error");
this.element.querySelector("#blue-commander-password-error").innerText = "Passwords must be different";
this.element.querySelector(".apply").classList.add("disabled");
}
if (data["gameMasterPassword"] !== "" && data["redCommanderPassword"] !== "" && data["gameMasterPassword"] === data["redCommanderPassword"]) {
this.element.querySelector("#game-master-password").classList.add("error");
this.element.querySelector("#game-master-password-error").innerText = "Passwords must be different";
this.element.querySelector("#red-commander-password").classList.add("error");
this.element.querySelector("#red-commander-password-error").innerText = "Passwords must be different";
this.element.querySelector(".apply").classList.add("disabled");
}
if (data["blueCommanderPassword"] !== "" && data["redCommanderPassword"] !== "" && data["blueCommanderPassword"] === data["redCommanderPassword"]) {
this.element.querySelector("#blue-commander-password").classList.add("error");
this.element.querySelector("#blue-commander-password-error").innerText = "Passwords must be different";
this.element.querySelector("#red-commander-password").classList.add("error");
this.element.querySelector("#red-commander-password-error").innerText = "Passwords must be different";
this.element.querySelector(".apply").classList.add("disabled");
}
if (data["gameMasterPassword"] === "" || data["blueCommanderPassword"] === "" || data["redCommanderPassword"] === "") {
this.element.querySelector(".apply").classList.add("disabled");
}
}
}
function loadDivs() {
regedit.list(shellFoldersKey, function (err, result) {
if (err) {
console.log(err);
}
else {
if (result[shellFoldersKey] !== undefined && result[shellFoldersKey]["exists"] && result[shellFoldersKey]['values'][saveGamesKey] !== undefined && result[shellFoldersKey]['values'][saveGamesKey]['value'] !== undefined) {
const searchpath = result[shellFoldersKey]['values'][saveGamesKey]['value'];
const folders = fs.readdirSync(searchpath);
instanceDivs = [];
const mainDiv = document.getElementById("main-div");
folders.forEach((folder) => {
if (fs.existsSync(path.join(searchpath, folder, "Config", "appsettings.lua")) ||
fs.existsSync(path.join(searchpath, folder, "Config", "serversettings.lua"))) {
instanceDivs.push(new InstanceDiv(mainDiv, path.join(searchpath, folder)));
}
});
mainDiv.replaceChildren(...instanceDivs.map((instanceDiv) => {
return instanceDiv.getDiv();
}));
instanceDivs.forEach((instanceDiv) => instanceDiv.checkFields())
} else {
console.error("An error occured while trying to fetch the location of the DCS folders.")
}
}
})
}

View File

@ -1,7 +1,7 @@
const ManagerPage = require("./managerpage");
const ejs = require('ejs')
class ManagerPasswords extends ManagerPage {
class PasswordsPage extends ManagerPage {
onBackClicked;
onNextClicked;
onCancelClicked;
@ -23,11 +23,10 @@ class ManagerPasswords extends ManagerPage {
this.element.querySelector(".red-commander").querySelector("input").addEventListener("change", async (e) => { this.instance.setRedCommanderPassword(e.target.value); })
}
show(instance) {
this.instance = instance;
this.options["instance"] = instance;
show() {
this.instance = this.options.instance;
ejs.renderFile("./ejs/managerpasswords.ejs", this.options, {}, (err, str) => {
ejs.renderFile("./ejs/passwords.ejs", this.options, {}, (err, str) => {
if (!err) {
this.render(str);
} else {
@ -39,4 +38,4 @@ class ManagerPasswords extends ManagerPage {
}
}
module.exports = ManagerPasswords;
module.exports = PasswordsPage;

View File

@ -1,10 +1,55 @@
function showPopup(message, onCloseCallback) {
function showErrorPopup(message, onCloseCallback) {
document.getElementById("grayout").classList.remove("hide");
document.getElementById("popup").classList.remove("hide");
document.getElementById("popup").querySelector(".close-popup").addEventListener("click", (e) => {
document.getElementById("popup").querySelector(".error").classList.remove("hide");
document.getElementById("popup").querySelector(".wait").classList.add("hide");
document.getElementById("popup").querySelector(".confirm").classList.add("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();
onCloseCallback();
})
if (onCloseCallback)
onCloseCallback();
}
document.getElementById("popup").querySelector(".content").innerText = message;
}
function showWaitPopup(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").innerText = message;
}
function showConfirmPopup(message, onAcceptCallback, 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.remove("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();
}
/* Not using event listeners to make sure we only have one callback */
document.getElementById("popup").querySelector(".accept-popup").onclick = (e) => {
hidePopup();
if (onAcceptCallback)
onAcceptCallback();
}
document.getElementById("popup").querySelector(".content").innerText = message;
}
@ -14,6 +59,8 @@ function hidePopup() {
}
module.exports = {
showPopup: showPopup,
showErrorPopup: showErrorPopup,
showConfirmPopup: showConfirmPopup,
showWaitPopup: showWaitPopup,
hidePopup: hidePopup
}

View File

@ -1,15 +1,8 @@
const Manager = require('./manager');
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
const ManagerMenu = require("./managermenu");
const ManagerInstallations = require('./managerinstallations');
const DCSInstance = require('./dcsinstance');
const ManagerConnections = require('./managerconnections');
const ManagerPasswords = require('./managerpasswords');
const { showPopup } = require('./popup');
const ManagerResult = require('./managerresult');
const { fixInstances } = require('./filesystem');
const ManagerInstances = require('./managerinstances');
const { exec } = require("child_process");
/* White-listed channels. */
const ipc = {
@ -57,156 +50,35 @@ contextBridge.exposeInMainWorld(
return ipcRenderer.invoke(channel, args);
}
}
}
);
});
var activeInstance;
async function setup() {
var instances = await DCSInstance.getInstances();
if (instances.some((instance) => {
return instance.installed && instance.error;
})) {
showPopup("One or more Olympus instances are corrupted or need updating. Press Close to fix this.", async () => {
fixInstances(instances.filter((instance) => {
return instance.installed && instance.error;
})).then(
() => { location.reload() },
() => { showPopup("An error occurred while trying to fix you installations. Please reinstall Olympus manually") }
)
})
}
/* Menu */
var managerMenu = new ManagerMenu();
managerMenu.onInstallClicked = (e) => {
managerMenu.hide();
managerInstallations.show();
}
managerMenu.onUpdateClicked = (e) => {
managerMenu.hide();
managerInstances.show();
}
/* Installations */
var managerInstallations = new ManagerInstallations({ instances: instances });
managerInstallations.onBackClicked = (e) => {
managerInstallations.hide();
managerMenu.show();
}
managerInstallations.onNextClicked = (e) => {
activeInstance = managerInstallations.getSelectedInstance();
if (activeInstance) {
managerInstallations.hide();
managerConnections.show(activeInstance);
} else {
showPopup("Please select the instance you want to install Olympus into.")
}
}
managerInstallations.onCancelClicked = (e) => {
managerInstallations.hide();
managerMenu.show();
}
/* Instances */
var managerInstances = new ManagerInstances({ instances: instances.filter((instance) => {return instance.installed; }) });
managerInstances.onBackClicked = (e) => {
managerInstances.hide();
managerMenu.show();
}
managerInstances.onNextClicked = (e) => {
activeInstance = managerInstances.getSelectedInstance();
if (activeInstance) {
managerInstances.hide();
managerConnections.show(activeInstance);
} else {
showPopup("Please select the instance you want to manage.")
}
}
managerInstances.onCancelClicked = (e) => {
managerInstances.hide();
managerMenu.show();
}
/* Connections */
var managerConnections = new ManagerConnections();
managerConnections.onBackClicked = (e) => {
managerConnections.hide();
managerInstallations.show();
}
managerConnections.onNextClicked = async (e) => {
if (activeInstance) {
if (await activeInstance.checkClientPort(activeInstance.clientPort) && await activeInstance.checkBackendPort(activeInstance.backendPort)) {
managerConnections.hide();
managerPasswords.show(activeInstance);
} else {
showPopup("Please make sure the selected ports are not already in use.")
}
} else {
showPopup("An error has occurred, please restart the Olympus Manager.")
}
}
managerConnections.onCancelClicked = (e) => {
managerConnections.hide();
managerMenu.show();
}
/* Passwords */
var managerPasswords = new ManagerPasswords();
managerPasswords.onBackClicked = (e) => {
if (activeInstance) {
managerPasswords.hide();
managerConnections.show(activeInstance);
} else {
showPopup("An error has occurred, please restart the Olympus Manager.")
}
}
managerPasswords.onNextClicked = (e) => {
if (activeInstance) {
if (activeInstance.gameMasterPassword === "" || activeInstance.blueCommanderPassword === "" || activeInstance.redCommanderPassword === "") {
showPopup("Please fill all the password inputs.")
}
else if (activeInstance.gameMasterPassword === activeInstance.blueCommanderPassword || activeInstance.blueCommanderPassword === activeInstance.redCommanderPassword || activeInstance.gameMasterPassword === activeInstance.redCommanderPassword) {
showPopup("All the passwords must be different from each other.")
} else {
managerPasswords.hide();
managerResult.show(activeInstance);
managerResult.startInstallation();
}
} else {
showPopup("An error has occurred, please restart the Olympus Manager.")
}
}
managerPasswords.onCancelClicked = (e) => {
managerPasswords.hide();
managerMenu.show();
}
/* Result */
var managerResult = new ManagerResult();
managerResult.onBackClicked = (e) => {
managerResult.hide();
location.reload();
}
managerResult.onCancelClicked = (e) => {
managerResult.hide();
location.reload();
}
document.body.appendChild(managerMenu.getElement());
document.body.appendChild(managerInstallations.getElement());
document.body.appendChild(managerInstances.getElement());
document.body.appendChild(managerConnections.getElement());
document.body.appendChild(managerPasswords.getElement());
document.body.appendChild(managerResult.getElement());
managerMenu.show();
}
const manager = new Manager();
/* On content loaded */
window.addEventListener('DOMContentLoaded', () => {
setup();
})
window.addEventListener('DOMContentLoaded', async () => {
computePagesHeight();
document.getElementById("loader").classList.remove("hide");
await manager.start();
computePagesHeight();
var links = document.querySelectorAll(".link");
for (let i = 0; i < links.length; i++) {
links[i].addEventListener("click", (e) => {
exec("start " + e.target.dataset.link);
})
}
})
window.addEventListener('resize', () => {
computePagesHeight();
})
function computePagesHeight() {
var pages = document.querySelectorAll(".manager-page");
var titleBar = document.querySelector("#title-bar");
var header = document.querySelector("#header");
for (let i = 0; i < pages.length; i++) {
pages[i].style.height = (window.innerHeight - (titleBar.clientHeight + header.clientHeight)) + "px";
}
}

View File

@ -2,7 +2,7 @@ const { installMod, installHooks, installJSON, applyConfiguration, installShortC
const ManagerPage = require("./managerpage");
const ejs = require('ejs')
class ManagerResult extends ManagerPage {
class ResultPage extends ManagerPage {
onBackClicked;
onNextClicked;
onCancelClicked;
@ -16,14 +16,12 @@ class ManagerResult extends ManagerPage {
element.innerHTML = str;
this.element.querySelector(".back").addEventListener("click", (e) => this.onBackClicked(e));
//this.element.querySelector(".cancel").addEventListener("click", (e) => this.onCancelClicked(e));
}
show(instance) {
this.instance = instance;
this.options["instance"] = instance;
show() {
this.instance = this.options.instance;
ejs.renderFile("./ejs/managerresult.ejs", this.options, {}, (err, str) => {
ejs.renderFile("./ejs/result.ejs", this.options, {}, (err, str) => {
if (!err) {
this.render(str);
} else {
@ -110,4 +108,4 @@ class ManagerResult extends ManagerPage {
}
}
module.exports = ManagerResult;
module.exports = ResultPage;

View File

@ -7,10 +7,11 @@ let window;
function createWindow() {
const window = new electronBrowserWindow({
width: 500,
height: 800,
width: 1500,
height: 850,
frame: false,
resizable: false,
resizable: true,
maximizable: true,
webPreferences: {
contextIsolation: true,
preload: path.join(__dirname, "javascripts", 'preload.js'),

119
manager/out.log Normal file
View File

@ -0,0 +1,119 @@
^CTerminare il processo batch (S/N)?
*********************************************************************
* _____ _____ _____ ____ _ *
* | __ \ / ____|/ ____| / __ \| | *
* | | | | | | (___ | | | | |_ _ _ __ ___ _ __ _ _ ___ *
* | | | | | \___ \ | | | | | | | | '_ ` _ \| '_ \| | | / __| *
* | |__| | |____ ____) | | |__| | | |_| | | | | | | |_) | |_| \__ \ *
* |_____/ \_____|_____/ \____/|_|\__, |_| |_| |_| .__/ \__,_|___/ *
* __/ | | | *
* |___/ |_| *
*********************************************************************

Please wait while DCS Olympus Server starts up...
Config location: C:\Users\Davide Passoni\Saved Games\DCS\Config\olympus.json
node:internal/modules/cjs/loader:1078
throw err;
^
Error: Cannot find module 'http-proxy-middleware'
Require stack:
- C:\Users\Davide Passoni\Documents\DCSOlympus\client\app.js
- C:\Users\Davide Passoni\Documents\DCSOlympus\client\bin\www
at Module._resolveFilename (node:internal/modules/cjs/loader:1075:15)
at Module._load (node:internal/modules/cjs/loader:920:27)
at Module.require (node:internal/modules/cjs/loader:1141:19)
at require (node:internal/modules/cjs/helpers:110:18)
at module.exports (C:\Users\Davide Passoni\Documents\DCSOlympus\client\app.js:8:39)
at Object.<anonymous> (C:\Users\Davide Passoni\Documents\DCSOlympus\client\bin\www:33:28)
at Module._compile (node:internal/modules/cjs/loader:1254:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)
at Module.load (node:internal/modules/cjs/loader:1117:32)
at Module._load (node:internal/modules/cjs/loader:958:12) {
code: 'MODULE_NOT_FOUND',
requireStack: [
'C:\\Users\\Davide Passoni\\Documents\\DCSOlympus\\client\\app.js',
'C:\\Users\\Davide Passoni\\Documents\\DCSOlympus\\client\\bin\\www'
]
}
Node.js v18.15.0
*********************************************************************
* _____ _____ _____ ____ _ *
* | __ \ / ____|/ ____| / __ \| | *
* | | | | | | (___ | | | | |_ _ _ __ ___ _ __ _ _ ___ *
* | | | | | \___ \ | | | | | | | | '_ ` _ \| '_ \| | | / __| *
* | |__| | |____ ____) | | |__| | | |_| | | | | | | |_) | |_| \__ \ *
* |_____/ \_____|_____/ \____/|_|\__, |_| |_| |_| .__/ \__,_|___/ *
* __/ | | | *
* |___/ |_| *
*********************************************************************

Please wait while DCS Olympus Server starts up...
Config location: C:\Users\Davide Passoni\Saved Games\DCS\Config\olympus.json
node:internal/modules/cjs/loader:1078
throw err;
^
Error: Cannot find module 'http-proxy-middleware'
Require stack:
- C:\Users\Davide Passoni\Documents\DCSOlympus\client\app.js
- C:\Users\Davide Passoni\Documents\DCSOlympus\client\bin\www
at Module._resolveFilename (node:internal/modules/cjs/loader:1075:15)
at Module._load (node:internal/modules/cjs/loader:920:27)
at Module.require (node:internal/modules/cjs/loader:1141:19)
at require (node:internal/modules/cjs/helpers:110:18)
at module.exports (C:\Users\Davide Passoni\Documents\DCSOlympus\client\app.js:8:39)
at Object.<anonymous> (C:\Users\Davide Passoni\Documents\DCSOlympus\client\bin\www:33:28)
at Module._compile (node:internal/modules/cjs/loader:1254:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)
at Module.load (node:internal/modules/cjs/loader:1117:32)
at Module._load (node:internal/modules/cjs/loader:958:12) {
code: 'MODULE_NOT_FOUND',
requireStack: [
'C:\\Users\\Davide Passoni\\Documents\\DCSOlympus\\client\\app.js',
'C:\\Users\\Davide Passoni\\Documents\\DCSOlympus\\client\\bin\\www'
]
}
Node.js v18.15.0
*********************************************************************
* _____ _____ _____ ____ _ *
* | __ \ / ____|/ ____| / __ \| | *
* | | | | | | (___ | | | | |_ _ _ __ ___ _ __ _ _ ___ *
* | | | | | \___ \ | | | | | | | | '_ ` _ \| '_ \| | | / __| *
* | |__| | |____ ____) | | |__| | | |_| | | | | | | |_) | |_| \__ \ *
* |_____/ \_____|_____/ \____/|_|\__, |_| |_| |_| .__/ \__,_|___/ *
* __/ | | | *
* |___/ |_| *
*********************************************************************

Please wait while DCS Olympus Server starts up...
Config location: C:\Users\Davide Passoni\Saved Games\DCS\Config\olympus.json
node:internal/modules/cjs/loader:1078
throw err;
^
Error: Cannot find module 'http-proxy-middleware'
Require stack:
- C:\Users\Davide Passoni\Documents\DCSOlympus\client\app.js
- C:\Users\Davide Passoni\Documents\DCSOlympus\client\bin\www
at Module._resolveFilename (node:internal/modules/cjs/loader:1075:15)
at Module._load (node:internal/modules/cjs/loader:920:27)
at Module.require (node:internal/modules/cjs/loader:1141:19)
at require (node:internal/modules/cjs/helpers:110:18)
at module.exports (C:\Users\Davide Passoni\Documents\DCSOlympus\client\app.js:8:39)
at Object.<anonymous> (C:\Users\Davide Passoni\Documents\DCSOlympus\client\bin\www:33:28)
at Module._compile (node:internal/modules/cjs/loader:1254:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)
at Module.load (node:internal/modules/cjs/loader:1117:32)
at Module._load (node:internal/modules/cjs/loader:958:12) {
code: 'MODULE_NOT_FOUND',
requireStack: [
'C:\\Users\\Davide Passoni\\Documents\\DCSOlympus\\client\\app.js',
'C:\\Users\\Davide Passoni\\Documents\\DCSOlympus\\client\\bin\\www'
]
}
Node.js v18.15.0
^CTerminare il processo batch (S/N)?

View File

@ -2,7 +2,7 @@
"name": "dcsolympus_manager",
"version": "1.0.0",
"description": "",
"main": "manager.js",
"main": "main.js",
"scripts": {
"start": "electron ."
},
@ -14,6 +14,7 @@
"dir-compare": "^4.2.0",
"ejs": "^3.1.9",
"electron": "^28.0.0",
"find-process": "^1.4.7",
"portfinder": "^1.0.32",
"regedit": "^5.1.2",
"sha256": "^0.2.0",

View File

@ -1,10 +1,12 @@
:root {
--background: #181e25;
--background-dark: #13181f;
--offwhite: #F2F2F2;
--blue: #247be2;
--red: #FF5858;
--green: #8bff63;
--gray: #cfd9e8;
--lightgray: #cfd9e8;
--gray: #989898;
--darkgray: #3d4651;
}
@ -13,10 +15,17 @@
box-sizing: border-box;
}
html {
height: 100%;
overflow: hidden;
}
body {
background-color: var(--background);
padding: 0px;
margin: 0px;
height: 100%;
overflow-x: auto;
}
#title-bar {
@ -30,6 +39,7 @@ body {
justify-content: end;
column-gap: 15px;
padding: 5px;
background-color: var(--background-dark);
}
#title-bar>*:first-child {
@ -75,8 +85,42 @@ body {
color: #F2F2F2;
font-weight: bold;
font-size: 16px;
padding: 20px 20px 0px 20px;
padding: 20px 20px 20px 20px;
column-gap: 10px;
background-color: var(--background-dark);
width: 100%;
-webkit-user-select: none;
-webkit-app-region: drag;
}
#header .link{
-webkit-user-select: text;
-webkit-app-region: no-drag;
}
#header>div {
display: flex;
flex-direction: column;
justify-content: center;
height: 60px;
}
.link {
font-weight: normal;
text-decoration: underline;
cursor: pointer;
}
.link.first {
margin-left: auto;
}
.link>a:link {
color: var(--offwhite);
}
.link>a:visited {
color: var(--offwhite);
}
.main-icon {
@ -84,10 +128,15 @@ body {
height: 60px;
}
body {
overflow: auto;
scrollbar-color: white transparent;
scrollbar-width: thin;
#loader {
color: var(--offwhite);
font-size: 20px;
font-weight: normal;
position: absolute;
display: flex;
width: 100%;
align-items: center;
justify-content: center;
}
::-webkit-scrollbar {
@ -117,11 +166,6 @@ body {
color: var(--green);
}
.manager-page {
height: 100%;
padding: 35px;
}
.page-header {
font-size: 18px;
font-weight: 600;
@ -135,21 +179,27 @@ body {
color: var(--offwhite);
display: flex;
flex-direction: column;
row-gap: 4px;
row-gap: 10px;
}
.instructions>span {
text-align: center;
}
.instructions>span:first-child {
font-size: 14px;
font-size: 22px;
font-weight: 600;
}
.instructions>span:not(:first-child) {
font-size: 13px;
font-size: 15px;
color: var(--gray);
}
.buttons-footer {
display: flex;
column-gap: 10px;
justify-content: center;
}
.button {
@ -160,19 +210,32 @@ body {
cursor: pointer;
}
.back {
.next {
color: var(--background);
background-color: var(--offwhite);
}
.next {
.uninstall {
color: var(--offwhite);
background-color: transparent;
border: 1px solid var(--offwhite);
}
.edit, .start-stop-server, .start-client {
color: var(--offwhite);
background-color: var(--blue);
}
.back {
color: var(--offwhite);
background-color: var(--background);
border: 1px solid var(--offwhite);
}
.cancel {
padding: 10px 5px;
color: var(--offwhite);
text-decoration: underline;
}
.close-popup {
@ -180,6 +243,11 @@ body {
background-color: var(--blue);
}
.accept-popup {
color: var(--background);
background-color: var(--offwhite);
}
input {
outline: none;
font-weight: 600;
@ -187,6 +255,8 @@ input {
font-size: 13px;
padding: 3px 10px;
border-radius: 5px;
text-align: center;
width: 300px;
}
.hide {
@ -201,11 +271,13 @@ input {
height: 100%;
background-color: black;
opacity: 30%;
z-index: 999;
}
#popup {
width: 300px;
height: 200px;
height: fit-content;
min-height: 200px;
position: absolute;
background-color: var(--background);
border-radius: 5px;
@ -215,12 +287,17 @@ input {
flex-direction: column;
justify-content: space-between;
padding: 20px;
align-items: center;
align-items: start;
z-index: 999;
}
#popup img {
width: 50px;
height: 50px;
width: 20px;
height: 20px;
}
#popup img.wait {
animation: rotate 2s linear infinite;
}
#popup .content {
@ -229,6 +306,7 @@ input {
font-weight: 600;
width: 100%;
text-align: left;
padding: 0px !important;
}
#popup .footer {
@ -236,4 +314,214 @@ input {
display: flex;
justify-content: end;
width: 100%;
}
column-gap: 10px;
}
.manager-page {
min-width: 1200px;
overflow-y: auto;
}
.manager-page>div {
display: flex;
flex-direction: row;
height: 100%;
min-height: 100%;
align-items: center;
}
.step-summary {
position: absolute;
display: flex;
flex-direction: column;
justify-content: center;
width: 30%;
font-size: 18px;
font-weight: 600;
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;
position: relative;
width: 500px;
}
.input-group>span:nth-child(1) {
font-size: 14px;
font-weight: 600;
}
.input-group>span:nth-child(2) {
font-size: 13px;
font-weight: normal;
}
.input-group div {
display: flex;
align-items: center;
column-gap: 5px;
flex-wrap: wrap;
}
.input-group span {
display: flex;
align-items: center;
column-gap: 5px;
flex-wrap: wrap;
}
.instructions {
margin-bottom: 10px;
}
.buttons-footer {
margin-top: 10px;
}
.instance-info {
display: flex;
flex-direction: column;
row-gap: 5px;
width: 100%;
}
.instance-info>span:nth-child(1) {
font-size: 18px;
font-weight: 600;
}
.instance-info>span:nth-child(2) {
font-size: 13px;
font-weight: 600;
color: var(--lightgray);
}
.instance-info>span:nth-child(2).installed {
font-weight: 600;
color: var(--green);
}
.instance-info>span:nth-child(2).error {
font-weight: 600;
color: orange;
}
.instance-info>span:nth-child(3) {
display: flex;
column-gap: 10px;
font-size: 13px;
font-weight: normal;
}
.instance-buttons {
display: flex;
flex-direction: row;
width: 100%;
justify-content: space-between;
}
.instance-info .info {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.instance-info .info>div:nth-child(1) {
font-weight: 600;
font-size: 14px;
}
.instance-info .info>div:nth-child(2) {
font-weight: normal;
font-size: 14px;
}
.divider {
border-top: 0px solid transparent !important;
border-bottom: 1px solid var(--offwhite) !important;
opacity: 80%;
height: 0px !important;
cursor: default;
}
.instance-info .divider {
margin-top: 15px;
margin-bottom: 15px;
}
@keyframes rotate {
0% {
transform: rotate(0deg)
}
100% {
transform: rotate(360deg)
}
}

Binary file not shown.