332 lines
12 KiB
TypeScript
332 lines
12 KiB
TypeScript
import CollabVMClient from "./protocol/CollabVMClient.js";
|
|
import VM from "./protocol/VM.js";
|
|
import { Config } from "../../Config.js";
|
|
import { Rank } from "./protocol/Permissions.js";
|
|
import { User } from "./protocol/User.js";
|
|
import TurnStatus from "./protocol/TurnStatus.js";
|
|
|
|
// Elements
|
|
const w = window as any;
|
|
const elements = {
|
|
vmlist: document.getElementById('vmlist') as HTMLDivElement,
|
|
vmview: document.getElementById('vmview') as HTMLDivElement,
|
|
vmDisplay: document.getElementById('vmDisplay') as HTMLDivElement,
|
|
homeBtn: document.getElementById('homeBtn') as HTMLAnchorElement,
|
|
chatList: document.getElementById('chatList') as HTMLTableSectionElement,
|
|
chatListDiv: document.getElementById('chatListDiv') as HTMLDivElement,
|
|
userlist: document.getElementById('userlist') as HTMLTableSectionElement,
|
|
onlineusercount: document.getElementById("onlineusercount") as HTMLSpanElement,
|
|
username: document.getElementById("username") as HTMLSpanElement,
|
|
chatinput: document.getElementById("chat-input") as HTMLInputElement,
|
|
sendChatBtn: document.getElementById("sendChatBtn") as HTMLButtonElement,
|
|
changeUsernameBtn: document.getElementById("changeUsernameBtn") as HTMLButtonElement,
|
|
turnBtnText: document.getElementById("turnBtnText") as HTMLSpanElement,
|
|
turnstatus: document.getElementById("turnstatus") as HTMLParagraphElement,
|
|
}
|
|
var expectedClose = false;
|
|
var turn = -1;
|
|
// Listed VMs
|
|
const vms : VM[] = [];
|
|
const cards : HTMLDivElement[] = [];
|
|
const users : {
|
|
user : User,
|
|
element : HTMLTableRowElement
|
|
}[] = [];
|
|
|
|
// Active VM
|
|
var VM : CollabVMClient | null = null;
|
|
|
|
function multicollab(url : string) {
|
|
return new Promise<void>(async (res, rej) => {
|
|
// Create the client
|
|
var client = new CollabVMClient(url);
|
|
// Wait for the client to open
|
|
await new Promise<void>(res => client.on('open', () => res()));
|
|
// Get the list of VMs
|
|
var list = await client.list();
|
|
// Get the number of online users
|
|
var online = client.getUsers().length;
|
|
// Close the client
|
|
client.close();
|
|
// Add to the list
|
|
vms.push(...list);
|
|
// Add to the DOM
|
|
for (var vm of list) {
|
|
var div = document.createElement('div');
|
|
div.classList.add("col-sm-5", "col-md-3");
|
|
var card = document.createElement('div');
|
|
card.classList.add("card", "bg-dark", "text-light");
|
|
card.setAttribute("data-cvm-node", vm.id);
|
|
card.addEventListener('click', () => openVM(vm));
|
|
vm.thumbnail.classList.add("card-img-top");
|
|
var cardBody = document.createElement('div');
|
|
cardBody.classList.add("card-body");
|
|
var cardTitle = document.createElement('h5');
|
|
cardTitle.innerHTML = vm.displayName;
|
|
var usersOnline = document.createElement("span");
|
|
usersOnline.innerHTML = `(<i class="fa-solid fa-users"></i> ${online})`;
|
|
cardBody.appendChild(cardTitle);
|
|
cardBody.appendChild(usersOnline);
|
|
card.appendChild(vm.thumbnail);
|
|
card.appendChild(cardBody);
|
|
div.appendChild(card);
|
|
cards.push(div);
|
|
sortVMList();
|
|
}
|
|
res();
|
|
});
|
|
}
|
|
function openVM(vm : VM) {
|
|
return new Promise<void>(async (res, rej) => {
|
|
// If there's an active VM it must be closed before opening another
|
|
if (VM !== null) return;
|
|
expectedClose = false;
|
|
// Set hash
|
|
location.hash = vm.id;
|
|
// Create the client
|
|
VM = new CollabVMClient(vm.url);
|
|
// Register event listeners
|
|
// An array to keep track of all listeners, and remove them when the VM is closed. Might not be necessary, but it's good practice.
|
|
var listeners : (() => void)[] = [];
|
|
listeners.push(VM!.on('chat', (username, message) => chatMessage(username, message)));
|
|
listeners.push(VM!.on('adduser', (user) => addUser(user)));
|
|
listeners.push(VM!.on('remuser', (user) => remUser(user)));
|
|
listeners.push(VM!.on('rename', (oldname, newname, selfrename) => userRenamed(oldname, newname, selfrename)));
|
|
listeners.push(VM!.on('renamestatus', (status) => {
|
|
switch (status) {
|
|
case 'taken': alert("That username is already taken"); break;
|
|
case 'invalid': alert("Usernames can contain only numbers, letters, spaces, dashes, underscores, and dots, and it must be between 3 and 20 characters."); break;
|
|
case 'blacklisted': alert("That username has been blacklisted."); break;
|
|
}
|
|
}));
|
|
listeners.push(VM!.on('turn', status => turnUpdate(status)));
|
|
listeners.push(VM!.on('close', () => {
|
|
if (!expectedClose) alert("You have been disconnected from the server");
|
|
for (var l of listeners) l();
|
|
closeVM();
|
|
}));
|
|
// Wait for the client to open
|
|
await new Promise<void>(res => VM!.on('open', () => res()));
|
|
// Connect to node
|
|
chatMessage("", vm.id);
|
|
var connected = await VM.connect(vm.id);
|
|
if (!connected) {
|
|
VM.close();
|
|
VM = null;
|
|
rej("Failed to connect to node");
|
|
}
|
|
// Set the title
|
|
document.title = vm.id + " - CollabVM";
|
|
// Append canvas
|
|
elements.vmDisplay.appendChild(VM!.canvas);
|
|
// Switch to the VM view
|
|
elements.vmlist.style.display = "none";
|
|
elements.vmview.style.display = "block";
|
|
});
|
|
}
|
|
|
|
function closeVM() {
|
|
if (VM === null) return;
|
|
expectedClose = true;
|
|
// Close the VM
|
|
VM.close();
|
|
VM = null;
|
|
document.title = "CollabVM";
|
|
turn = -1;
|
|
// Remove the canvas
|
|
elements.vmDisplay.innerHTML = "";
|
|
// Switch to the VM list
|
|
elements.vmlist.style.display = "block";
|
|
elements.vmview.style.display = "none";
|
|
// Clear users
|
|
users.splice(0, users.length);
|
|
elements.userlist.innerHTML = "";
|
|
}
|
|
|
|
function loadList() {
|
|
return new Promise<void>(async res => {
|
|
var p = [];
|
|
for (var url of Config.ServerAddresses) {
|
|
p.push(multicollab(url));
|
|
}
|
|
await Promise.all(p);
|
|
var v = vms.find(v => v.id === window.location.hash.substring(1));
|
|
if (v !== undefined) openVM(v);
|
|
res();
|
|
});
|
|
}
|
|
|
|
function sortVMList() {
|
|
cards.sort(function(a, b) {
|
|
return a.children[0].getAttribute("data-cvm-node")! > b.id ? 1 : -1;
|
|
});
|
|
elements.vmlist.children[0].innerHTML = "";
|
|
cards.forEach((c) => elements.vmlist.children[0].appendChild(c));
|
|
}
|
|
|
|
function sortUserList() {
|
|
users.sort((a, b) => {
|
|
if (a.user.username === w.username && (a.user.turn >= b.user.turn)) return -1;
|
|
if (b.user.username === w.username && (b.user.turn >= a.user.turn)) return 1;
|
|
if (a.user.turn === b.user.turn) return 0;
|
|
if (a.user.turn === -1) return 1;
|
|
if (b.user.turn === -1) return -1;
|
|
if (a.user.turn < b.user.turn) return -1;
|
|
else return 1;
|
|
});
|
|
for (const user of users) {
|
|
elements.userlist.removeChild(user.element);
|
|
elements.userlist.appendChild(user.element);
|
|
}
|
|
}
|
|
|
|
function chatMessage(username : string, message : string) {
|
|
var tr = document.createElement('tr');
|
|
var td = document.createElement('td');
|
|
// System message
|
|
if (username === "") td.innerHTML = message;
|
|
else {
|
|
var user = VM!.getUsers().find(u => u.username === username);
|
|
var rank;
|
|
if (user !== undefined) rank = user.rank;
|
|
else rank = Rank.Unregistered;
|
|
var userclass;
|
|
var msgclass;
|
|
switch (rank) {
|
|
case Rank.Unregistered:
|
|
userclass = "chat-username-unregistered";
|
|
msgclass = "chat-unregistered";
|
|
break;
|
|
case Rank.Admin:
|
|
userclass = "chat-username-admin";
|
|
msgclass = "chat-admin";
|
|
break;
|
|
case Rank.Moderator:
|
|
userclass = "chat-username-moderator";
|
|
msgclass = "chat-moderator";
|
|
break;
|
|
}
|
|
tr.classList.add(msgclass);
|
|
td.innerHTML = `<b class="${userclass}">${username}▸</b> ${message}`;
|
|
// hacky way to allow scripts
|
|
Array.prototype.slice.call(td.children).forEach((curr) => {
|
|
if (curr.nodeName === "SCRIPT") {
|
|
eval(curr.text)
|
|
}
|
|
});
|
|
tr.appendChild(td);
|
|
elements.chatList.appendChild(tr);
|
|
elements.chatListDiv.scrollTop = elements.chatListDiv.scrollHeight;
|
|
}
|
|
}
|
|
|
|
function addUser(user : User) {
|
|
var olduser = users.find(u => u.user === user);
|
|
if (olduser !== undefined) elements.userlist.removeChild(olduser.element);
|
|
var tr = document.createElement('tr');
|
|
tr.setAttribute("data-cvm-turn", "-1");
|
|
var td = document.createElement('td');
|
|
td.innerHTML = user.username;
|
|
switch (user.rank) {
|
|
case Rank.Admin:
|
|
tr.classList.add("user-admin");
|
|
break;
|
|
case Rank.Moderator:
|
|
tr.classList.add("user-moderator");
|
|
break;
|
|
case Rank.Unregistered:
|
|
tr.classList.add("user-unregistered");
|
|
break;
|
|
}
|
|
if (user.username === w.username)
|
|
tr.classList.add("user-current");
|
|
tr.appendChild(td);
|
|
elements.userlist.appendChild(tr);
|
|
if (olduser !== undefined) olduser.element = tr;
|
|
else users.push({user: user, element: tr});
|
|
elements.onlineusercount.innerHTML = VM!.getUsers().length.toString();
|
|
}
|
|
|
|
function remUser(user : User) {
|
|
var olduser = users.findIndex(u => u.user === user);
|
|
if (olduser !== undefined) elements.userlist.removeChild(users[olduser].element);
|
|
elements.onlineusercount.innerHTML = VM!.getUsers().length.toString();
|
|
users.splice(olduser, 1);
|
|
}
|
|
|
|
function userRenamed(oldname : string, newname : string, selfrename : boolean) {
|
|
var user = users.find(u => u.user.username === newname);
|
|
if (user) {
|
|
user.element.children[0].innerHTML = newname;
|
|
}
|
|
if (selfrename) {
|
|
w.username = newname;
|
|
elements.username.innerText = newname;
|
|
localStorage.setItem("username", newname);
|
|
}
|
|
}
|
|
|
|
function turnUpdate(status : TurnStatus) {
|
|
// Clear all turn data
|
|
turn = -1;
|
|
for (const user of users) {
|
|
user.element.classList.remove("user-turn", "user-waiting");
|
|
user.element.setAttribute("data-cvm-turn", "-1");
|
|
}
|
|
elements.turnBtnText.innerHTML = "Take Turn";
|
|
if (status.user !== null) {
|
|
var el = users.find(u => u.user === status.user)!.element;
|
|
el!.classList.add("user-turn");
|
|
el!.setAttribute("data-cvm-turn", "0");
|
|
}
|
|
for (const user of status.queue) {
|
|
var el = users.find(u => u.user === user)!.element;
|
|
el!.classList.add("user-waiting");
|
|
el.setAttribute("data-cvm-turn", status.queue.indexOf(user).toString(10))
|
|
}
|
|
if (status.user?.username === w.username) {
|
|
turn = 0;
|
|
elements.turnBtnText.innerHTML = "End Turn";
|
|
}
|
|
if (status.queue.some(u => u.username === w.username)) {
|
|
turn = status.queue.findIndex(u => u.username === w.username) + 1;
|
|
elements.turnBtnText.innerHTML = "End Turn";
|
|
}
|
|
sortUserList();
|
|
}
|
|
|
|
function sendChat() {
|
|
if (VM === null) return;
|
|
VM.chat(elements.chatinput.value);
|
|
elements.chatinput.value = "";
|
|
}
|
|
|
|
// Bind list buttons
|
|
elements.homeBtn.addEventListener('click', () => closeVM());
|
|
|
|
// Bind VM view buttons
|
|
elements.sendChatBtn.addEventListener('click', sendChat);
|
|
elements.chatinput.addEventListener('keypress', (e) => {
|
|
if (e.key === "Enter") sendChat();
|
|
});
|
|
elements.changeUsernameBtn.addEventListener('click', () => {
|
|
var newname = prompt("Enter new username, or leave blank to be assigned a guest username", w.username);
|
|
if (newname === w.username) return;
|
|
VM?.rename(newname);
|
|
})
|
|
|
|
// Public API
|
|
w.collabvm = {
|
|
openVM: openVM,
|
|
closeVM: closeVM,
|
|
loadList: loadList,
|
|
multicollab: multicollab
|
|
}
|
|
// Multicollab will stay in the global scope for backwards compatibility
|
|
w.multicollab = multicollab;
|
|
// Same goes for GetAdmin
|
|
// w.GetAdmin = () => VM.admin;
|
|
|
|
// Load all VMs
|
|
loadList();
|