Delete src directory cuz u suck @everyone
This commit is contained in:
parent
42265ab184
commit
6c38ad997a
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 408 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 78 KiB |
|
|
@ -1,342 +0,0 @@
|
||||||
#vmview {
|
|
||||||
display: none;
|
|
||||||
padding-right: 0 !important;
|
|
||||||
padding-left: 0 !important;
|
|
||||||
}
|
|
||||||
/*.vmtile {
|
|
||||||
text-decoration: none;
|
|
||||||
color: #FFFFFF;
|
|
||||||
font-size: 16pt;
|
|
||||||
border: 2px solid #575757;
|
|
||||||
border-radius: 15px;
|
|
||||||
height: fit-content;
|
|
||||||
width: fit-content;
|
|
||||||
display: block;
|
|
||||||
padding: 4px;
|
|
||||||
}*/
|
|
||||||
#vmDisplay, #btns {
|
|
||||||
text-align: center;
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#vmlist > div.row > div {
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
#vmlist div.col-md-3 > div.card:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
border-color: rgb(8, 121, 250);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vmtile > img {
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-table, .username-table {
|
|
||||||
overflow-y: auto;
|
|
||||||
border: 1px solid #575757;
|
|
||||||
}
|
|
||||||
.chat-table {
|
|
||||||
height: 30vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.username-table {
|
|
||||||
max-height: 30vh;
|
|
||||||
}
|
|
||||||
.username-table > table > thead {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#turnstatus {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
#voteResetPanel {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.focused {
|
|
||||||
box-shadow: 0 0 9px 0 rgba(45,213,255,.75);
|
|
||||||
-moz-box-shadow: 0 0 9px 0 rgba(45,213,255,.75);
|
|
||||||
-webkit-box-shadow: 0 0 9px 0 rgba(45,213,255,.75)
|
|
||||||
}
|
|
||||||
|
|
||||||
.waiting {
|
|
||||||
box-shadow: 0 0 9px 0 rgba(242,255,63,.75);
|
|
||||||
-moz-box-shadow: 0 0 9px 0 rgba(242,255,63,.75);
|
|
||||||
-webkit-box-shadow: 0 0 9px 0 rgba(242,255,63,.75)
|
|
||||||
}
|
|
||||||
|
|
||||||
#staffbtns {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#staffbtns > button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qemuMonitorOutput {
|
|
||||||
height: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#xssCheckboxContainer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#forceVotePanel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.user-admin > td, .chat-username-admin, .username-admin {
|
|
||||||
color: #FF0000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.user-moderator > td, .chat-username-moderator, .username-moderator {
|
|
||||||
color: #00FF00 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
html[data-bs-theme="dark"] {
|
|
||||||
tr.user-unregistered > td, .chat-username-unregistered, .username-unregistered {
|
|
||||||
color: #b1b1b1 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.user-registered > td, .chat-username-registered, .username-registered {
|
|
||||||
color: #FFFFFF !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.user-registered.user-turn > td, tr.user-registered.user-waiting > td {
|
|
||||||
color: #000000 !important;
|
|
||||||
--bs-table-color: #000000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.user-unregistered.user-turn > td, tr.user-unregistered.user-waiting > td {
|
|
||||||
color: #585858 !important;
|
|
||||||
--bs-table-color: #585858 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html[data-bs-theme="light"] {
|
|
||||||
tr.user-unregistered > td, .chat-username-unregistered, .username-unregistered {
|
|
||||||
color: #6b6b6b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.user-registered > td, .chat-username-registered, .username-registered {
|
|
||||||
color: #000 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.user-turn > td {
|
|
||||||
background-color: #cfe2ff !important;
|
|
||||||
--bs-table-bg-state: #cfe2ff !important;
|
|
||||||
color: #000000;
|
|
||||||
--bs-table-color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.user-turn:hover, tr.user-turn > td:hover {
|
|
||||||
background-color: #bacbe6 !important;
|
|
||||||
--bs-table-bg-state: #bacbe6 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.user-waiting > td {
|
|
||||||
background-color: #fff3cd !important;
|
|
||||||
--bs-table-bg-state: #fff3cd !important;
|
|
||||||
color: #000000;
|
|
||||||
--bs-table-color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tr.user-waiting:hover, tr.user-waiting > td:hover {
|
|
||||||
background-color: #ece1be !important;
|
|
||||||
--bs-table-bg-state: #ece1be !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-current {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Start OSK */
|
|
||||||
.osk-container {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
max-width: 1024px;
|
|
||||||
margin: 0 auto;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simple-keyboard.hg-theme-default {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.osk-main.simple-keyboard {
|
|
||||||
max-width: 640px;
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.osk-main.simple-keyboard .hg-row:first-child {
|
|
||||||
margin-bottom: 8.51px; /* wtf? */
|
|
||||||
}
|
|
||||||
|
|
||||||
.osk-arrows.simple-keyboard {
|
|
||||||
align-self: flex-end;
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simple-keyboard .hg-button.selectedButton {
|
|
||||||
background: rgba(5, 25, 70, 0.53);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simple-keyboard .hg-button.emptySpace {
|
|
||||||
pointer-events: none;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.osk-arrows .hg-row {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.osk-arrows .hg-button {
|
|
||||||
width: 50px;
|
|
||||||
flex-grow: 0 !important;
|
|
||||||
justify-content: center !important;
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controlArrows {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-flow: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.osk-control.simple-keyboard {
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.osk-control.simple-keyboard .hg-row:first-child {
|
|
||||||
margin-bottom: 8.51px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.osk-control .hg-button {
|
|
||||||
width: 50px;
|
|
||||||
flex-grow: 0 !important;
|
|
||||||
justify-content: center !important;
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.numPad {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.osk-numpad.simple-keyboard {
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.osk-numpad.simple-keyboard {
|
|
||||||
width: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.osk-numpad.simple-keyboard .hg-button {
|
|
||||||
width: 50px;
|
|
||||||
justify-content: center;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.osk-numpadEnd.simple-keyboard {
|
|
||||||
width: 50px;
|
|
||||||
background: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 5px 5px 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.osk-numpadEnd.simple-keyboard .hg-button {
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.osk-numpadEnd .hg-button.hg-standardBtn.hg-button-plus {
|
|
||||||
height: 85px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.osk-numpadEnd.simple-keyboard .hg-button.hg-button-enter {
|
|
||||||
height: 85px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simple-keyboard.hg-theme-default .hg-button.hg-selectedButton {
|
|
||||||
background: rgba(5, 25, 70, 0.53);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hg-button.hg-functionBtn.hg-button-space {
|
|
||||||
width: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 640px) {
|
|
||||||
.hg-button:not(:last-child) {
|
|
||||||
margin-right: 1px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Theme: cvmDark
|
|
||||||
*/
|
|
||||||
|
|
||||||
.simple-keyboard.cvmDark .hg-button {
|
|
||||||
border-bottom: none;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simple-keyboard.cvmDark .hg-button:active {
|
|
||||||
background: #1c4995;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#root .simple-keyboard.cvmDark + .simple-keyboard-preview {
|
|
||||||
background: #1c4995;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Theme: cvmDisabled
|
|
||||||
*/
|
|
||||||
.simple-keyboard.cvmDisabled .hg-button {
|
|
||||||
border-bottom: none;
|
|
||||||
pointer-events: none;
|
|
||||||
background: gray;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* End OSK */
|
|
||||||
|
|
||||||
#badPasswordAlert {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* NSFW Blur */
|
|
||||||
.cvm-nsfw {
|
|
||||||
img {
|
|
||||||
filter:blur(40px)!important;
|
|
||||||
}
|
|
||||||
h5::before {
|
|
||||||
content: "[NSFW] ";
|
|
||||||
color: #ff0000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#accountDropdownMenuLink, #accountModalError, #accountModalSuccess {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Emoji font for systems without one */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Noto Color Emoji';
|
|
||||||
src: url('../assets/NotoColorEmoji.ttf');
|
|
||||||
}
|
|
||||||
|
|
@ -1,309 +0,0 @@
|
||||||
<!DOCTYPE HTML>
|
|
||||||
<html prefix="og: https://ogp.me/ns#" data-bs-theme="dark">
|
|
||||||
<head>
|
|
||||||
<title>CollabVM</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<link href="../css/style.css" rel="stylesheet" type="text/css"/>
|
|
||||||
<link href="../../node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" type="text/css"/>
|
|
||||||
<script src="https://kit.fontawesome.com/7add23c1ae.js" crossorigin="anonymous"></script>
|
|
||||||
<link rel="icon" href="../assets/favicon.ico">
|
|
||||||
<meta name="description" content="A website that lets you take turns controlling online virtual machines with complete strangers!"/>
|
|
||||||
<!-- Opengraph shit -->
|
|
||||||
<meta property="og:type" content="website"/>
|
|
||||||
<meta property="og:title" content="CollabVM"/>
|
|
||||||
<meta property="og:url" content="https://computernewb.com/collab-vm/"/>
|
|
||||||
<meta property="og:description" content="A website that lets you take turns controlling online virtual machines with complete strangers!"/>
|
|
||||||
<meta property="og:site_name" content="Computernewb"/>
|
|
||||||
<meta property="og:image" content="https://computernewb.com/collab-vm/desktop.png"/>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="modal fade" id="qemuMonitorModal" tabindex="-1" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 id="qemuModalHeader" class="modal-title"></h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<textarea id="qemuMonitorOutput" readonly="" class="form-control"></textarea>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" id="qemuMonitorInput" class="form-control" placeholder="Command"/>
|
|
||||||
<button class="btn btn-outline-secondary btn-primary" type="button" id="qemuMonitorSendBtn"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal fade" id="welcomeModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false" aria-labelledby="welcomeModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-lg">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h1 id="welcomeModalHeader"></h1>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body" id="welcomeModalBody"></div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" id="welcomeModalDismiss" class="btn btn-primary" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal fade" id="loginModal" tabindex="-1" aria-labelledby="loginModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-md">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Login</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="alert alert-danger alert-dismissible" id="badPasswordAlert" role="alert">
|
|
||||||
<span id="badPasswordAlertText"></span>
|
|
||||||
<button type="button" class="btn-close" aria-label="Close" id="incorrectPasswordDismissBtn"></button>
|
|
||||||
</div>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="hidden" name="username" id="adminInputVMID"/>
|
|
||||||
<span class="input-group-text" id="loginModalPasswordText"></span>
|
|
||||||
<input id="adminPassword" type="password" class="form-control" name="password"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" id="loginButton" class="btn btn-primary"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal fade" id="accountModal" tabindex="-1" aria-labelledby="accountModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-lg">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="accountModalTitle"></h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="alert alert-danger alert-dismissible" id="accountModalError" role="alert">
|
|
||||||
<span id="accountModalErrorText"></span>
|
|
||||||
<button type="button" class="btn-close" aria-label="Close" id="accountModalErrorDismiss"></button>
|
|
||||||
</div>
|
|
||||||
<div class="alert alert-success alert-dismissible" id="accountModalSuccess" role="alert">
|
|
||||||
<span id="accountModalSuccessText"></span>
|
|
||||||
<button type="button" class="btn-close" aria-label="Close" id="accountModalSuccessDismiss"></button>
|
|
||||||
</div>
|
|
||||||
<div id="accountLoginSection">
|
|
||||||
<form id="accountLoginForm">
|
|
||||||
<label for="accountLoginUsername" id="accountLoginUsernameLabel"></label><br/>
|
|
||||||
<input id="accountLoginUsername" type="text" class="form-control" name="username" required/><br>
|
|
||||||
<label for="accountLoginPassword" id="accountLoginPasswordLabel"></label><br/>
|
|
||||||
<input id="accountLoginPassword" type="password" class="form-control" name="password" required><br>
|
|
||||||
<div id="accountLoginCaptcha"></div>
|
|
||||||
<button type="submit" class="btn btn-primary" id="accountModalLoginBtn"></button> <button type="button" class="btn btn-secondary" id="accountForgotPasswordButton"></button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div id="accountRegisterSection">
|
|
||||||
<form id="accountRegisterForm">
|
|
||||||
<label for="accountRegisterEmail" id="accountRegisterEmailLabel"></label><br/>
|
|
||||||
<input id="accountRegisterEmail" type="email" class="form-control" name="email" required/><br>
|
|
||||||
<label for="accountRegisterUsername" id="accountRegisterUsernameLabel"></label><br/>
|
|
||||||
<input id="accountRegisterUsername" type="text" class="form-control" name="username" required/><br>
|
|
||||||
<label for="accountRegisterPassword" id="accountRegisterPasswordLabel"></label><br/>
|
|
||||||
<input id="accountRegisterPassword" type="password" class="form-control" name="password" required><br>
|
|
||||||
<label for="accountRegisterConfirmPassword" id="accountRegisterConfirmPasswordLabel"></label><br/>
|
|
||||||
<input id="accountRegisterConfirmPassword" type="password" class="form-control" name="confirmpassword" required><br>
|
|
||||||
<label for="accountRegisterDateOfBirth" id="accountRegisterDateOfBirthLabel"></label><br/>
|
|
||||||
<input id="accountRegisterDateOfBirth" type="date" class="form-control" name="dateofbirth" required><br/>
|
|
||||||
<div id="accountRegisterCaptcha"></div>
|
|
||||||
<button type="submit" class="btn btn-primary" id="accountModalRegisterBtn"></button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div id="accountVerifyEmailSection">
|
|
||||||
<center>
|
|
||||||
<i class="fa-solid fa-envelope" style="font-size: 12rem"></i>
|
|
||||||
<p id="accountVerifyEmailText"></p>
|
|
||||||
<form id="accountVerifyEmailForm">
|
|
||||||
<label for="accountVerifyEmailCode" id="accountVerifyEmailCodeLabel"></label><br>
|
|
||||||
<input id="accountVerifyEmailCode" type="text" class="form-control" name="code" required><br>
|
|
||||||
<label for="accountVerifyEmailPassword" id="accountVerifyEmailPasswordLabel"></label><br>
|
|
||||||
<input id="accountVerifyEmailPassword" type="password" class="form-control" name="password" required/><br/>
|
|
||||||
<button type="submit" class="btn btn-primary" id="accountModalVerifyEmailBtn"></button>
|
|
||||||
</form>
|
|
||||||
</center>
|
|
||||||
</div>
|
|
||||||
<div id="accountSettingsSection">
|
|
||||||
<form id="accountSettingsForm">
|
|
||||||
<label for="accountSettingsEmail" id="accountSettingsEmailLabel"></label><br>
|
|
||||||
<input id="accountSettingsEmail" type="email" class="form-control" name="email" required/><br/>
|
|
||||||
<label for="accountSettingsUsername" id="accountSettingsUsernameLabel"></label><br>
|
|
||||||
<input id="accountSettingsUsername" type="text" class="form-control" name="username" required/><br/>
|
|
||||||
<label for="accountSettingsNewPassword" id="accountSettingsNewPasswordLabel"></label>
|
|
||||||
<input id="accountSettingsNewPassword" type="password" class="form-control" name="password"/><br/>
|
|
||||||
<label for="accountSettingsConfirmNewPassword" id="accountSettingsConfirmNewPasswordLabel"></label>
|
|
||||||
<input id="accountSettingsConfirmNewPassword" type="password" class="form-control" name="confirmpassword"/><br/>
|
|
||||||
<label for="accountSettingsCurrentPassword" id="accountSettingsCurrentPasswordLabel"></label>
|
|
||||||
<input id="accountSettingsCurrentPassword" type="password" class="form-control" name="currentpassword" required/><br/>
|
|
||||||
<button type="submit" class="btn btn-primary" id="updateAccountSettingsBtn"></button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div id="accountResetPasswordSection">
|
|
||||||
<form id="accountResetPasswordForm">
|
|
||||||
<label for="accountResetPasswordEmail" id="accountResetPasswordEmailLabel"></label><br>
|
|
||||||
<input id="accountResetPasswordEmail" type="email" class="form-control" name="email" required/><br/>
|
|
||||||
<label for="accountResetPasswordUsername" id="accountResetPasswordUsernameLabel"></label>
|
|
||||||
<input id="accountResetPasswordUsername" type="text" class="form-control" name="username" required/><br/>
|
|
||||||
<div id="accountResetPasswordCaptcha"></div>
|
|
||||||
<button type="submit" class="btn btn-primary" id="accountResetPasswordBtn"></button>
|
|
||||||
</div>
|
|
||||||
<div id="accountResetPasswordVerifySection">
|
|
||||||
<center>
|
|
||||||
<i class="fa-solid fa-envelope" style="font-size: 12rem"></i>
|
|
||||||
<p id="accountVerifyPasswordResetText"></p>
|
|
||||||
<form id="accountResetPasswordVerifyForm">
|
|
||||||
<label for="accountResetPasswordCode" id="accountResetPasswordCodeLabel"></label><br>
|
|
||||||
<input id="accountResetPasswordCode" type="text" class="form-control" name="code" required><br>
|
|
||||||
<label for="accountResetPasswordNewPassword" id="accountResetPasswordNewPasswordLabel"></label><br>
|
|
||||||
<input id="accountResetPasswordNewPassword" type="password" class="form-control" name="password" required/><br/>
|
|
||||||
<label for="accountResetPasswordConfirmNewPassword" id="accountResetPasswordConfirmNewPasswordLabel"></label><br>
|
|
||||||
<input id="accountResetPasswordConfirmNewPassword" type="password" class="form-control" name="confirmpassword" required/><br/>
|
|
||||||
<button type="submit" class="btn btn-primary" id="accountResetPasswordVerifyBtn"></button>
|
|
||||||
</form>
|
|
||||||
</center>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<nav class="navbar navbar-expand-lg">
|
|
||||||
<div class="container-fluid">
|
|
||||||
<a class="navbar-brand" href="#"><span id="siteNameText"></span></a>
|
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
|
||||||
<ul class="navbar-nav me-auto">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a id="homeBtn" href="#" class="nav-link active" aria-current="page"><i class="fa-solid fa-house"></i> <span id="homeBtnText"></span></a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a href="https://computernewb.com/collab-vm/faq/" class="nav-link"><i class="fa-solid fa-circle-question"></i> <span id="faqBtnText"></span></a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a id="rulesBtn" href="https://computernewb.com/collab-vm/rules" class="nav-link"><i class="fa-solid fa-clipboard-check"></i> <span id="rulesBtnText"></span></a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a href="https://discord.gg/a4kqb4mGyX" class="nav-link"><i class="fa-brands fa-discord"></i> Discord</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a href="https://reddit.com/r/collabvm" class="nav-link"><i class="fa-brands fa-reddit"></i> Subreddit</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a rel="me" class="nav-link" href="https://fedi.computernewb.com/@collabvm"><i class="fa-brands fa-mastodon"></i> Mastodon</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a href="https://computernewb.com/collab-vm/user-vm" class="nav-link"><i class="fa-solid fa-user"></i> UserVM</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a id="toggleThemeBtn" href="#" class="nav-link"><i id="toggleThemeIcon" class="fa-solid fa-sun"></i> <span id="toggleThemeBtnText"></span></a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item dropdown">
|
|
||||||
<a id="languageDropdownLink" class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
||||||
<i class="fa-solid fa-globe"></i> <span id="languageDropdownText"></span>
|
|
||||||
</a>
|
|
||||||
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="languageDropdownLink">
|
|
||||||
<span id="languageDropdown"></span>
|
|
||||||
<a class="dropdown-item" href="https://github.com/computernewb/collab-vm-1.2-webapp/wiki/Contributing-a-Language" target="_blank"><i class="fa-solid fa-plus"></i> Add Yours</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div class="navbar-text dropdown">
|
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="accountDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
||||||
<i class="fa-solid fa-user"></i> <span id="accountDropdownUsername"></span>
|
|
||||||
</a>
|
|
||||||
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="accountDropdownMenuLink">
|
|
||||||
<a class="dropdown-item" href="#" id="accountLoginButton"></a>
|
|
||||||
<a class="dropdown-item" href="#" id="accountRegisterButton"></a>
|
|
||||||
<a class="dropdown-item" href="#" id="accountSettingsButton"></a>
|
|
||||||
<a class="dropdown-item" href="#" id="accountLogoutButton"></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<div class="container-fluid" id="vmlist">
|
|
||||||
<div class="row"></div>
|
|
||||||
</div>
|
|
||||||
<div id="vmview">
|
|
||||||
<div id="vmDisplay"></div>
|
|
||||||
<p id="turnstatus"></p>
|
|
||||||
<div id="voteResetPanel" style="display:none;">
|
|
||||||
<span id="voteResetHeaderText"></span><br/>
|
|
||||||
<button class="btn btn-success" id="voteYesBtn"><i class="fa-solid fa-check"></i> <span id="voteYesBtnText"></span><span class="badge bg-secondary" id="voteYesLabel"></span></button> <button class="btn btn-danger" id="voteNoBtn"><i class="fa-solid fa-ban"></i> <span id="voteNoBtnText"></span><span class="badge bg-secondary" id="voteNoLabel"></span></button><br/>
|
|
||||||
<span id="voteTimeText"></span>
|
|
||||||
<div id="forceVotePanel">
|
|
||||||
<button class="btn btn-info" id="forceVoteYesBtn"><i class="fa-solid fa-check"></i> <span id="passVoteBtnText"></span></button>
|
|
||||||
<button class="btn btn-info" id="forceVoteNoBtn"><i class="fa-solid fa-ban"></i> <span id="cancelVoteBtnText"></span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="btns">
|
|
||||||
<button class="btn btn-secondary" id="takeTurnBtn"><i class="fa-solid fa-computer-mouse"></i> <span id="turnBtnText"></span></button>
|
|
||||||
<button class="btn btn-secondary" id="oskBtn"><i class="fa-solid fa-keyboard"></i> <span id="oskBtnText"></span></button>
|
|
||||||
<button class="btn btn-secondary" id="changeUsernameBtn"><i class="fa-solid fa-signature"></i> <span id="changeUsernameBtnText"></span></button>
|
|
||||||
<button class="btn btn-secondary" id="voteResetButton"><i class="fa-solid fa-rotate-left"></i> <span id="voteForResetBtnText"></span></button>
|
|
||||||
<button class="btn btn-secondary" id="screenshotButton"><i class="fa-solid fa-camera"></i> <span id="screenshotBtnText"></span></button>
|
|
||||||
<button class="btn btn-secondary" id="ctrlAltDelBtn"><i class="fa-solid fa-gear"></i> <span id="ctrlAltDelBtnText"></span></button>
|
|
||||||
<div id="staffbtns">
|
|
||||||
<button class="btn btn-secondary" id="restoreBtn"><i class="fa-solid fa-rotate-left"></i> <span id="restoreBtnText"></span></button>
|
|
||||||
<button class="btn btn-secondary" id="rebootBtn"><i class="fa-solid fa-power-off"></i> <span id="rebootBtnText"></span></button>
|
|
||||||
<button class="btn btn-secondary" id="clearQueueBtn"><i class="fa-solid fa-eraser"></i> <span id="clearQueueBtnText"></span></button>
|
|
||||||
<button class="btn btn-secondary" id="bypassTurnBtn"><i class="fa-solid fa-forward"></i> <span id="bypassTurnBtnText"></span></button>
|
|
||||||
<button class="btn btn-secondary" id="endTurnBtn"><i class="fa-solid fa-ban"></i> <span id="endTurnBtnText"></span></button>
|
|
||||||
<button class="btn btn-secondary" id="indefTurnBtn"><i class="fa-solid fa-infinity"></i> <span id="indefTurnBtnText"></span></button>
|
|
||||||
<button class="btn btn-secondary" id="qemuMonitorBtn" data-bs-toggle="modal" data-bs-target="#qemuMonitorModal"><i class="fa-solid fa-terminal"></i> <span id="qemuMonitorBtnText"></span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="osk-container d-none" id="osk-container">
|
|
||||||
<div class="osk-main"></div>
|
|
||||||
|
|
||||||
<div class="controlArrows">
|
|
||||||
<div class="osk-control"></div>
|
|
||||||
<div class="osk-arrows"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="numPad">
|
|
||||||
<div class="osk-numpad"></div>
|
|
||||||
<div class="osk-numpadEnd"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row container-fluid">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="table-responsive username-table">
|
|
||||||
<table class="table table-hover table-borderless">
|
|
||||||
<thead>
|
|
||||||
<th class="bg-body-tertiary"><i class="fa-solid fa-user"></i> <span id="usersOnlineText"></span> (<span id="onlineusercount"></span>)</th>
|
|
||||||
</thead>
|
|
||||||
<tbody id="userlist"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-8">
|
|
||||||
<div class="table-responsive chat-table" id="chatListDiv">
|
|
||||||
<table class="table table-hover table-borderless">
|
|
||||||
<tbody id="chatList">
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-text username-unregistered" id="username"></span>
|
|
||||||
<input type="text" class="form-control" id="chat-input"/>
|
|
||||||
<div class="input-group-text" id="xssCheckboxContainer">
|
|
||||||
<input class="form-check-input" type="checkbox" value="" id="xssCheckbox"/>
|
|
||||||
<label class="form-check-label" for="xssCheckbox">XSS</label>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary" type="button" id="sendChatBtn"><i class="fa-solid fa-paper-plane"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="https://js.hcaptcha.com/1/api.js"></script>
|
|
||||||
<script type="module" src="../ts/main.ts" type="application/javascript"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,256 +0,0 @@
|
||||||
import * as dayjs from 'dayjs';
|
|
||||||
|
|
||||||
export default class AuthManager {
|
|
||||||
apiEndpoint : string;
|
|
||||||
info : AuthServerInformation | null;
|
|
||||||
account : Account | null;
|
|
||||||
constructor(apiEndpoint : string) {
|
|
||||||
this.apiEndpoint = apiEndpoint;
|
|
||||||
this.info = null;
|
|
||||||
this.account = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAPIInformation() : Promise<AuthServerInformation> {
|
|
||||||
return new Promise(async res => {
|
|
||||||
var data = await fetch(this.apiEndpoint + "/api/v1/info");
|
|
||||||
this.info = await data.json();
|
|
||||||
res(this.info!);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
login(username : string, password : string, captchaToken : string | undefined) : Promise<AccountLoginResult> {
|
|
||||||
return new Promise(async (res,rej) => {
|
|
||||||
if (!this.info) throw new Error("Cannot login before fetching API information.");
|
|
||||||
if (!captchaToken && this.info.hcaptcha.required) throw new Error("This API requires a valid hCaptcha token.");
|
|
||||||
var data = await fetch(this.apiEndpoint + "/api/v1/login", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
captchaToken: captchaToken
|
|
||||||
})
|
|
||||||
});
|
|
||||||
var json = await data.json() as AccountLoginResult;
|
|
||||||
if (!json) throw new Error("data.json() gave null or undefined result");
|
|
||||||
if (json.success && !json.verificationRequired) {
|
|
||||||
this.account = {
|
|
||||||
username: json.username!,
|
|
||||||
email: json.email!,
|
|
||||||
sessionToken: json.token!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res(json);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
loadSession(token : string) {
|
|
||||||
return new Promise<SessionResult>(async (res, rej) => {
|
|
||||||
var data = await fetch(this.apiEndpoint + "/api/v1/session", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
token: token,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
var json = await data.json() as SessionResult;
|
|
||||||
if (json.success) {
|
|
||||||
this.account = {
|
|
||||||
sessionToken: token,
|
|
||||||
username: json.username!,
|
|
||||||
email: json.email!,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
res(json);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
register(username : string, password : string, email : string, dateOfBirth : dayjs.Dayjs, captchaToken : string | undefined) : Promise<AccountRegisterResult> {
|
|
||||||
return new Promise(async (res, rej) => {
|
|
||||||
if (!this.info) throw new Error("Cannot login before fetching API information.");
|
|
||||||
if (!captchaToken && this.info.hcaptcha.required) throw new Error("This API requires a valid hCaptcha token.");
|
|
||||||
var data = await fetch(this.apiEndpoint + "/api/v1/register", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
email: email,
|
|
||||||
dateOfBirth: dateOfBirth.format("YYYY-MM-DD"),
|
|
||||||
captchatoken: captchaToken
|
|
||||||
})
|
|
||||||
});
|
|
||||||
res(await data.json() as AccountRegisterResult);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
logout() {
|
|
||||||
return new Promise<LogoutResult>(async res => {
|
|
||||||
if (!this.account) throw new Error("Cannot log out without logging in first");
|
|
||||||
var data = await fetch(this.apiEndpoint + "/api/v1/logout", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
token: this.account.sessionToken
|
|
||||||
})
|
|
||||||
});
|
|
||||||
var json = await data.json() as LogoutResult;
|
|
||||||
this.account = null;
|
|
||||||
res(json);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyEmail(username : string, password : string, code : string) {
|
|
||||||
return new Promise<VerifyEmailResult>(async res => {
|
|
||||||
var data = await fetch(this.apiEndpoint + "/api/v1/verify", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
code: code,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
res(await data.json() as VerifyEmailResult);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAccount(currentPassword : string, newEmail : string | undefined, newUsername : string | undefined, newPassword : string | undefined) {
|
|
||||||
return new Promise<UpdateAccountResult>(async res => {
|
|
||||||
if (!this.account) throw new Error("Cannot update account without being logged in.");
|
|
||||||
if (!newEmail && !newUsername && !newPassword) throw new Error("Cannot update account without any new information.");
|
|
||||||
var data = await fetch(this.apiEndpoint + "/api/v1/update", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
token: this.account!.sessionToken,
|
|
||||||
currentPassword: currentPassword,
|
|
||||||
newPassword: newPassword,
|
|
||||||
username: newUsername,
|
|
||||||
email: newEmail,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
var json = await data.json() as UpdateAccountResult;
|
|
||||||
if (json.success) {
|
|
||||||
if (this.account!.email !== newEmail) this.account!.email = newEmail!;
|
|
||||||
if (this.account!.username !== newUsername) this.account!.username = newUsername!;
|
|
||||||
if (json.sessionExpired || json.verificationRequired) {
|
|
||||||
this.account = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res(json);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sendPasswordResetEmail(username : string, email : string, captchaToken : string | undefined) {
|
|
||||||
return new Promise<PasswordResetResult>(async res => {
|
|
||||||
if (!this.info) throw new Error("Cannot send password reset email without fetching API information.");
|
|
||||||
if (!captchaToken && this.info.hcaptcha.required) throw new Error("This API requires a valid hCaptcha token.");
|
|
||||||
var data = await fetch(this.apiEndpoint + "/api/v1/sendreset", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: username,
|
|
||||||
email: email,
|
|
||||||
captchaToken: captchaToken
|
|
||||||
})
|
|
||||||
});
|
|
||||||
res(await data.json() as PasswordResetResult);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
resetPassword(username : string, email : string, code : string, newPassword : string) {
|
|
||||||
return new Promise<PasswordResetResult>(async res => {
|
|
||||||
var data = await fetch(this.apiEndpoint + "/api/v1/reset", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: username,
|
|
||||||
email: email,
|
|
||||||
code: code,
|
|
||||||
newPassword: newPassword
|
|
||||||
})
|
|
||||||
});
|
|
||||||
res(await data.json() as PasswordResetResult);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthServerInformation {
|
|
||||||
registrationOpen : boolean;
|
|
||||||
hcaptcha : {
|
|
||||||
required : boolean;
|
|
||||||
siteKey : string | undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AccountRegisterResult {
|
|
||||||
success : boolean;
|
|
||||||
error : string | undefined;
|
|
||||||
verificationRequired : boolean | undefined;
|
|
||||||
username : string | undefined;
|
|
||||||
email : string | undefined;
|
|
||||||
sessionToken : string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AccountLoginResult {
|
|
||||||
success : boolean;
|
|
||||||
token : string | undefined;
|
|
||||||
error : string | undefined;
|
|
||||||
verificationRequired : boolean | undefined;
|
|
||||||
email : string | undefined;
|
|
||||||
username : string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SessionResult {
|
|
||||||
success : boolean;
|
|
||||||
error : string | undefined;
|
|
||||||
banned : boolean;
|
|
||||||
username : string | undefined;
|
|
||||||
email : string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VerifyEmailResult {
|
|
||||||
success : boolean;
|
|
||||||
error : string | undefined;
|
|
||||||
sessionToken : string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LogoutResult {
|
|
||||||
success : boolean;
|
|
||||||
error : string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Account {
|
|
||||||
username : string;
|
|
||||||
email : string;
|
|
||||||
sessionToken : string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateAccountResult {
|
|
||||||
success : boolean;
|
|
||||||
error : string | undefined;
|
|
||||||
verificationRequired : boolean | undefined;
|
|
||||||
sessionExpired : boolean | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PasswordResetResult {
|
|
||||||
success : boolean;
|
|
||||||
error : string | undefined;
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
// TODO: `Object` has a toString(), but we should probably gate that off
|
|
||||||
/// Interface for things that can be turned into strings
|
|
||||||
export interface ToStringable {
|
|
||||||
toString(): string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A type for strings, or things that can (in a valid manner) be turned into strings
|
|
||||||
export type StringLike = string | ToStringable;
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
import { Language } from "./i18n.js";
|
|
||||||
|
|
||||||
const fallbackLanguage : Language = {
|
|
||||||
"languageName": "English (US)",
|
|
||||||
"translatedLanguageName": "English (US)",
|
|
||||||
"flag": "🇺🇸",
|
|
||||||
"author": "Computernewb",
|
|
||||||
|
|
||||||
"stringKeys": {
|
|
||||||
"kGeneric_CollabVM": "CollabVM",
|
|
||||||
"kGeneric_Yes": "Yes",
|
|
||||||
"kGeneric_No": "No",
|
|
||||||
"kGeneric_Ok": "OK",
|
|
||||||
"kGeneric_Cancel": "Cancel",
|
|
||||||
"kGeneric_Send": "Send",
|
|
||||||
"kGeneric_Understood": "Understood",
|
|
||||||
"kGeneric_Username": "Username",
|
|
||||||
"kGeneric_Password": "Password",
|
|
||||||
"kGeneric_Login": "Log in",
|
|
||||||
"kGeneric_Register": "Register",
|
|
||||||
"kGeneric_EMail": "E-Mail",
|
|
||||||
"kGeneric_DateOfBirth": "Date of Birth",
|
|
||||||
"kGeneric_VerificationCode": "Verification Code",
|
|
||||||
"kGeneric_Verify": "Verify",
|
|
||||||
"kGeneric_Update": "Update",
|
|
||||||
"kGeneric_Logout": "Log out",
|
|
||||||
|
|
||||||
"kWelcomeModal_Header": "Welcome to CollabVM",
|
|
||||||
"kWelcomeModal_Body": "<p>Before continuing, please familiarize yourself with our rules:</p> <h3>R1. Don't break the law.</h3> Do not use CollabVM or CollabVM's network to violate United States federal law, New York state law, or international law. If CollabVM becomes aware a crime has been committed through its service, you will be immediately banned, and your activities may be reported to the authorities if necessary.<br><br>CollabVM is required by law to notify law enforcement agencies if it becomes aware of the presence of child pornography on, or being transmitted through its network.<br><br>COPPA is also enforced, please do not use CollabVM if you are under the age of 13 years old. <h3>R2. No running DoS/DDoS tools.</h3> Do not use CollabVM to DoS/DDoS an indivdiual, business, company, or anyone else. <h3>R3. No spam distribution.</h3> Do not spam any emails using this service or push spam in general. <h3>R4. Do not abuse any exploits.</h3> Do not abuse any exploits, additionally if you see someone abusing exploits or you need to report one, please contact me at: computernewbab@gmail.com <h3>R5. Don't impersonate other users.</h3> Do not impersonate other members of CollabVM. If caught, you'll be temporarily disconnected, and banned if necessary. <h3>R6. One vote per person.</h3> Do not use any methods or tools to bypass the vote restriction. Only one vote per person is allowed, no matter what. Anybody who is caught doing this will be banned. <h3>R7. No Remote Administration Tools.</h3> Do not use any remote administration tools (ex: DarkComet, NanoCore, Anydesk, TeamViewer, Orcus, etc.) <h3>R8. No bypassing CollabNet.</h3> Do not attempt to bypass the blocking provided by CollabNet, especially if it is being used to break Rule 1, Rule 2, or Rule 7 (or run stupid over-used things). <h3>R9. No performing destructive actions constantly.</h3> Any user may not destroy the VM (rendering it unusable constantly), install/reinstall the operating system (except on VM7 or VM8), or run bots that do such. This includes bots that spam massive amounts of keyboard/mouse input (\"kitting\"). <h3>R10. No Cryptomining</h3> Attempting to mine cryptocurrency on the VMs will result in a kick, and then a permanent ban if you keep attempting. Besides, it's not like you're gonna make any money off it. <h3>NSFW Warning</h3> Please note that NSFW content is allowed on our anarchy VM (VM0b0t), and is viewed regularly. In addition, while we give a good effort to keep NSFW off the main VMs, people will occasionally slip it through.",
|
|
||||||
|
|
||||||
"kSiteButtons_Home": "Home",
|
|
||||||
"kSiteButtons_FAQ": "FAQ",
|
|
||||||
"kSiteButtons_Rules": "Rules",
|
|
||||||
"kSiteButtons_DarkMode": "Dark Mode",
|
|
||||||
"kSiteButtons_LightMode": "Light Mode",
|
|
||||||
|
|
||||||
"kVM_UsersOnlineText": "Users Online:",
|
|
||||||
|
|
||||||
"kVM_TurnTimeTimer": "Turn expires in {0} seconds.",
|
|
||||||
"kVM_WaitingTurnTimer": "Waiting for turn in {0} seconds.",
|
|
||||||
"kVM_VoteCooldownTimer": "Please wait {0} seconds before starting another vote.",
|
|
||||||
|
|
||||||
"kVM_VoteForResetTitle": "Do you want to reset the VM?",
|
|
||||||
"kVM_VoteForResetTimer": "Vote ends in {0} seconds",
|
|
||||||
|
|
||||||
"kVMButtons_TakeTurn": "Take Turn",
|
|
||||||
"kVMButtons_EndTurn": "End Turn",
|
|
||||||
"kVMButtons_ChangeUsername": "Change Username",
|
|
||||||
"kVMButtons_Keyboard": "Keyboard",
|
|
||||||
"KVMButtons_CtrlAltDel": "Ctrl+Alt+Del",
|
|
||||||
|
|
||||||
"kVMButtons_VoteForReset": "Vote For Reset",
|
|
||||||
"kVMButtons_Screenshot": "Screenshot",
|
|
||||||
|
|
||||||
"kQEMUMonitor": "QEMU Monitor",
|
|
||||||
"kAdminVMButtons_PassVote": "Pass Vote",
|
|
||||||
"kAdminVMButtons_CancelVote": "Cancel Vote",
|
|
||||||
|
|
||||||
"kAdminVMButtons_Restore": "Restore",
|
|
||||||
"kAdminVMButtons_Reboot": "Reboot",
|
|
||||||
"kAdminVMButtons_ClearTurnQueue": "Clear Turn Queue",
|
|
||||||
"kAdminVMButtons_BypassTurn": "Bypass Turn",
|
|
||||||
"kAdminVMButtons_IndefiniteTurn": "Indefinite Turn",
|
|
||||||
|
|
||||||
"kAdminVMButtons_Ban": "Ban",
|
|
||||||
"kAdminVMButtons_Kick": "Kick",
|
|
||||||
"kAdminVMButtons_TempMute": "Temporary Mute",
|
|
||||||
"kAdminVMButtons_IndefMute": "Indefinite Mute",
|
|
||||||
"kAdminVMButtons_Unmute": "Unmute",
|
|
||||||
"kAdminVMButtons_GetIP": "Get IP",
|
|
||||||
|
|
||||||
"kVMPrompts_AdminChangeUsernamePrompt": "Enter new username for {0}:",
|
|
||||||
"kVMPrompts_AdminRestoreVMPrompt": "Are you sure you want to restore the VM?",
|
|
||||||
"kVMPrompts_EnterNewUsernamePrompt": "Enter a new username, or leave the field blank to be assigned a guest username",
|
|
||||||
|
|
||||||
"kError_UnexpectedDisconnection": "You have been disconnected from the server.",
|
|
||||||
"kError_UsernameTaken": "That username is already taken",
|
|
||||||
"kError_UsernameInvalid": "Usernames can contain only numbers, letters, spaces, dashes, underscores, and dots, and it must be between 3 and 20 characters.",
|
|
||||||
"kError_UsernameBlacklisted": "That username has been blacklisted.",
|
|
||||||
"kError_IncorrectPassword": "Incorrect password.",
|
|
||||||
|
|
||||||
"kAccountModal_Verify": "Verify E-Mail",
|
|
||||||
"kAccountModal_AccountSettings": "Account Settings",
|
|
||||||
"kAccountModal_ResetPassword": "Reset Password",
|
|
||||||
|
|
||||||
"kAccountModal_NewPassword": "New Password",
|
|
||||||
"kAccountModal_ConfirmNewPassword": "Confirm New Password",
|
|
||||||
"kAccountModal_CurrentPassword": "Current Password",
|
|
||||||
"kAccountModal_ConfirmPassword": "Confirm Password",
|
|
||||||
|
|
||||||
"kMissingCaptcha": "Please fill out the captcha.",
|
|
||||||
"kPasswordsMustMatch": "Passwords must match.",
|
|
||||||
"kAccountModal_VerifyText": "We sent an E-Mail to {0}. To verify your account, please enter the 8-digit code from the E-Mail below.",
|
|
||||||
"kAccountModal_VerifyPasswordResetText": "We sent an E-Mail to {0}. To reset your password, please enter the 8-digit code from the E-Mail below.",
|
|
||||||
"kAccountModal_PasswordResetSuccess": "Your password has been changed successfully. You can now log in with your new password.",
|
|
||||||
|
|
||||||
"kNotLoggedIn": "Not Logged in"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default fallbackLanguage;
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
import { StringLike } from './StringLike';
|
|
||||||
|
|
||||||
function isalpha(char: number) {
|
|
||||||
return RegExp(/^\p{L}/, 'u').test(String.fromCharCode(char));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A simple function for formatting strings in a more expressive manner.
|
|
||||||
/// While JavaScript *does* have string interpolation, it's not a total replacement
|
|
||||||
/// for just formatting strings, and a method like this is better for data independent formatting.
|
|
||||||
///
|
|
||||||
/// ## Example usage
|
|
||||||
///
|
|
||||||
/// ```typescript
|
|
||||||
/// let hello = Format("Hello, {0}!", "World");
|
|
||||||
/// ```
|
|
||||||
export function Format(pattern: string, ...args: Array<StringLike>) {
|
|
||||||
let argumentsAsStrings: Array<string> = [...args].map((el) => {
|
|
||||||
// This catches cases where the thing already is a string
|
|
||||||
if (typeof el == 'string') return el as string;
|
|
||||||
return el.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
let pat = pattern;
|
|
||||||
|
|
||||||
// Handle pattern ("{0} {1} {2} {3} {4} {5}") syntax if found
|
|
||||||
for (let i = 0; i < pat.length; ++i) {
|
|
||||||
if (pat[i] == '{') {
|
|
||||||
let replacementStart = i;
|
|
||||||
let foundSpecifierEnd = false;
|
|
||||||
|
|
||||||
// Make sure the specifier is not cut off (the last character of the string)
|
|
||||||
if (i + 3 > pat.length) {
|
|
||||||
throw new Error(`Error in format pattern "${pat}": Cutoff/invalid format specifier`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try and find the specifier end ('}').
|
|
||||||
// Whitespace and a '{' are considered errors.
|
|
||||||
for (let j = i + 1; j < pat.length; ++j) {
|
|
||||||
switch (pat[j]) {
|
|
||||||
case '}':
|
|
||||||
foundSpecifierEnd = true;
|
|
||||||
i = j;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case '{':
|
|
||||||
throw new Error(`Error in format pattern "${pat}": Cannot start a format specifier in an existing replacement`);
|
|
||||||
case ' ':
|
|
||||||
throw new Error(`Error in format pattern "${pat}": Whitespace inside format specifier`);
|
|
||||||
|
|
||||||
case '-':
|
|
||||||
throw new Error(`Error in format pattern "${pat}": Malformed format specifier`);
|
|
||||||
|
|
||||||
default:
|
|
||||||
if (isalpha(pat.charCodeAt(j))) throw new Error(`Error in format pattern "${pat}": Malformed format specifier`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundSpecifierEnd) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundSpecifierEnd) throw new Error(`Error in format pattern "${pat}": No terminating "}" character found`);
|
|
||||||
|
|
||||||
// Get the beginning and trailer
|
|
||||||
let beginning = pat.substring(0, replacementStart);
|
|
||||||
let trailer = pat.substring(replacementStart + 3);
|
|
||||||
|
|
||||||
let argumentIndex = parseInt(pat.substring(replacementStart + 1, i));
|
|
||||||
if (Number.isNaN(argumentIndex) || argumentIndex > argumentsAsStrings.length) throw new Error(`Error in format pattern "${pat}": Argument index out of bounds`);
|
|
||||||
|
|
||||||
// This is seriously the only decent way to do this in javascript
|
|
||||||
// thanks brendan eich (replace this thanking with more choice words in your head)
|
|
||||||
pat = beginning + argumentsAsStrings[argumentIndex] + trailer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pat;
|
|
||||||
}
|
|
||||||
442
src/ts/i18n.ts
442
src/ts/i18n.ts
|
|
@ -1,442 +0,0 @@
|
||||||
import { StringLike } from './StringLike';
|
|
||||||
import { Format } from './format';
|
|
||||||
import { Emitter, Unsubscribe, createNanoEvents } from 'nanoevents';
|
|
||||||
import Config from '../../config.json';
|
|
||||||
|
|
||||||
/// All string keys.
|
|
||||||
export enum I18nStringKey {
|
|
||||||
// Generic things
|
|
||||||
kGeneric_CollabVM = 'kGeneric_CollabVM',
|
|
||||||
kGeneric_Yes = 'kGeneric_Yes',
|
|
||||||
kGeneric_No = 'kGeneric_No',
|
|
||||||
kGeneric_Ok = 'kGeneric_Ok',
|
|
||||||
kGeneric_Cancel = 'kGeneric_Cancel',
|
|
||||||
kGeneric_Send = 'kGeneric_Send',
|
|
||||||
kGeneric_Understood = 'kGeneric_Understood',
|
|
||||||
kGeneric_Username = 'kGeneric_Username',
|
|
||||||
kGeneric_Password = 'kGeneric_Password',
|
|
||||||
kGeneric_Login = 'kGeneric_Login',
|
|
||||||
kGeneric_Register = 'kGeneric_Register',
|
|
||||||
kGeneric_EMail = 'kGeneric_EMail',
|
|
||||||
kGeneric_DateOfBirth = 'kGeneric_DateOfBirth',
|
|
||||||
kGeneric_VerificationCode = 'kGeneric_VerificationCode',
|
|
||||||
kGeneric_Verify = 'kGeneric_Verify',
|
|
||||||
kGeneric_Update = 'kGeneric_Update',
|
|
||||||
kGeneric_Logout = 'kGeneric_Logout',
|
|
||||||
|
|
||||||
kWelcomeModal_Header = 'kWelcomeModal_Header',
|
|
||||||
kWelcomeModal_Body = 'kWelcomeModal_Body',
|
|
||||||
|
|
||||||
kSiteButtons_Home = 'kSiteButtons_Home',
|
|
||||||
kSiteButtons_FAQ = 'kSiteButtons_FAQ',
|
|
||||||
kSiteButtons_Rules = 'kSiteButtons_Rules',
|
|
||||||
kSiteButtons_DarkMode = 'kSiteButtons_DarkMode',
|
|
||||||
kSiteButtons_LightMode = 'kSiteButtons_LightMode',
|
|
||||||
kSiteButtons_Languages = 'kSiteButtons_Languages',
|
|
||||||
|
|
||||||
kVM_UsersOnlineText = 'kVM_UsersOnlineText',
|
|
||||||
|
|
||||||
kVM_TurnTimeTimer = 'kVM_TurnTimeTimer',
|
|
||||||
kVM_WaitingTurnTimer = 'kVM_WaitingTurnTimer',
|
|
||||||
kVM_VoteCooldownTimer = 'kVM_VoteCooldownTimer',
|
|
||||||
|
|
||||||
kVM_VoteForResetTitle = 'kVM_VoteForResetTitle',
|
|
||||||
kVM_VoteForResetTimer = 'kVM_VoteForResetTimer',
|
|
||||||
|
|
||||||
kVMButtons_TakeTurn = 'kVMButtons_TakeTurn',
|
|
||||||
kVMButtons_EndTurn = 'kVMButtons_EndTurn',
|
|
||||||
kVMButtons_ChangeUsername = 'kVMButtons_ChangeUsername',
|
|
||||||
kVMButtons_Keyboard = 'kVMButtons_Keyboard',
|
|
||||||
KVMButtons_CtrlAltDel = 'KVMButtons_CtrlAltDel',
|
|
||||||
|
|
||||||
kVMButtons_VoteForReset = 'kVMButtons_VoteForReset',
|
|
||||||
kVMButtons_Screenshot = 'kVMButtons_Screenshot',
|
|
||||||
|
|
||||||
// Admin VM buttons
|
|
||||||
kQEMUMonitor = 'kQEMUMonitor',
|
|
||||||
kAdminVMButtons_PassVote = 'kAdminVMButtons_PassVote',
|
|
||||||
kAdminVMButtons_CancelVote = 'kAdminVMButtons_CancelVote',
|
|
||||||
|
|
||||||
kAdminVMButtons_Restore = 'kAdminVMButtons_Restore',
|
|
||||||
kAdminVMButtons_Reboot = 'kAdminVMButtons_Reboot',
|
|
||||||
kAdminVMButtons_ClearTurnQueue = 'kAdminVMButtons_ClearTurnQueue',
|
|
||||||
kAdminVMButtons_BypassTurn = 'kAdminVMButtons_BypassTurn',
|
|
||||||
kAdminVMButtons_IndefiniteTurn = 'kAdminVMButtons_IndefiniteTurn',
|
|
||||||
|
|
||||||
kAdminVMButtons_Ban = 'kAdminVMButtons_Ban',
|
|
||||||
kAdminVMButtons_Kick = 'kAdminVMButtons_Kick',
|
|
||||||
kAdminVMButtons_TempMute = 'kAdminVMButtons_TempMute',
|
|
||||||
kAdminVMButtons_IndefMute = 'kAdminVMButtons_IndefMute',
|
|
||||||
kAdminVMButtons_Unmute = 'kAdminVMButtons_Unmute',
|
|
||||||
kAdminVMButtons_GetIP = 'kAdminVMButtons_GetIP',
|
|
||||||
|
|
||||||
// prompts
|
|
||||||
kVMPrompts_AdminChangeUsernamePrompt = 'kVMPrompts_AdminChangeUsernamePrompt',
|
|
||||||
kVMPrompts_AdminRestoreVMPrompt = 'kVMPrompts_AdminRestoreVMPrompt',
|
|
||||||
kVMPrompts_EnterNewUsernamePrompt = 'kVMPrompts_EnterNewUsernamePrompt',
|
|
||||||
|
|
||||||
// error messages
|
|
||||||
kError_UnexpectedDisconnection = 'kError_UnexpectedDisconnection',
|
|
||||||
|
|
||||||
kError_UsernameTaken = 'kError_UsernameTaken',
|
|
||||||
kError_UsernameInvalid = 'kError_UsernameInvalid',
|
|
||||||
kError_UsernameBlacklisted = 'kError_UsernameBlacklisted',
|
|
||||||
kError_IncorrectPassword = 'kError_IncorrectPassword',
|
|
||||||
|
|
||||||
// Auth
|
|
||||||
kAccountModal_Verify = 'kAccountModal_Verify',
|
|
||||||
kAccountModal_AccountSettings = 'kAccountModal_AccountSettings',
|
|
||||||
kAccountModal_ResetPassword = 'kAccountModal_ResetPassword',
|
|
||||||
|
|
||||||
kAccountModal_NewPassword = 'kAccountModal_NewPassword',
|
|
||||||
kAccountModal_ConfirmNewPassword = 'kAccountModal_ConfirmNewPassword',
|
|
||||||
kAccountModal_CurrentPassword = 'kAccountModal_CurrentPassword',
|
|
||||||
kAccountModal_ConfirmPassword = 'kAccountModal_ConfirmPassword',
|
|
||||||
|
|
||||||
kAccountModal_VerifyText = 'kAccountModal_VerifyText',
|
|
||||||
kAccountModal_VerifyPasswordResetText = 'kAccountModal_VerifyPasswordResetText',
|
|
||||||
kAccountModal_PasswordResetSuccess = 'kAccountModal_PasswordResetSuccess',
|
|
||||||
kMissingCaptcha = 'kMissingCaptcha',
|
|
||||||
kPasswordsMustMatch = 'kPasswordsMustMatch',
|
|
||||||
|
|
||||||
kNotLoggedIn = 'kNotLoggedIn',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface I18nEvents {
|
|
||||||
// Called when the language is changed
|
|
||||||
languageChanged: (lang: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This models the JSON structure.
|
|
||||||
export type Language = {
|
|
||||||
languageName: string;
|
|
||||||
translatedLanguageName: string;
|
|
||||||
flag: string; // country flag, can be blank if not applicable. will be displayed in language dropdown
|
|
||||||
author: string;
|
|
||||||
|
|
||||||
stringKeys: {
|
|
||||||
// This is fancy typescript speak for
|
|
||||||
// "any string index returns a string",
|
|
||||||
// which is our expectation.
|
|
||||||
// See https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures if this is confusing.
|
|
||||||
[key: string]: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// `languages.json`
|
|
||||||
export type LanguagesJson = {
|
|
||||||
// Array of language IDs to allow loading
|
|
||||||
languages: Array<string>;
|
|
||||||
|
|
||||||
// The default language (set if a invalid language not in the languages array is set, or no language is set)
|
|
||||||
defaultLanguage: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ID for fallback language
|
|
||||||
const fallbackId = '!!fallback';
|
|
||||||
|
|
||||||
// This language is provided in the webapp itself just in case language stuff fails
|
|
||||||
import fallbackLanguage from './fallbackLanguage.js';
|
|
||||||
|
|
||||||
interface StringKeyMap {
|
|
||||||
[k: string]: I18nStringKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// our fancy internationalization helper.
|
|
||||||
export class I18n {
|
|
||||||
// The language data itself
|
|
||||||
private langs : Map<string, Language> = new Map<string, Language>();
|
|
||||||
private lang: Language = fallbackLanguage;
|
|
||||||
private languageDropdown: HTMLSpanElement = document.getElementById('languageDropdown') as HTMLSpanElement;
|
|
||||||
private emitter: Emitter<I18nEvents> = createNanoEvents();
|
|
||||||
|
|
||||||
CurrentLanguage = () => this.langId;
|
|
||||||
|
|
||||||
// the ID of the language
|
|
||||||
private langId: string = fallbackId;
|
|
||||||
|
|
||||||
async Init() {
|
|
||||||
// Load language list
|
|
||||||
var res = await fetch("lang/languages.json");
|
|
||||||
if (!res.ok) {
|
|
||||||
alert("Failed to load languages.json: " + res.statusText);
|
|
||||||
this.SetLanguage(fallbackLanguage, fallbackId);
|
|
||||||
this.ReplaceStaticStrings();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var langData = await res.json() as LanguagesJson;
|
|
||||||
for (const langId of langData.languages) {
|
|
||||||
let path = `./lang/${langId}.json`;
|
|
||||||
let res = await fetch(path);
|
|
||||||
if (!res.ok) {
|
|
||||||
console.error(`Failed to load lang/${langId}.json: ${res.statusText}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let _lang = await res.json() as Language;
|
|
||||||
this.langs.set(langId, _lang);
|
|
||||||
}
|
|
||||||
this.langs.forEach((_lang, langId) => {
|
|
||||||
// Add to language dropdown
|
|
||||||
var a = document.createElement('a');
|
|
||||||
a.classList.add('dropdown-item');
|
|
||||||
a.href = '#';
|
|
||||||
a.innerText = `${_lang.flag} ${_lang.languageName}`;
|
|
||||||
a.addEventListener('click', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.SetLanguage(_lang, langId);
|
|
||||||
this.ReplaceStaticStrings();
|
|
||||||
});
|
|
||||||
this.languageDropdown.appendChild(a);
|
|
||||||
});
|
|
||||||
let lang = null;
|
|
||||||
let lsLang = window.localStorage.getItem('i18n-lang');
|
|
||||||
var browserLang = navigator.language.toLowerCase();
|
|
||||||
// If the language is set in localstorage, use that
|
|
||||||
if (lsLang !== null && this.langs.has(lsLang)) lang = lsLang;
|
|
||||||
// If the browser language is in the list, use that
|
|
||||||
else if (this.langs.has(browserLang)) lang = browserLang;
|
|
||||||
else {
|
|
||||||
// If the exact browser language isn't in the list, try to find a language with the same prefix
|
|
||||||
for (let langId of langData.languages) {
|
|
||||||
if (langId.split('-')[0] === browserLang.split('-')[0]) {
|
|
||||||
lang = langId;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If all else fails, use the default language
|
|
||||||
if (lang === null) lang = langData.defaultLanguage;
|
|
||||||
this.SetLanguage(this.langs.get(lang) as Language, lang);
|
|
||||||
this.ReplaceStaticStrings();
|
|
||||||
}
|
|
||||||
|
|
||||||
private SetLanguage(lang: Language, id: string) {
|
|
||||||
let lastId = this.langId;
|
|
||||||
this.langId = id;
|
|
||||||
this.lang = lang;
|
|
||||||
|
|
||||||
// Only replace static strings
|
|
||||||
if (this.langId != lastId) this.ReplaceStaticStrings();
|
|
||||||
|
|
||||||
// Set the language ID localstorage entry
|
|
||||||
if (this.langId !== fallbackId) {
|
|
||||||
window.localStorage.setItem('i18n-lang', this.langId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emitter.emit('languageChanged', this.langId);
|
|
||||||
console.log('i18n initalized for', id, 'sucessfully!');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replaces static strings that we don't recompute
|
|
||||||
private ReplaceStaticStrings() {
|
|
||||||
const kDomIdtoStringMap: StringKeyMap = {
|
|
||||||
siteNameText: I18nStringKey.kGeneric_CollabVM,
|
|
||||||
homeBtnText: I18nStringKey.kSiteButtons_Home,
|
|
||||||
faqBtnText: I18nStringKey.kSiteButtons_FAQ,
|
|
||||||
rulesBtnText: I18nStringKey.kSiteButtons_Rules,
|
|
||||||
accountLoginButton: I18nStringKey.kGeneric_Login,
|
|
||||||
accountRegisterButton: I18nStringKey.kGeneric_Register,
|
|
||||||
accountSettingsButton: I18nStringKey.kAccountModal_AccountSettings,
|
|
||||||
accountLogoutButton: I18nStringKey.kGeneric_Logout,
|
|
||||||
languageDropdownText: I18nStringKey.kSiteButtons_Languages,
|
|
||||||
|
|
||||||
welcomeModalHeader: I18nStringKey.kWelcomeModal_Header,
|
|
||||||
welcomeModalBody: I18nStringKey.kWelcomeModal_Body,
|
|
||||||
welcomeModalDismiss: I18nStringKey.kGeneric_Understood,
|
|
||||||
|
|
||||||
usersOnlineText: I18nStringKey.kVM_UsersOnlineText,
|
|
||||||
|
|
||||||
voteResetHeaderText: I18nStringKey.kVM_VoteForResetTitle,
|
|
||||||
voteYesBtnText: I18nStringKey.kGeneric_Yes,
|
|
||||||
voteNoBtnText: I18nStringKey.kGeneric_No,
|
|
||||||
|
|
||||||
changeUsernameBtnText: I18nStringKey.kVMButtons_ChangeUsername,
|
|
||||||
oskBtnText: I18nStringKey.kVMButtons_Keyboard,
|
|
||||||
ctrlAltDelBtnText: I18nStringKey.KVMButtons_CtrlAltDel,
|
|
||||||
voteForResetBtnText: I18nStringKey.kVMButtons_VoteForReset,
|
|
||||||
screenshotBtnText: I18nStringKey.kVMButtons_Screenshot,
|
|
||||||
|
|
||||||
// admin stuff
|
|
||||||
badPasswordAlertText: I18nStringKey.kError_IncorrectPassword,
|
|
||||||
loginModalPasswordText: I18nStringKey.kGeneric_Password,
|
|
||||||
loginButton: I18nStringKey.kGeneric_Login,
|
|
||||||
passVoteBtnText: I18nStringKey.kAdminVMButtons_PassVote,
|
|
||||||
cancelVoteBtnText: I18nStringKey.kAdminVMButtons_CancelVote,
|
|
||||||
endTurnBtnText: I18nStringKey.kVMButtons_EndTurn,
|
|
||||||
qemuMonitorBtnText: I18nStringKey.kQEMUMonitor,
|
|
||||||
qemuModalHeader: I18nStringKey.kQEMUMonitor,
|
|
||||||
qemuMonitorSendBtn: I18nStringKey.kGeneric_Send,
|
|
||||||
|
|
||||||
restoreBtnText: I18nStringKey.kAdminVMButtons_Restore,
|
|
||||||
rebootBtnText: I18nStringKey.kAdminVMButtons_Reboot,
|
|
||||||
clearQueueBtnText: I18nStringKey.kAdminVMButtons_ClearTurnQueue,
|
|
||||||
bypassTurnBtnText: I18nStringKey.kAdminVMButtons_BypassTurn,
|
|
||||||
indefTurnBtnText: I18nStringKey.kAdminVMButtons_IndefiniteTurn,
|
|
||||||
|
|
||||||
// Account modal
|
|
||||||
accountLoginUsernameLabel: I18nStringKey.kGeneric_Username,
|
|
||||||
accountLoginPasswordLabel: I18nStringKey.kGeneric_Password,
|
|
||||||
accountModalLoginBtn: I18nStringKey.kGeneric_Login,
|
|
||||||
accountForgotPasswordButton: I18nStringKey.kAccountModal_ResetPassword,
|
|
||||||
accountRegisterEmailLabel: I18nStringKey.kGeneric_EMail,
|
|
||||||
accountRegisterUsernameLabel: I18nStringKey.kGeneric_Username,
|
|
||||||
accountRegisterPasswordLabel: I18nStringKey.kGeneric_Password,
|
|
||||||
accountRegisterConfirmPasswordLabel: I18nStringKey.kAccountModal_ConfirmPassword,
|
|
||||||
accountRegisterDateOfBirthLabel: I18nStringKey.kGeneric_DateOfBirth,
|
|
||||||
accountModalRegisterBtn: I18nStringKey.kGeneric_Register,
|
|
||||||
accountVerifyEmailCodeLabel: I18nStringKey.kGeneric_VerificationCode,
|
|
||||||
accountVerifyEmailPasswordLabel: I18nStringKey.kGeneric_Password,
|
|
||||||
accountModalVerifyEmailBtn: I18nStringKey.kGeneric_Verify,
|
|
||||||
accountSettingsEmailLabel: I18nStringKey.kGeneric_EMail,
|
|
||||||
accountSettingsUsernameLabel: I18nStringKey.kGeneric_Username,
|
|
||||||
accountSettingsNewPasswordLabel: I18nStringKey.kAccountModal_NewPassword,
|
|
||||||
accountSettingsConfirmNewPasswordLabel: I18nStringKey.kAccountModal_ConfirmNewPassword,
|
|
||||||
accountSettingsCurrentPasswordLabel: I18nStringKey.kAccountModal_CurrentPassword,
|
|
||||||
updateAccountSettingsBtn: I18nStringKey.kGeneric_Update,
|
|
||||||
accountResetPasswordEmailLabel: I18nStringKey.kGeneric_EMail,
|
|
||||||
accountResetPasswordUsernameLabel: I18nStringKey.kGeneric_Username,
|
|
||||||
accountResetPasswordBtn: I18nStringKey.kAccountModal_ResetPassword,
|
|
||||||
accountResetPasswordCodeLabel: I18nStringKey.kGeneric_VerificationCode,
|
|
||||||
accountResetPasswordNewPasswordLabel: I18nStringKey.kAccountModal_NewPassword,
|
|
||||||
accountResetPasswordConfirmNewPasswordLabel: I18nStringKey.kAccountModal_ConfirmNewPassword,
|
|
||||||
accountResetPasswordVerifyBtn: I18nStringKey.kAccountModal_ResetPassword,
|
|
||||||
};
|
|
||||||
|
|
||||||
const kDomAttributeToStringMap = {
|
|
||||||
adminPassword: {
|
|
||||||
placeholder: I18nStringKey.kGeneric_Password,
|
|
||||||
},
|
|
||||||
accountLoginUsername: {
|
|
||||||
placeholder: I18nStringKey.kGeneric_Username,
|
|
||||||
},
|
|
||||||
accountLoginPassword: {
|
|
||||||
placeholder: I18nStringKey.kGeneric_Password,
|
|
||||||
},
|
|
||||||
accountRegisterEmail: {
|
|
||||||
placeholder: I18nStringKey.kGeneric_EMail,
|
|
||||||
},
|
|
||||||
accountRegisterUsername: {
|
|
||||||
placeholder: I18nStringKey.kGeneric_Username,
|
|
||||||
},
|
|
||||||
accountRegisterPassword: {
|
|
||||||
placeholder: I18nStringKey.kGeneric_Password,
|
|
||||||
},
|
|
||||||
accountRegisterConfirmPassword: {
|
|
||||||
placeholder: I18nStringKey.kAccountModal_ConfirmPassword,
|
|
||||||
},
|
|
||||||
accountVerifyEmailCode: {
|
|
||||||
placeholder: I18nStringKey.kGeneric_VerificationCode,
|
|
||||||
},
|
|
||||||
accountVerifyEmailPassword: {
|
|
||||||
placeholder: I18nStringKey.kGeneric_Password,
|
|
||||||
},
|
|
||||||
accountSettingsEmail: {
|
|
||||||
placeholder: I18nStringKey.kGeneric_EMail,
|
|
||||||
},
|
|
||||||
accountSettingsUsername: {
|
|
||||||
placeholder: I18nStringKey.kGeneric_Username,
|
|
||||||
},
|
|
||||||
accountSettingsNewPassword: {
|
|
||||||
placeholder: I18nStringKey.kAccountModal_NewPassword,
|
|
||||||
},
|
|
||||||
accountSettingsConfirmNewPassword: {
|
|
||||||
placeholder: I18nStringKey.kAccountModal_ConfirmNewPassword,
|
|
||||||
},
|
|
||||||
accountSettingsCurrentPassword: {
|
|
||||||
placeholder: I18nStringKey.kAccountModal_CurrentPassword,
|
|
||||||
},
|
|
||||||
accountResetPasswordEmail: {
|
|
||||||
placeholder: I18nStringKey.kGeneric_EMail,
|
|
||||||
},
|
|
||||||
accountResetPasswordUsername: {
|
|
||||||
placeholder: I18nStringKey.kGeneric_Username,
|
|
||||||
},
|
|
||||||
accountResetPasswordCode: {
|
|
||||||
placeholder: I18nStringKey.kGeneric_VerificationCode,
|
|
||||||
},
|
|
||||||
accountResetPasswordNewPassword: {
|
|
||||||
placeholder: I18nStringKey.kAccountModal_NewPassword,
|
|
||||||
},
|
|
||||||
accountResetPasswordConfirmNewPassword: {
|
|
||||||
placeholder: I18nStringKey.kAccountModal_ConfirmNewPassword,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const kDomClassToStringMap: StringKeyMap = {
|
|
||||||
"mod-end-turn-btn": I18nStringKey.kVMButtons_EndTurn,
|
|
||||||
"mod-ban-btn": I18nStringKey.kAdminVMButtons_Ban,
|
|
||||||
"mod-kick-btn": I18nStringKey.kAdminVMButtons_Kick,
|
|
||||||
"mod-change-username-btn": I18nStringKey.kVMButtons_ChangeUsername,
|
|
||||||
"mod-temp-mute-btn": I18nStringKey.kAdminVMButtons_TempMute,
|
|
||||||
"mod-indef-mute-btn": I18nStringKey.kAdminVMButtons_IndefMute,
|
|
||||||
"mod-unmute-btn": I18nStringKey.kAdminVMButtons_Unmute,
|
|
||||||
"mod-get-ip-btn": I18nStringKey.kAdminVMButtons_GetIP,
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let domId of Object.keys(kDomIdtoStringMap)) {
|
|
||||||
let element = document.getElementById(domId);
|
|
||||||
if (element == null) {
|
|
||||||
alert(`Error: Could not find element with ID ${domId} in the DOM! Please tell a site admin this happened.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do the magic.
|
|
||||||
// N.B: For now, we assume all strings in this map are not formatted.
|
|
||||||
// If this assumption changes, then we should just use GetString() again
|
|
||||||
// and maybe include arguments, but for now this is okay
|
|
||||||
element.innerHTML = this.GetStringRaw(kDomIdtoStringMap[domId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let domId of Object.keys(kDomAttributeToStringMap)) {
|
|
||||||
let element = document.getElementById(domId);
|
|
||||||
if (element == null) {
|
|
||||||
alert(`Error: Could not find element with ID ${domId} in the DOM! Please tell a site admin this happened.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Figure out if we can get rid of this ts-ignore
|
|
||||||
// @ts-ignore
|
|
||||||
let attributes = kDomAttributeToStringMap[domId];
|
|
||||||
for (let attr of Object.keys(attributes)) {
|
|
||||||
element.setAttribute(attr, this.GetStringRaw(attributes[attr] as I18nStringKey));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let domClass of Object.keys(kDomClassToStringMap)) {
|
|
||||||
let elements = document.getElementsByClassName(domClass);
|
|
||||||
for (let element of elements) {
|
|
||||||
element.innerHTML = this.GetStringRaw(kDomClassToStringMap[domClass]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a (raw, unformatted) string. Currently only used if we don't need formatting.
|
|
||||||
GetStringRaw(key: I18nStringKey): string {
|
|
||||||
if (key === I18nStringKey.kGeneric_CollabVM && Config.SiteNameOverride) return Config.SiteNameOverride;
|
|
||||||
if (key === I18nStringKey.kWelcomeModal_Header && Config.WelcomeModalTitleOverride) return Config.WelcomeModalTitleOverride;
|
|
||||||
if (key === I18nStringKey.kWelcomeModal_Body && Config.WelcomeModalBodyOverride) return Config.WelcomeModalBodyOverride;
|
|
||||||
let val = this.lang.stringKeys[key];
|
|
||||||
|
|
||||||
// Look up the fallback language by default if the language doesn't
|
|
||||||
// have that string key yet; if the fallback doesn't have it either,
|
|
||||||
// then just return the string key and a bit of a notice things have gone wrong
|
|
||||||
if (val == undefined) {
|
|
||||||
let fallback = fallbackLanguage.stringKeys[key];
|
|
||||||
if (fallback !== undefined) val = fallback;
|
|
||||||
else return `${key} (ERROR LOOKING UP TRANSLATION!!!)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a formatted localized string.
|
|
||||||
GetString(key: I18nStringKey, ...replacements: StringLike[]): string {
|
|
||||||
return Format(this.GetStringRaw(key), ...replacements);
|
|
||||||
}
|
|
||||||
|
|
||||||
on<e extends keyof I18nEvents>(event: e, cb: I18nEvents[e]): Unsubscribe {
|
|
||||||
return this.emitter.on(event, cb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export let TheI18n = new I18n();
|
|
||||||
|
|
@ -1,408 +0,0 @@
|
||||||
// Pulled a bunch of functions out of the guac source code to get a keysym
|
|
||||||
// and then a wrapper
|
|
||||||
// shitty but it works so /shrug
|
|
||||||
// THIS SUCKS SO BAD AND I HATE IT PLEASE REWRITE ALL OF THIS
|
|
||||||
|
|
||||||
export default function GetKeysym(keyCode: number, key: string, location: number): number | null {
|
|
||||||
let keysym = keysym_from_key_identifier(key, location) || keysym_from_keycode(keyCode, location);
|
|
||||||
return keysym;
|
|
||||||
}
|
|
||||||
|
|
||||||
function keysym_from_key_identifier(identifier: string, location: number): number | null {
|
|
||||||
if (!identifier) return null;
|
|
||||||
|
|
||||||
let typedCharacter: string | undefined;
|
|
||||||
|
|
||||||
// If identifier is U+xxxx, decode Unicode character
|
|
||||||
const unicodePrefixLocation = identifier.indexOf('U+');
|
|
||||||
if (unicodePrefixLocation >= 0) {
|
|
||||||
const hex = identifier.substring(unicodePrefixLocation + 2);
|
|
||||||
typedCharacter = String.fromCharCode(parseInt(hex, 16));
|
|
||||||
} else if (identifier.length === 1) typedCharacter = identifier;
|
|
||||||
else return get_keysym(keyidentifier_keysym[identifier], location);
|
|
||||||
|
|
||||||
if (!typedCharacter) return null;
|
|
||||||
|
|
||||||
const codepoint = typedCharacter.charCodeAt(0);
|
|
||||||
return keysym_from_charcode(codepoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_keysym(keysyms: number[] | null, location: number): number | null {
|
|
||||||
if (!keysyms) return null;
|
|
||||||
return keysyms[location] || keysyms[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
function keysym_from_charcode(codepoint: number): number | null {
|
|
||||||
if (isControlCharacter(codepoint)) return 0xff00 | codepoint;
|
|
||||||
if (codepoint >= 0x0000 && codepoint <= 0x00ff) return codepoint;
|
|
||||||
if (codepoint >= 0x0100 && codepoint <= 0x10ffff) return 0x01000000 | codepoint;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isControlCharacter(codepoint: number): boolean {
|
|
||||||
return codepoint <= 0x1f || (codepoint >= 0x7f && codepoint <= 0x9f);
|
|
||||||
}
|
|
||||||
|
|
||||||
function keysym_from_keycode(keyCode: number, location: number): number | null {
|
|
||||||
return get_keysym(keycodeKeysyms[keyCode], location);
|
|
||||||
}
|
|
||||||
|
|
||||||
function key_identifier_sane(keyCode: number, keyIdentifier: string): boolean {
|
|
||||||
if (!keyIdentifier) return false;
|
|
||||||
const unicodePrefixLocation = keyIdentifier.indexOf('U+');
|
|
||||||
if (unicodePrefixLocation === -1) return true;
|
|
||||||
|
|
||||||
const codepoint = parseInt(keyIdentifier.substring(unicodePrefixLocation + 2), 16);
|
|
||||||
if (keyCode !== codepoint) return true;
|
|
||||||
if ((keyCode >= 65 && keyCode <= 90) || (keyCode >= 48 && keyCode <= 57)) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OSK_buttonToKeysym(button: string): number | null {
|
|
||||||
const keyMapping = OSK_keyMappings.find((mapping) => mapping.includes(button));
|
|
||||||
if (keyMapping) {
|
|
||||||
const [, keyCode, keyIdentifier, key, location] = keyMapping;
|
|
||||||
return GetKeysym(keyCode, key, location);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface KeyIdentifierKeysym {
|
|
||||||
[key: string]: number[] | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface KeyCodeKeysyms {
|
|
||||||
[key: number]: number[] | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keycodeKeysyms: KeyCodeKeysyms = {
|
|
||||||
8: [0xff08], // backspace
|
|
||||||
9: [0xff09], // tab
|
|
||||||
12: [0xff0b, 0xff0b, 0xff0b, 0xffb5], // clear / KP 5
|
|
||||||
13: [0xff0d], // enter
|
|
||||||
16: [0xffe1, 0xffe1, 0xffe2], // shift
|
|
||||||
17: [0xffe3, 0xffe3, 0xffe4], // ctrl
|
|
||||||
18: [0xffe9, 0xffe9, 0xfe03], // alt
|
|
||||||
19: [0xff13], // pause/break
|
|
||||||
20: [0xffe5], // caps lock
|
|
||||||
27: [0xff1b], // escape
|
|
||||||
32: [0x0020], // space
|
|
||||||
33: [0xff55, 0xff55, 0xff55, 0xffb9], // page up / KP 9
|
|
||||||
34: [0xff56, 0xff56, 0xff56, 0xffb3], // page down / KP 3
|
|
||||||
35: [0xff57, 0xff57, 0xff57, 0xffb1], // end / KP 1
|
|
||||||
36: [0xff50, 0xff50, 0xff50, 0xffb7], // home / KP 7
|
|
||||||
37: [0xff51, 0xff51, 0xff51, 0xffb4], // left arrow / KP 4
|
|
||||||
38: [0xff52, 0xff52, 0xff52, 0xffb8], // up arrow / KP 8
|
|
||||||
39: [0xff53, 0xff53, 0xff53, 0xffb6], // right arrow / KP 6
|
|
||||||
40: [0xff54, 0xff54, 0xff54, 0xffb2], // down arrow / KP 2
|
|
||||||
45: [0xff63, 0xff63, 0xff63, 0xffb0], // insert / KP 0
|
|
||||||
46: [0xffff, 0xffff, 0xffff, 0xffae], // delete / KP decimal
|
|
||||||
91: [0xffeb], // left window key (hyper_l)
|
|
||||||
92: [0xff67], // right window key (menu key?)
|
|
||||||
93: null, // select key
|
|
||||||
96: [0xffb0], // KP 0
|
|
||||||
97: [0xffb1], // KP 1
|
|
||||||
98: [0xffb2], // KP 2
|
|
||||||
99: [0xffb3], // KP 3
|
|
||||||
100: [0xffb4], // KP 4
|
|
||||||
101: [0xffb5], // KP 5
|
|
||||||
102: [0xffb6], // KP 6
|
|
||||||
103: [0xffb7], // KP 7
|
|
||||||
104: [0xffb8], // KP 8
|
|
||||||
105: [0xffb9], // KP 9
|
|
||||||
106: [0xffaa], // KP multiply
|
|
||||||
107: [0xffab], // KP add
|
|
||||||
109: [0xffad], // KP subtract
|
|
||||||
110: [0xffae], // KP decimal
|
|
||||||
111: [0xffaf], // KP divide
|
|
||||||
112: [0xffbe], // f1
|
|
||||||
113: [0xffbf], // f2
|
|
||||||
114: [0xffc0], // f3
|
|
||||||
115: [0xffc1], // f4
|
|
||||||
116: [0xffc2], // f5
|
|
||||||
117: [0xffc3], // f6
|
|
||||||
118: [0xffc4], // f7
|
|
||||||
119: [0xffc5], // f8
|
|
||||||
120: [0xffc6], // f9
|
|
||||||
121: [0xffc7], // f10
|
|
||||||
122: [0xffc8], // f11
|
|
||||||
123: [0xffc9], // f12
|
|
||||||
144: [0xff7f], // num lock
|
|
||||||
145: [0xff14], // scroll lock
|
|
||||||
225: [0xfe03] // altgraph (iso_level3_shift)
|
|
||||||
};
|
|
||||||
|
|
||||||
const keyidentifier_keysym: KeyIdentifierKeysym = {
|
|
||||||
Again: [0xff66],
|
|
||||||
AllCandidates: [0xff3d],
|
|
||||||
Alphanumeric: [0xff30],
|
|
||||||
Alt: [0xffe9, 0xffe9, 0xfe03],
|
|
||||||
Attn: [0xfd0e],
|
|
||||||
AltGraph: [0xfe03],
|
|
||||||
ArrowDown: [0xff54],
|
|
||||||
ArrowLeft: [0xff51],
|
|
||||||
ArrowRight: [0xff53],
|
|
||||||
ArrowUp: [0xff52],
|
|
||||||
Backspace: [0xff08],
|
|
||||||
CapsLock: [0xffe5],
|
|
||||||
Cancel: [0xff69],
|
|
||||||
Clear: [0xff0b],
|
|
||||||
Convert: [0xff21],
|
|
||||||
Copy: [0xfd15],
|
|
||||||
Crsel: [0xfd1c],
|
|
||||||
CrSel: [0xfd1c],
|
|
||||||
CodeInput: [0xff37],
|
|
||||||
Compose: [0xff20],
|
|
||||||
Control: [0xffe3, 0xffe3, 0xffe4],
|
|
||||||
ContextMenu: [0xff67],
|
|
||||||
DeadGrave: [0xfe50],
|
|
||||||
DeadAcute: [0xfe51],
|
|
||||||
DeadCircumflex: [0xfe52],
|
|
||||||
DeadTilde: [0xfe53],
|
|
||||||
DeadMacron: [0xfe54],
|
|
||||||
DeadBreve: [0xfe55],
|
|
||||||
DeadAboveDot: [0xfe56],
|
|
||||||
DeadUmlaut: [0xfe57],
|
|
||||||
DeadAboveRing: [0xfe58],
|
|
||||||
DeadDoubleacute: [0xfe59],
|
|
||||||
DeadCaron: [0xfe5a],
|
|
||||||
DeadCedilla: [0xfe5b],
|
|
||||||
DeadOgonek: [0xfe5c],
|
|
||||||
DeadIota: [0xfe5d],
|
|
||||||
DeadVoicedSound: [0xfe5e],
|
|
||||||
DeadSemivoicedSound: [0xfe5f],
|
|
||||||
Delete: [0xffff],
|
|
||||||
Down: [0xff54],
|
|
||||||
End: [0xff57],
|
|
||||||
Enter: [0xff0d],
|
|
||||||
EraseEof: [0xfd06],
|
|
||||||
Escape: [0xff1b],
|
|
||||||
Execute: [0xff62],
|
|
||||||
Exsel: [0xfd1d],
|
|
||||||
ExSel: [0xfd1d],
|
|
||||||
F1: [0xffbe],
|
|
||||||
F2: [0xffbf],
|
|
||||||
F3: [0xffc0],
|
|
||||||
F4: [0xffc1],
|
|
||||||
F5: [0xffc2],
|
|
||||||
F6: [0xffc3],
|
|
||||||
F7: [0xffc4],
|
|
||||||
F8: [0xffc5],
|
|
||||||
F9: [0xffc6],
|
|
||||||
F10: [0xffc7],
|
|
||||||
F11: [0xffc8],
|
|
||||||
F12: [0xffc9],
|
|
||||||
F13: [0xffca],
|
|
||||||
F14: [0xffcb],
|
|
||||||
F15: [0xffcc],
|
|
||||||
F16: [0xffcd],
|
|
||||||
F17: [0xffce],
|
|
||||||
F18: [0xffcf],
|
|
||||||
F19: [0xffd0],
|
|
||||||
F20: [0xffd1],
|
|
||||||
F21: [0xffd2],
|
|
||||||
F22: [0xffd3],
|
|
||||||
F23: [0xffd4],
|
|
||||||
F24: [0xffd5],
|
|
||||||
Find: [0xff68],
|
|
||||||
GroupFirst: [0xfe0c],
|
|
||||||
GroupLast: [0xfe0e],
|
|
||||||
GroupNext: [0xfe08],
|
|
||||||
GroupPrevious: [0xfe0a],
|
|
||||||
FullWidth: null,
|
|
||||||
HalfWidth: null,
|
|
||||||
HangulMode: [0xff31],
|
|
||||||
Hankaku: [0xff29],
|
|
||||||
HanjaMode: [0xff34],
|
|
||||||
Help: [0xff6a],
|
|
||||||
Hiragana: [0xff25],
|
|
||||||
HiraganaKatakana: [0xff27],
|
|
||||||
Home: [0xff50],
|
|
||||||
Hyper: [0xffed, 0xffed, 0xffee],
|
|
||||||
Insert: [0xff63],
|
|
||||||
JapaneseHiragana: [0xff25],
|
|
||||||
JapaneseKatakana: [0xff26],
|
|
||||||
JapaneseRomaji: [0xff24],
|
|
||||||
JunjaMode: [0xff38],
|
|
||||||
KanaMode: [0xff2d],
|
|
||||||
KanjiMode: [0xff21],
|
|
||||||
Katakana: [0xff26],
|
|
||||||
Left: [0xff51],
|
|
||||||
Meta: [0xffe7, 0xffe7, 0xffe8],
|
|
||||||
ModeChange: [0xff7e],
|
|
||||||
NumLock: [0xff7f],
|
|
||||||
PageDown: [0xff56],
|
|
||||||
PageUp: [0xff55],
|
|
||||||
Pause: [0xff13],
|
|
||||||
Play: [0xfd16],
|
|
||||||
PreviousCandidate: [0xff3e],
|
|
||||||
PrintScreen: [0xfd1d],
|
|
||||||
Redo: [0xff66],
|
|
||||||
Right: [0xff53],
|
|
||||||
RomanCharacters: null,
|
|
||||||
Scroll: [0xff14],
|
|
||||||
Select: [0xff60],
|
|
||||||
Separator: [0xffac],
|
|
||||||
Shift: [0xffe1, 0xffe1, 0xffe2],
|
|
||||||
SingleCandidate: [0xff3c],
|
|
||||||
Super: [0xffeb, 0xffeb, 0xffec],
|
|
||||||
Tab: [0xff09],
|
|
||||||
Up: [0xff52],
|
|
||||||
Undo: [0xff65],
|
|
||||||
Win: [0xffeb],
|
|
||||||
Zenkaku: [0xff28],
|
|
||||||
ZenkakuHankaku: [0xff2a]
|
|
||||||
};
|
|
||||||
|
|
||||||
const OSK_keyMappings: [string, number, string, string, number][] = [
|
|
||||||
['!', 49, 'Digit1', '!', 0],
|
|
||||||
['#', 51, 'Digit3', '#', 0],
|
|
||||||
['$', 52, 'Digit4', '$', 0],
|
|
||||||
['%', 53, 'Digit5', '%', 0],
|
|
||||||
['&', 55, 'Digit7', '&', 0],
|
|
||||||
["'", 222, 'Quote', "'", 0],
|
|
||||||
['(', 57, 'Digit9', '(', 0],
|
|
||||||
[')', 48, 'Digit0', ')', 0],
|
|
||||||
['*', 56, 'Digit8', '*', 0],
|
|
||||||
['+', 187, 'Equal', '+', 0],
|
|
||||||
[',', 188, 'Comma', ',', 0],
|
|
||||||
['-', 189, 'Minus', '-', 0],
|
|
||||||
['.', 190, 'Period', '.', 0],
|
|
||||||
['/', 191, 'Slash', '/', 0],
|
|
||||||
['0', 48, 'Digit0', '0', 0],
|
|
||||||
['1', 49, 'Digit1', '1', 0],
|
|
||||||
['2', 50, 'Digit2', '2', 0],
|
|
||||||
['3', 51, 'Digit3', '3', 0],
|
|
||||||
['4', 52, 'Digit4', '4', 0],
|
|
||||||
['5', 53, 'Digit5', '5', 0],
|
|
||||||
['6', 54, 'Digit6', '6', 0],
|
|
||||||
['7', 55, 'Digit7', '7', 0],
|
|
||||||
['8', 56, 'Digit8', '8', 0],
|
|
||||||
['9', 57, 'Digit9', '9', 0],
|
|
||||||
[':', 186, 'Semicolon', ':', 0],
|
|
||||||
[';', 186, 'Semicolon', ';', 0],
|
|
||||||
['<', 188, 'Comma', '<', 0],
|
|
||||||
['=', 187, 'Equal', '=', 0],
|
|
||||||
['>', 190, 'Period', '>', 0],
|
|
||||||
['?', 191, 'Slash', '?', 0],
|
|
||||||
['@', 50, 'Digit2', '@', 0],
|
|
||||||
['A', 65, 'KeyA', 'A', 0],
|
|
||||||
['B', 66, 'KeyB', 'B', 0],
|
|
||||||
['C', 67, 'KeyC', 'C', 0],
|
|
||||||
['D', 68, 'KeyD', 'D', 0],
|
|
||||||
['E', 69, 'KeyE', 'E', 0],
|
|
||||||
['F', 70, 'KeyF', 'F', 0],
|
|
||||||
['G', 71, 'KeyG', 'G', 0],
|
|
||||||
['H', 72, 'KeyH', 'H', 0],
|
|
||||||
['I', 73, 'KeyI', 'I', 0],
|
|
||||||
['J', 74, 'KeyJ', 'J', 0],
|
|
||||||
['K', 75, 'KeyK', 'K', 0],
|
|
||||||
['L', 76, 'KeyL', 'L', 0],
|
|
||||||
['M', 77, 'KeyM', 'M', 0],
|
|
||||||
['N', 78, 'KeyN', 'N', 0],
|
|
||||||
['O', 79, 'KeyO', 'O', 0],
|
|
||||||
['P', 80, 'KeyP', 'P', 0],
|
|
||||||
['Q', 81, 'KeyQ', 'Q', 0],
|
|
||||||
['R', 82, 'KeyR', 'R', 0],
|
|
||||||
['S', 83, 'KeyS', 'S', 0],
|
|
||||||
['T', 84, 'KeyT', 'T', 0],
|
|
||||||
['U', 85, 'KeyU', 'U', 0],
|
|
||||||
['V', 86, 'KeyV', 'V', 0],
|
|
||||||
['W', 87, 'KeyW', 'W', 0],
|
|
||||||
['X', 88, 'KeyX', 'X', 0],
|
|
||||||
['Y', 89, 'KeyY', 'Y', 0],
|
|
||||||
['Z', 90, 'KeyZ', 'Z', 0],
|
|
||||||
['[', 219, 'BracketLeft', '[', 0],
|
|
||||||
['\\', 220, 'Backslash', '\\', 0],
|
|
||||||
[']', 221, 'BracketRight', ']', 0],
|
|
||||||
['^', 54, 'Digit6', '^', 0],
|
|
||||||
['_', 189, 'Minus', '_', 0],
|
|
||||||
['`', 192, 'Backquote', '`', 0],
|
|
||||||
['a', 65, 'KeyA', 'a', 0],
|
|
||||||
['b', 66, 'KeyB', 'b', 0],
|
|
||||||
['c', 67, 'KeyC', 'c', 0],
|
|
||||||
['d', 68, 'KeyD', 'd', 0],
|
|
||||||
['e', 69, 'KeyE', 'e', 0],
|
|
||||||
['f', 70, 'KeyF', 'f', 0],
|
|
||||||
['g', 71, 'KeyG', 'g', 0],
|
|
||||||
['h', 72, 'KeyH', 'h', 0],
|
|
||||||
['i', 73, 'KeyI', 'i', 0],
|
|
||||||
['j', 74, 'KeyJ', 'j', 0],
|
|
||||||
['k', 75, 'KeyK', 'k', 0],
|
|
||||||
['l', 76, 'KeyL', 'l', 0],
|
|
||||||
['m', 77, 'KeyM', 'm', 0],
|
|
||||||
['n', 78, 'KeyN', 'n', 0],
|
|
||||||
['o', 79, 'KeyO', 'o', 0],
|
|
||||||
['p', 80, 'KeyP', 'p', 0],
|
|
||||||
['q', 81, 'KeyQ', 'q', 0],
|
|
||||||
['r', 82, 'KeyR', 'r', 0],
|
|
||||||
['s', 83, 'KeyS', 's', 0],
|
|
||||||
['t', 84, 'KeyT', 't', 0],
|
|
||||||
['u', 85, 'KeyU', 'u', 0],
|
|
||||||
['v', 86, 'KeyV', 'v', 0],
|
|
||||||
['w', 87, 'KeyW', 'w', 0],
|
|
||||||
['x', 88, 'KeyX', 'x', 0],
|
|
||||||
['y', 89, 'KeyY', 'y', 0],
|
|
||||||
['z', 90, 'KeyZ', 'z', 0],
|
|
||||||
['{', 219, 'BracketLeft', '{', 0],
|
|
||||||
['{altleft}', 18, 'AltLeft', 'AltLeft', 1],
|
|
||||||
['{altright}', 18, 'AltRight', 'AltRight', 2],
|
|
||||||
['{arrowdown}', 40, 'ArrowDown', 'ArrowDown', 0],
|
|
||||||
['{arrowleft}', 37, 'ArrowLeft', 'ArrowLeft', 0],
|
|
||||||
['{arrowright}', 39, 'ArrowRight', 'ArrowRight', 0],
|
|
||||||
['{arrowup}', 38, 'ArrowUp', 'ArrowUp', 0],
|
|
||||||
['{backspace}', 8, 'Backspace', 'Backspace', 0],
|
|
||||||
['{capslock}', 20, 'CapsLock', 'CapsLock', 0],
|
|
||||||
['{controlleft}', 17, 'ControlLeft', 'ControlLeft', 1],
|
|
||||||
['{controlright}', 17, 'ControlRight', 'ControlRight', 2],
|
|
||||||
['{delete}', 46, 'Delete', 'Delete', 0],
|
|
||||||
['{end}', 35, 'End', 'End', 0],
|
|
||||||
['{enter}', 13, 'Enter', 'Enter', 0],
|
|
||||||
['{escape}', 27, 'Escape', 'Escape', 0],
|
|
||||||
['{f10}', 121, 'F10', 'F10', 0],
|
|
||||||
['{f11}', 122, 'F11', 'F11', 0],
|
|
||||||
['{f12}', 123, 'F12', 'F12', 0],
|
|
||||||
['{f1}', 112, 'F1', 'F1', 0],
|
|
||||||
['{f2}', 113, 'F2', 'F2', 0],
|
|
||||||
['{f3}', 114, 'F3', 'F3', 0],
|
|
||||||
['{f4}', 115, 'F4', 'F4', 0],
|
|
||||||
['{f5}', 116, 'F5', 'F5', 0],
|
|
||||||
['{f6}', 117, 'F6', 'F6', 0],
|
|
||||||
['{f7}', 118, 'F7', 'F7', 0],
|
|
||||||
['{f8}', 119, 'F8', 'F8', 0],
|
|
||||||
['{f9}', 120, 'F9', 'F9', 0],
|
|
||||||
['{home}', 36, 'Home', 'Home', 0],
|
|
||||||
['{insert}', 45, 'Insert', 'Insert', 0],
|
|
||||||
['{metaleft}', 91, 'OSLeft', 'OSLeft', 1],
|
|
||||||
['{metaright}', 92, 'OSRight', 'OSRight', 2],
|
|
||||||
['{numlock}', 144, 'NumLock', 'NumLock', 0],
|
|
||||||
['{numpad0}', 96, 'Numpad0', 'Numpad0', 3],
|
|
||||||
['{numpad1}', 97, 'Numpad1', 'Numpad1', 3],
|
|
||||||
['{numpad2}', 98, 'Numpad2', 'Numpad2', 3],
|
|
||||||
['{numpad3}', 99, 'Numpad3', 'Numpad3', 3],
|
|
||||||
['{numpad4}', 100, 'Numpad4', 'Numpad4', 3],
|
|
||||||
['{numpad5}', 101, 'Numpad5', 'Numpad5', 3],
|
|
||||||
['{numpad6}', 102, 'Numpad6', 'Numpad6', 3],
|
|
||||||
['{numpad7}', 103, 'Numpad7', 'Numpad7', 3],
|
|
||||||
['{numpad8}', 104, 'Numpad8', 'Numpad8', 3],
|
|
||||||
['{numpad9}', 105, 'Numpad9', 'Numpad9', 3],
|
|
||||||
['{numpadadd}', 107, 'NumpadAdd', 'NumpadAdd', 3],
|
|
||||||
['{numpaddecimal}', 110, 'NumpadDecimal', 'NumpadDecimal', 3],
|
|
||||||
['{numpaddivide}', 111, 'NumpadDivide', 'NumpadDivide', 3],
|
|
||||||
['{numpadenter}', 13, 'NumpadEnter', 'NumpadEnter', 3],
|
|
||||||
['{numpadmultiply}', 106, 'NumpadMultiply', 'NumpadMultiply', 3],
|
|
||||||
['{numpadsubtract}', 109, 'NumpadSubtract', 'NumpadSubtract', 3],
|
|
||||||
['{pagedown}', 34, 'PageDown', 'PageDown', 0],
|
|
||||||
['{pageup}', 33, 'PageUp', 'PageUp', 0],
|
|
||||||
['{pause}', 19, 'Pause', 'Pause', 0],
|
|
||||||
['{prtscr}', 44, 'PrintScreen', 'PrintScreen', 0],
|
|
||||||
['{scrolllock}', 145, 'ScrollLock', 'ScrollLock', 0],
|
|
||||||
['{shiftleft}', 16, 'ShiftLeft', 'ShiftLeft', 1],
|
|
||||||
['{shiftright}', 16, 'ShiftRight', 'ShiftRight', 2],
|
|
||||||
['{space}', 32, 'Space', 'Space', 0],
|
|
||||||
['{tab}', 9, 'Tab', 'Tab', 0],
|
|
||||||
['|', 220, 'Backslash', '|', 0],
|
|
||||||
['}', 221, 'BracketRight', '}', 0],
|
|
||||||
['~', 192, 'Backquote', '~', 0],
|
|
||||||
['"', 222, 'Quote', '"', 0]
|
|
||||||
];
|
|
||||||
1321
src/ts/main.ts
1321
src/ts/main.ts
File diff suppressed because it is too large
Load Diff
|
|
@ -1,685 +0,0 @@
|
||||||
import { createNanoEvents, Emitter, DefaultEvents, Unsubscribe } from 'nanoevents';
|
|
||||||
import * as Guacutils from './Guacutils.js';
|
|
||||||
import VM from './VM.js';
|
|
||||||
import { User } from './User.js';
|
|
||||||
import { AdminOpcode, Permissions, Rank } from './Permissions.js';
|
|
||||||
import TurnStatus from './TurnStatus.js';
|
|
||||||
import Mouse from './mouse.js';
|
|
||||||
import GetKeysym from '../keyboard.js';
|
|
||||||
import VoteStatus from './VoteStatus.js';
|
|
||||||
import MuteState from './MuteState.js';
|
|
||||||
import { StringLike } from '../StringLike.js';
|
|
||||||
|
|
||||||
export interface CollabVMClientEvents {
|
|
||||||
//open: () => void;
|
|
||||||
close: () => void;
|
|
||||||
|
|
||||||
message: (...args: string[]) => void;
|
|
||||||
|
|
||||||
// Protocol stuff
|
|
||||||
chat: (username: string, message: string) => void;
|
|
||||||
|
|
||||||
adduser: (user: User) => void;
|
|
||||||
remuser: (user: User) => void;
|
|
||||||
|
|
||||||
renamestatus: (status: 'taken' | 'invalid' | 'blacklisted') => void;
|
|
||||||
turn: (status: TurnStatus) => void;
|
|
||||||
|
|
||||||
rename: (oldUsername: string, newUsername: string, selfRename: boolean) => void;
|
|
||||||
|
|
||||||
vote: (status: VoteStatus) => void;
|
|
||||||
voteend: () => void;
|
|
||||||
votecd: (coolDownTime: number) => void;
|
|
||||||
|
|
||||||
badpw: () => void;
|
|
||||||
login: (rank: Rank, perms: Permissions) => void;
|
|
||||||
|
|
||||||
// Auth stuff
|
|
||||||
auth: (server: string) => void;
|
|
||||||
accountlogin: (success: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// types for private emitter
|
|
||||||
interface CollabVMClientPrivateEvents {
|
|
||||||
open: () => void;
|
|
||||||
list: (listEntries: string[]) => void;
|
|
||||||
connect: (connectedToVM: boolean) => void;
|
|
||||||
ip: (username: string, ip: string) => void;
|
|
||||||
qemu: (qemuResponse: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class CollabVMClient {
|
|
||||||
// Fields
|
|
||||||
private socket: WebSocket;
|
|
||||||
canvas: HTMLCanvasElement;
|
|
||||||
// A secondary canvas that is not scaled
|
|
||||||
unscaledCanvas: HTMLCanvasElement;
|
|
||||||
canvasScale : { width : number, height : number } = { width: 0, height: 0 };
|
|
||||||
actualScreenSize : { width : number, height : number } = { width: 0, height: 0 };
|
|
||||||
private unscaledCtx: CanvasRenderingContext2D;
|
|
||||||
private ctx: CanvasRenderingContext2D;
|
|
||||||
private url: string;
|
|
||||||
private connectedToVM: boolean = false;
|
|
||||||
private users: User[] = [];
|
|
||||||
private username: string | null = null;
|
|
||||||
private mouse: Mouse = new Mouse();
|
|
||||||
private rank: Rank = Rank.Unregistered;
|
|
||||||
private perms: Permissions = new Permissions(0);
|
|
||||||
private voteStatus: VoteStatus | null = null;
|
|
||||||
private node: string | null = null;
|
|
||||||
private auth: boolean = false;
|
|
||||||
// events that are used internally and not exposed
|
|
||||||
private internalEmitter: Emitter<CollabVMClientPrivateEvents>;
|
|
||||||
// public events
|
|
||||||
private publicEmitter: Emitter<CollabVMClientEvents>;
|
|
||||||
|
|
||||||
private unsubscribeCallbacks: Array<Unsubscribe> = [];
|
|
||||||
|
|
||||||
constructor(url: string) {
|
|
||||||
// Save the URL
|
|
||||||
this.url = url;
|
|
||||||
// Create the events
|
|
||||||
this.internalEmitter = createNanoEvents();
|
|
||||||
this.publicEmitter = createNanoEvents();
|
|
||||||
// Create the canvas
|
|
||||||
this.canvas = document.createElement('canvas');
|
|
||||||
this.unscaledCanvas = document.createElement('canvas');
|
|
||||||
// Set tab index so it can be focused
|
|
||||||
this.canvas.tabIndex = -1;
|
|
||||||
// Get the 2D context
|
|
||||||
this.ctx = this.canvas.getContext('2d')!;
|
|
||||||
this.unscaledCtx = this.unscaledCanvas.getContext('2d')!;
|
|
||||||
// Bind canvas click
|
|
||||||
this.canvas.addEventListener('click', (e) => {
|
|
||||||
if (this.users.find((u) => u.username === this.username)?.turn === -1) this.turn(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bind keyboard and mouse
|
|
||||||
this.canvas.addEventListener(
|
|
||||||
'mousedown',
|
|
||||||
(e: MouseEvent) => {
|
|
||||||
if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return;
|
|
||||||
this.mouse.initFromMouseEvent(e);
|
|
||||||
this.sendmouse(this.mouse.x, this.mouse.y, this.mouse.makeMask());
|
|
||||||
},
|
|
||||||
{
|
|
||||||
capture: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.canvas.addEventListener(
|
|
||||||
'mouseup',
|
|
||||||
(e: MouseEvent) => {
|
|
||||||
if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return;
|
|
||||||
this.mouse.initFromMouseEvent(e);
|
|
||||||
this.sendmouse(this.mouse.x, this.mouse.y, this.mouse.makeMask());
|
|
||||||
},
|
|
||||||
{
|
|
||||||
capture: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.canvas.addEventListener(
|
|
||||||
'mousemove',
|
|
||||||
(e: MouseEvent) => {
|
|
||||||
if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return;
|
|
||||||
this.mouse.initFromMouseEvent(e);
|
|
||||||
this.sendmouse(this.mouse.x, this.mouse.y, this.mouse.makeMask());
|
|
||||||
},
|
|
||||||
{
|
|
||||||
capture: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.canvas.addEventListener(
|
|
||||||
'keydown',
|
|
||||||
(e: KeyboardEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return;
|
|
||||||
let keysym = GetKeysym(e.keyCode, e.key, e.location);
|
|
||||||
if (keysym === null) return;
|
|
||||||
this.key(keysym, true);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
capture: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.canvas.addEventListener(
|
|
||||||
'keyup',
|
|
||||||
(e: KeyboardEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return;
|
|
||||||
let keysym = GetKeysym(e.keyCode, e.key, e.location);
|
|
||||||
if (keysym === null) return;
|
|
||||||
this.key(keysym, false);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
capture: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.canvas.addEventListener(
|
|
||||||
'wheel',
|
|
||||||
(ev: WheelEvent) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return;
|
|
||||||
this.mouse.initFromWheelEvent(ev);
|
|
||||||
|
|
||||||
this.sendmouse(this.mouse.x, this.mouse.y, this.mouse.makeMask());
|
|
||||||
|
|
||||||
// this is a very, very ugly hack but it seems to work so /shrug
|
|
||||||
if (this.mouse.scrollUp) this.mouse.scrollUp = false;
|
|
||||||
else if (this.mouse.scrollDown) this.mouse.scrollDown = false;
|
|
||||||
|
|
||||||
this.sendmouse(this.mouse.x, this.mouse.y, this.mouse.makeMask());
|
|
||||||
},
|
|
||||||
{
|
|
||||||
capture: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
window.addEventListener('resize', (e) => this.onWindowResize(e));
|
|
||||||
this.canvas.addEventListener('contextmenu', (e) => e.preventDefault());
|
|
||||||
// Create the WebSocket
|
|
||||||
this.socket = new WebSocket(url, 'guacamole');
|
|
||||||
// Add the event listeners
|
|
||||||
this.socket.addEventListener('open', () => this.onOpen());
|
|
||||||
this.socket.addEventListener('message', (event) => this.onMessage(event));
|
|
||||||
this.socket.addEventListener('close', () => this.publicEmitter.emit('close'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fires when the WebSocket connection is opened
|
|
||||||
private onOpen() {
|
|
||||||
this.internalEmitter.emit('open');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fires on WebSocket message
|
|
||||||
private onMessage(event: MessageEvent) {
|
|
||||||
let msgArr: string[];
|
|
||||||
try {
|
|
||||||
msgArr = Guacutils.decode(event.data);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Server sent invalid message (${e})`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.publicEmitter.emit('message', ...msgArr);
|
|
||||||
switch (msgArr[0]) {
|
|
||||||
case 'nop': {
|
|
||||||
// Send a NOP back
|
|
||||||
this.send('nop');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'list': {
|
|
||||||
// pass msgarr to the emitter for processing by list()
|
|
||||||
this.internalEmitter.emit('list', msgArr.slice(1));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'connect': {
|
|
||||||
this.connectedToVM = msgArr[1] === '1';
|
|
||||||
this.internalEmitter.emit('connect', this.connectedToVM);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'size': {
|
|
||||||
if (msgArr[1] !== '0') return;
|
|
||||||
this.recalculateCanvasScale(parseInt(msgArr[2]), parseInt(msgArr[3]));
|
|
||||||
this.unscaledCanvas.width = this.actualScreenSize.width;
|
|
||||||
this.unscaledCanvas.height = this.actualScreenSize.height;
|
|
||||||
this.canvas.width = this.canvasScale.width;
|
|
||||||
this.canvas.height = this.canvasScale.height;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'png': {
|
|
||||||
// Despite the opcode name, this is actually JPEG, because old versions of the server used PNG and yknow backwards compatibility
|
|
||||||
let img = new Image();
|
|
||||||
var x = parseInt(msgArr[3]);
|
|
||||||
var y = parseInt(msgArr[4]);
|
|
||||||
img.addEventListener('load', () => {
|
|
||||||
if (this.actualScreenSize.width !== this.canvasScale.width || this.actualScreenSize.height !== this.canvasScale.height)
|
|
||||||
this.unscaledCtx.drawImage(img, x, y);
|
|
||||||
// Scale the image to the canvas
|
|
||||||
this.ctx.drawImage(img, 0, 0, img.width, img.height,
|
|
||||||
(x / this.actualScreenSize.width) * this.canvas.width,
|
|
||||||
(y / this.actualScreenSize.height) * this.canvas.height,
|
|
||||||
(img.width / this.actualScreenSize.width) * this.canvas.width,
|
|
||||||
(img.height / this.actualScreenSize.height) * this.canvas.height
|
|
||||||
);
|
|
||||||
});
|
|
||||||
img.src = 'data:image/jpeg;base64,' + msgArr[5];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'chat': {
|
|
||||||
for (let i = 1; i < msgArr.length; i += 2) {
|
|
||||||
this.publicEmitter.emit('chat', msgArr[i], msgArr[i + 1]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'adduser': {
|
|
||||||
for (let i = 2; i < msgArr.length; i += 2) {
|
|
||||||
let _user = this.users.find((u) => u.username === msgArr[i]);
|
|
||||||
if (_user !== undefined) {
|
|
||||||
_user.rank = parseInt(msgArr[i + 1]);
|
|
||||||
} else {
|
|
||||||
_user = new User(msgArr[i], parseInt(msgArr[i + 1]));
|
|
||||||
this.users.push(_user);
|
|
||||||
}
|
|
||||||
this.publicEmitter.emit('adduser', _user);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'remuser': {
|
|
||||||
for (let i = 2; i < msgArr.length; i++) {
|
|
||||||
let _user = this.users.find((u) => u.username === msgArr[i]);
|
|
||||||
if (_user === undefined) continue;
|
|
||||||
this.users.splice(this.users.indexOf(_user), 1);
|
|
||||||
this.publicEmitter.emit('remuser', _user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'rename': {
|
|
||||||
let selfrename = false;
|
|
||||||
let oldusername: string | null = null;
|
|
||||||
// We've been renamed
|
|
||||||
if (msgArr[1] === '0') {
|
|
||||||
selfrename = true;
|
|
||||||
oldusername = this.username;
|
|
||||||
// msgArr[2] is the status of the rename
|
|
||||||
// Anything other than 0 is an error, however the server will still rename us to a guest name
|
|
||||||
switch (msgArr[2]) {
|
|
||||||
case '1':
|
|
||||||
// The username we wanted was taken
|
|
||||||
this.publicEmitter.emit('renamestatus', 'taken');
|
|
||||||
break;
|
|
||||||
case '2':
|
|
||||||
// The username we wanted was invalid
|
|
||||||
this.publicEmitter.emit('renamestatus', 'invalid');
|
|
||||||
break;
|
|
||||||
case '3':
|
|
||||||
// The username we wanted is blacklisted
|
|
||||||
this.publicEmitter.emit('renamestatus', 'blacklisted');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
this.username = msgArr[3];
|
|
||||||
} else oldusername = msgArr[2];
|
|
||||||
let _user = this.users.find((u) => u.username === oldusername);
|
|
||||||
if (_user) {
|
|
||||||
_user.username = msgArr[3];
|
|
||||||
}
|
|
||||||
this.publicEmitter.emit('rename', oldusername!, msgArr[3], selfrename);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'turn': {
|
|
||||||
// Reset all turn data
|
|
||||||
for (let user of this.users) user.turn = -1;
|
|
||||||
let queuedUsers = parseInt(msgArr[2]);
|
|
||||||
if (queuedUsers === 0) {
|
|
||||||
this.publicEmitter.emit('turn', {
|
|
||||||
user: null,
|
|
||||||
queue: [],
|
|
||||||
turnTime: null,
|
|
||||||
queueTime: null
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let currentTurn = this.users.find((u) => u.username === msgArr[3])!;
|
|
||||||
currentTurn.turn = 0;
|
|
||||||
let queue: User[] = [];
|
|
||||||
if (queuedUsers > 1) {
|
|
||||||
for (let i = 1; i < queuedUsers; i++) {
|
|
||||||
let user = this.users.find((u) => u.username === msgArr[i + 3])!;
|
|
||||||
queue.push(user);
|
|
||||||
user.turn = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.publicEmitter.emit('turn', {
|
|
||||||
user: currentTurn,
|
|
||||||
queue: queue,
|
|
||||||
turnTime: currentTurn.username === this.username ? parseInt(msgArr[1]) : null,
|
|
||||||
queueTime: queue.some((u) => u.username === this.username) ? parseInt(msgArr[msgArr.length - 1]) : null
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'vote': {
|
|
||||||
switch (msgArr[1]) {
|
|
||||||
case '0':
|
|
||||||
// Vote started
|
|
||||||
case '1':
|
|
||||||
// Vote updated
|
|
||||||
let timeToEnd = parseInt(msgArr[2]);
|
|
||||||
let yesVotes = parseInt(msgArr[3]);
|
|
||||||
let noVotes = parseInt(msgArr[4]);
|
|
||||||
// Some server implementations dont send data for status 0, and some do
|
|
||||||
if (Number.isNaN(timeToEnd) || Number.isNaN(yesVotes) || Number.isNaN(noVotes)) return;
|
|
||||||
this.voteStatus = {
|
|
||||||
timeToEnd: timeToEnd,
|
|
||||||
yesVotes: yesVotes,
|
|
||||||
noVotes: noVotes
|
|
||||||
};
|
|
||||||
this.publicEmitter.emit('vote', this.voteStatus);
|
|
||||||
break;
|
|
||||||
case '2':
|
|
||||||
// Vote ended
|
|
||||||
this.voteStatus = null;
|
|
||||||
this.publicEmitter.emit('voteend');
|
|
||||||
break;
|
|
||||||
case '3':
|
|
||||||
// Cooldown
|
|
||||||
this.publicEmitter.emit('votecd', parseInt(msgArr[2]));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// auth stuff
|
|
||||||
case 'auth': {
|
|
||||||
this.publicEmitter.emit('auth', msgArr[1]);
|
|
||||||
this.auth = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'login': {
|
|
||||||
if (msgArr[1] === "1") {
|
|
||||||
this.rank = Rank.Registered;
|
|
||||||
this.publicEmitter.emit('login', Rank.Registered, new Permissions(0));
|
|
||||||
}
|
|
||||||
this.publicEmitter.emit('accountlogin', msgArr[1] === "1");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'admin': {
|
|
||||||
switch (msgArr[1]) {
|
|
||||||
case '0': {
|
|
||||||
// Login
|
|
||||||
switch (msgArr[2]) {
|
|
||||||
case '0':
|
|
||||||
this.publicEmitter.emit('badpw');
|
|
||||||
return;
|
|
||||||
case '1':
|
|
||||||
this.perms.set(65535);
|
|
||||||
this.rank = Rank.Admin;
|
|
||||||
break;
|
|
||||||
case '3':
|
|
||||||
this.perms.set(parseInt(msgArr[3]));
|
|
||||||
this.rank = Rank.Moderator;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
this.publicEmitter.emit('login', this.rank, this.perms);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '19': {
|
|
||||||
// IP
|
|
||||||
this.internalEmitter.emit('ip', msgArr[2], msgArr[3]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '2': {
|
|
||||||
// QEMU
|
|
||||||
this.internalEmitter.emit('qemu', msgArr[2]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onWindowResize(e: Event) {
|
|
||||||
if (!this.connectedToVM) return;
|
|
||||||
// If the canvas is the same size as the screen, don't bother redrawing
|
|
||||||
if (window.innerWidth >= this.actualScreenSize.width && this.canvas.width === this.actualScreenSize.width) return;
|
|
||||||
if (this.actualScreenSize.width === this.canvasScale.width && this.actualScreenSize.height === this.canvasScale.height) {
|
|
||||||
this.unscaledCtx.drawImage(this.canvas, 0, 0);
|
|
||||||
}
|
|
||||||
this.recalculateCanvasScale(this.actualScreenSize.width, this.actualScreenSize.height);
|
|
||||||
this.canvas.width = this.canvasScale.width;
|
|
||||||
this.canvas.height = this.canvasScale.height;
|
|
||||||
this.ctx.drawImage(this.unscaledCanvas, 0, 0, this.actualScreenSize.width, this.actualScreenSize.height, 0, 0, this.canvas.width, this.canvas.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
private recalculateCanvasScale(width: number, height: number) {
|
|
||||||
this.actualScreenSize.width = width;
|
|
||||||
this.actualScreenSize.height = height;
|
|
||||||
// If the screen is bigger than the canvas, don't downscale
|
|
||||||
if (window.innerWidth >= this.actualScreenSize.width) {
|
|
||||||
this.canvasScale.width = this.actualScreenSize.width;
|
|
||||||
this.canvasScale.height = this.actualScreenSize.height;
|
|
||||||
} else {
|
|
||||||
// If the canvas is bigger than the screen, downscale
|
|
||||||
this.canvasScale.width = window.innerWidth;
|
|
||||||
this.canvasScale.height = (window.innerWidth / this.actualScreenSize.width) * this.actualScreenSize.height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async WaitForOpen() {
|
|
||||||
return new Promise<void>((res) => {
|
|
||||||
// TODO: should probably reject on close
|
|
||||||
let unsub = this.onInternal('open', () => {
|
|
||||||
unsub();
|
|
||||||
res();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends a message to the server
|
|
||||||
send(...args: StringLike[]) {
|
|
||||||
let guacElements = [...args].map((el) => {
|
|
||||||
// This catches cases where the thing already is a string
|
|
||||||
if (typeof el == 'string') return el as string;
|
|
||||||
return el.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.socket.send(Guacutils.encode(...guacElements));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a list of all VMs
|
|
||||||
list(): Promise<VM[]> {
|
|
||||||
return new Promise((res, rej) => {
|
|
||||||
let u = this.onInternal('list', (list: string[]) => {
|
|
||||||
u();
|
|
||||||
let vms: VM[] = [];
|
|
||||||
for (let i = 0; i < list.length; i += 3) {
|
|
||||||
let th = new Image();
|
|
||||||
th.src = 'data:image/jpeg;base64,' + list[i + 2];
|
|
||||||
vms.push({
|
|
||||||
url: this.url,
|
|
||||||
id: list[i],
|
|
||||||
displayName: list[i + 1],
|
|
||||||
thumbnail: th
|
|
||||||
});
|
|
||||||
}
|
|
||||||
res(vms);
|
|
||||||
});
|
|
||||||
this.send('list');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to a node
|
|
||||||
connect(id: string, username: string | null = null): Promise<boolean> {
|
|
||||||
return new Promise((res) => {
|
|
||||||
let u = this.onInternal('connect', (success: boolean) => {
|
|
||||||
u();
|
|
||||||
res(success);
|
|
||||||
});
|
|
||||||
if (username === null) this.send('rename');
|
|
||||||
else this.send('rename', username);
|
|
||||||
this.send('connect', id);
|
|
||||||
this.node = id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the connection
|
|
||||||
close() {
|
|
||||||
this.connectedToVM = false;
|
|
||||||
|
|
||||||
// call all unsubscribe callbacks explicitly
|
|
||||||
for (let cb of this.unsubscribeCallbacks) {
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
this.unsubscribeCallbacks = [];
|
|
||||||
|
|
||||||
if (this.socket.readyState === WebSocket.OPEN) this.socket.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get users
|
|
||||||
getUsers(): User[] {
|
|
||||||
// Return a copy of the array
|
|
||||||
return this.users.slice();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send a chat message
|
|
||||||
chat(message: string) {
|
|
||||||
this.send('chat', message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rename
|
|
||||||
rename(username: string | null = null) {
|
|
||||||
if (username) this.send('rename', username);
|
|
||||||
else this.send('rename');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take or drop turn
|
|
||||||
turn(taketurn: boolean) {
|
|
||||||
this.send('turn', taketurn ? '1' : '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send mouse instruction
|
|
||||||
sendmouse(_x: number, _y: number, mask: number) {
|
|
||||||
let x = Math.round((_x / this.canvas.width) * this.actualScreenSize.width);
|
|
||||||
let y = Math.round((_y / this.canvas.height) * this.actualScreenSize.height);
|
|
||||||
this.send('mouse', x, y, mask);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send key
|
|
||||||
key(keysym: number, down: boolean) {
|
|
||||||
this.send('key', keysym, down ? '1' : '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get vote status
|
|
||||||
getVoteStatus(): VoteStatus | null {
|
|
||||||
return this.voteStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start a vote, or vote
|
|
||||||
vote(vote: boolean) {
|
|
||||||
this.send('vote', vote ? '1' : '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to login using the specified password
|
|
||||||
login(password: string) {
|
|
||||||
this.send('admin', AdminOpcode.Login, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Admin commands */
|
|
||||||
|
|
||||||
// Restore
|
|
||||||
restore() {
|
|
||||||
if (!this.node) return;
|
|
||||||
this.send('admin', AdminOpcode.Restore, this.node!);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reboot
|
|
||||||
reboot() {
|
|
||||||
if (!this.node) return;
|
|
||||||
this.send('admin', AdminOpcode.Reboot, this.node!);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear turn queue
|
|
||||||
clearQueue() {
|
|
||||||
if (!this.node) return;
|
|
||||||
this.send('admin', AdminOpcode.ClearTurns, this.node!);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bypass turn
|
|
||||||
bypassTurn() {
|
|
||||||
this.send('admin', AdminOpcode.BypassTurn);
|
|
||||||
}
|
|
||||||
|
|
||||||
// End turn
|
|
||||||
endTurn(user: string) {
|
|
||||||
this.send('admin', AdminOpcode.EndTurn, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ban
|
|
||||||
ban(user: string) {
|
|
||||||
this.send('admin', AdminOpcode.BanUser, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kick
|
|
||||||
kick(user: string) {
|
|
||||||
this.send('admin', AdminOpcode.KickUser, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rename user
|
|
||||||
renameUser(oldname: string, newname: string) {
|
|
||||||
this.send('admin', AdminOpcode.RenameUser, oldname, newname);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mute user
|
|
||||||
mute(user: string, state: MuteState) {
|
|
||||||
this.send('admin', AdminOpcode.MuteUser, user, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab IP
|
|
||||||
getip(user: string) {
|
|
||||||
if (this.users.find((u) => u.username === user) === undefined) return false;
|
|
||||||
return new Promise<string>((res) => {
|
|
||||||
let unsubscribe = this.onInternal('ip', (username: string, ip: string) => {
|
|
||||||
if (username !== user) return;
|
|
||||||
unsubscribe();
|
|
||||||
res(ip);
|
|
||||||
});
|
|
||||||
this.send('admin', AdminOpcode.GetIP, user);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// QEMU Monitor
|
|
||||||
qemuMonitor(cmd: string) {
|
|
||||||
return new Promise<string>((res) => {
|
|
||||||
let unsubscribe = this.onInternal('qemu', (output) => {
|
|
||||||
unsubscribe();
|
|
||||||
res(output);
|
|
||||||
});
|
|
||||||
this.send('admin', AdminOpcode.MonitorCommand, this.node!, cmd);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// XSS
|
|
||||||
xss(msg: string) {
|
|
||||||
this.send('admin', AdminOpcode.ChatXSS, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force vote
|
|
||||||
forceVote(result: boolean) {
|
|
||||||
this.send('admin', AdminOpcode.ForceVote, result ? '1' : '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle turns
|
|
||||||
turns(enabled: boolean) {
|
|
||||||
this.send('admin', AdminOpcode.ToggleTurns, enabled ? '1' : '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Indefinite turn
|
|
||||||
indefiniteTurn() {
|
|
||||||
this.send('admin', AdminOpcode.IndefiniteTurn);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide screen
|
|
||||||
hideScreen(hidden: boolean) {
|
|
||||||
this.send('admin', AdminOpcode.HideScreen, hidden ? '1' : '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login to account
|
|
||||||
loginAccount(token: string) {
|
|
||||||
this.send('login', token);
|
|
||||||
}
|
|
||||||
|
|
||||||
usesAccountAuth() {
|
|
||||||
return this.auth;
|
|
||||||
}
|
|
||||||
|
|
||||||
getNode() {
|
|
||||||
return this.node;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onInternal<E extends keyof CollabVMClientPrivateEvents>(event: E, callback: CollabVMClientPrivateEvents[E]): Unsubscribe {
|
|
||||||
return this.internalEmitter.on(event, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
on<E extends keyof CollabVMClientEvents>(event: E, callback: CollabVMClientEvents[E]): Unsubscribe {
|
|
||||||
let unsub = this.publicEmitter.on(event, callback);
|
|
||||||
this.unsubscribeCallbacks.push(unsub);
|
|
||||||
return unsub;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
export function decode(string: string): string[] {
|
|
||||||
let pos = -1;
|
|
||||||
let sections = [];
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
let len = string.indexOf('.', pos + 1);
|
|
||||||
|
|
||||||
if (len === -1) break;
|
|
||||||
|
|
||||||
pos = parseInt(string.slice(pos + 1, len)) + len + 1;
|
|
||||||
|
|
||||||
// don't allow funky protocol length
|
|
||||||
if (pos > string.length) return [];
|
|
||||||
|
|
||||||
sections.push(string.slice(len + 1, pos));
|
|
||||||
|
|
||||||
const sep = string.slice(pos, pos + 1);
|
|
||||||
|
|
||||||
if (sep === ',') continue;
|
|
||||||
else if (sep === ';') break;
|
|
||||||
// Invalid data.
|
|
||||||
else return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return sections;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function encode(...string: string[]): string {
|
|
||||||
let command = '';
|
|
||||||
|
|
||||||
for (let i = 0; i < string.length; i++) {
|
|
||||||
let current = string[i];
|
|
||||||
command += current.toString().length + '.' + current;
|
|
||||||
command += i < string.length - 1 ? ',' : ';';
|
|
||||||
}
|
|
||||||
return command;
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
enum MuteState {
|
|
||||||
Temp = 0,
|
|
||||||
Perma = 1,
|
|
||||||
Unmuted = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MuteState;
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
export class Permissions {
|
|
||||||
restore: boolean = false;
|
|
||||||
reboot: boolean = false;
|
|
||||||
ban: boolean = false;
|
|
||||||
forcevote: boolean = false;
|
|
||||||
mute: boolean = false;
|
|
||||||
kick: boolean = false;
|
|
||||||
bypassturn: boolean = false;
|
|
||||||
rename: boolean = false;
|
|
||||||
grabip: boolean = false;
|
|
||||||
xss: boolean = false;
|
|
||||||
|
|
||||||
constructor(mask: number) {
|
|
||||||
this.set(mask);
|
|
||||||
}
|
|
||||||
|
|
||||||
set(mask: number) {
|
|
||||||
this.restore = (mask & 1) !== 0;
|
|
||||||
this.reboot = (mask & 2) !== 0;
|
|
||||||
this.ban = (mask & 4) !== 0;
|
|
||||||
this.forcevote = (mask & 8) !== 0;
|
|
||||||
this.mute = (mask & 16) !== 0;
|
|
||||||
this.kick = (mask & 32) !== 0;
|
|
||||||
this.bypassturn = (mask & 64) !== 0;
|
|
||||||
this.rename = (mask & 128) !== 0;
|
|
||||||
this.grabip = (mask & 256) !== 0;
|
|
||||||
this.xss = (mask & 512) !== 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum Rank {
|
|
||||||
Unregistered = 0,
|
|
||||||
Registered = 1,
|
|
||||||
Admin = 2,
|
|
||||||
Moderator = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
// All used admin opcodes as a enum
|
|
||||||
export enum AdminOpcode {
|
|
||||||
Login = 2,
|
|
||||||
MonitorCommand = 5,
|
|
||||||
Restore = 8,
|
|
||||||
Reboot = 10,
|
|
||||||
BanUser = 12,
|
|
||||||
ForceVote = 13,
|
|
||||||
MuteUser = 14,
|
|
||||||
KickUser = 15,
|
|
||||||
EndTurn = 16,
|
|
||||||
ClearTurns = 17,
|
|
||||||
RenameUser = 18,
|
|
||||||
GetIP = 19,
|
|
||||||
BypassTurn = 20,
|
|
||||||
ChatXSS = 21,
|
|
||||||
ToggleTurns = 22,
|
|
||||||
IndefiniteTurn = 23,
|
|
||||||
HideScreen = 24
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { User } from './User.js';
|
|
||||||
|
|
||||||
export default interface TurnStatus {
|
|
||||||
// The user currently taking their turn
|
|
||||||
user: User | null;
|
|
||||||
// The users in the turn queue
|
|
||||||
queue: User[];
|
|
||||||
// Amount of time left in the turn. Null unless the user is taking their turn
|
|
||||||
turnTime: number | null;
|
|
||||||
// Amount of time until the user gets their turn. Null unless the user is in the queue
|
|
||||||
queueTime: number | null;
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import { Rank } from './Permissions.js';
|
|
||||||
|
|
||||||
export class User {
|
|
||||||
username: string;
|
|
||||||
rank: Rank;
|
|
||||||
// -1 means not in the turn queue, 0 means the current turn, anything else is the position in the queue
|
|
||||||
turn: number;
|
|
||||||
|
|
||||||
constructor(username: string, rank: Rank = Rank.Unregistered) {
|
|
||||||
this.username = username;
|
|
||||||
this.rank = rank;
|
|
||||||
this.turn = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
export default interface VM {
|
|
||||||
url: string;
|
|
||||||
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
displayName: string;
|
|
||||||
|
|
||||||
thumbnail: HTMLImageElement;
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
export default interface VoteStatus {
|
|
||||||
timeToEnd: number;
|
|
||||||
yesVotes: number;
|
|
||||||
noVotes: number;
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
function maskContains(mask: number, bit: number): boolean {
|
|
||||||
return (mask & bit) == bit;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Mouse {
|
|
||||||
left: boolean = false;
|
|
||||||
middle: boolean = false;
|
|
||||||
right: boolean = false;
|
|
||||||
scrollDown: boolean = false;
|
|
||||||
scrollUp: boolean = false;
|
|
||||||
x: number = 0;
|
|
||||||
y: number = 0;
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
makeMask() {
|
|
||||||
var mask = 0;
|
|
||||||
if (this.left) mask |= 1;
|
|
||||||
if (this.middle) mask |= 2;
|
|
||||||
if (this.right) mask |= 4;
|
|
||||||
if (this.scrollUp) mask |= 8;
|
|
||||||
if (this.scrollDown) mask |= 16;
|
|
||||||
return mask;
|
|
||||||
}
|
|
||||||
|
|
||||||
initFromMouseEvent(e: MouseEvent) {
|
|
||||||
this.left = maskContains(e.buttons, 1);
|
|
||||||
this.right = maskContains(e.buttons, 2);
|
|
||||||
this.middle = maskContains(e.buttons, 4);
|
|
||||||
|
|
||||||
this.x = e.offsetX;
|
|
||||||
this.y = e.offsetY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't think there's a good way of shoehorning this in processEvent so ..
|
|
||||||
// (I guess could union e to be MouseEvent|WheelEvent and put this in there, but it'd be a
|
|
||||||
// completely unnesscary runtime check for a one-event situation, so having it be seperate
|
|
||||||
// and even call the MouseEvent implementation is more than good enough)
|
|
||||||
initFromWheelEvent(ev: WheelEvent) {
|
|
||||||
this.initFromMouseEvent(ev as MouseEvent);
|
|
||||||
|
|
||||||
// Now do the actual wheel handling
|
|
||||||
if (ev.deltaY < 0) this.scrollUp = true;
|
|
||||||
else if (ev.deltaY > 0) this.scrollDown = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import { Format } from '../format';
|
|
||||||
|
|
||||||
test('a string without any format specifiers in it is unaltered', () => {
|
|
||||||
expect(Format('Hello World')).toBe('Hello World');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('formatting a string works', () => {
|
|
||||||
expect(Format('Hello, {0}!', 'World')).toBe('Hello, World!');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('a cut off format specifier throws', () => {
|
|
||||||
expect(() => Format('a{0', 1)).toThrow('Cutoff/invalid format specifier');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('a malformed format specifier throws', () => {
|
|
||||||
expect(() => Format('a{-0}', 1)).toThrow('Malformed format specifier');
|
|
||||||
expect(() => Format('a{0-}', 1)).toThrow('Malformed format specifier');
|
|
||||||
expect(() => Format('a{0ab}', 1)).toThrow('Malformed format specifier');
|
|
||||||
expect(() => Format('a{ab0ab}', 1)).toThrow('Malformed format specifier');
|
|
||||||
|
|
||||||
// Whitespace is not permitted inside a format specifier
|
|
||||||
expect(() => Format('a{0 }', 1)).toThrow('Whitespace inside format specifier');
|
|
||||||
expect(() => Format('a{ 0}', 1)).toThrow('Whitespace inside format specifier');
|
|
||||||
expect(() => Format('a{ 0 }', 1)).toThrow('Whitespace inside format specifier');
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a OOB format specifier doesn't work", () => {
|
|
||||||
expect(() => Format('a {37}', 1)).toThrow('Argument index out of bounds');
|
|
||||||
});
|
|
||||||
Loading…
Reference in New Issue
Block a user