diff --git a/src/html/index.html b/src/html/index.html index 10d0202..88a73f2 100644 --- a/src/html/index.html +++ b/src/html/index.html @@ -257,6 +257,7 @@ + diff --git a/src/ts/fallbackLanguage.ts b/src/ts/fallbackLanguage.ts index 01dcff3..f1586f8 100644 --- a/src/ts/fallbackLanguage.ts +++ b/src/ts/fallbackLanguage.ts @@ -61,6 +61,8 @@ const fallbackLanguage : Language = { "kAdminVMButtons_ClearTurnQueue": "Clear Turn Queue", "kAdminVMButtons_BypassTurn": "Bypass Turn", "kAdminVMButtons_IndefiniteTurn": "Indefinite Turn", + "kAdminVMButtons_GhostTurnOn": "Ghost Turn (On)", + "kAdminVMButtons_GhostTurnOff": "Ghost Turn (Off)", "kAdminVMButtons_Ban": "Ban", "kAdminVMButtons_Kick": "Kick", diff --git a/src/ts/i18n.ts b/src/ts/i18n.ts index 48db09d..0a2c2ab 100644 --- a/src/ts/i18n.ts +++ b/src/ts/i18n.ts @@ -62,6 +62,8 @@ export enum I18nStringKey { kAdminVMButtons_ClearTurnQueue = 'kAdminVMButtons_ClearTurnQueue', kAdminVMButtons_BypassTurn = 'kAdminVMButtons_BypassTurn', kAdminVMButtons_IndefiniteTurn = 'kAdminVMButtons_IndefiniteTurn', + kAdminVMButtons_GhostTurnOn = 'kAdminVMButtons_GhostTurnOn', + kAdminVMButtons_GhostTurnOff = 'kAdminVMButtons_GhostTurnOff', kAdminVMButtons_Ban = 'kAdminVMButtons_Ban', kAdminVMButtons_Kick = 'kAdminVMButtons_Kick', @@ -272,6 +274,7 @@ export class I18n { clearQueueBtnText: I18nStringKey.kAdminVMButtons_ClearTurnQueue, bypassTurnBtnText: I18nStringKey.kAdminVMButtons_BypassTurn, indefTurnBtnText: I18nStringKey.kAdminVMButtons_IndefiniteTurn, + ghostTurnBtnText: I18nStringKey.kAdminVMButtons_GhostTurnOff, // Account modal accountLoginUsernameLabel: I18nStringKey.kGeneric_Username, diff --git a/src/ts/main.ts b/src/ts/main.ts index 1e4e566..d216f0e 100644 --- a/src/ts/main.ts +++ b/src/ts/main.ts @@ -70,6 +70,8 @@ const elements = { forceVoteYesBtn: document.getElementById('forceVoteYesBtn') as HTMLButtonElement, forceVoteNoBtn: document.getElementById('forceVoteNoBtn') as HTMLButtonElement, indefTurnBtn: document.getElementById('indefTurnBtn') as HTMLButtonElement, + ghostTurnBtn: document.getElementById('ghostTurnBtn') as HTMLButtonElement, + ghostTurnBtnText: document.getElementById('ghostTurnBtnText') as HTMLSpanElement, qemuMonitorInput: document.getElementById('qemuMonitorInput') as HTMLInputElement, qemuMonitorSendBtn: document.getElementById('qemuMonitorSendBtn') as HTMLButtonElement, qemuMonitorOutput: document.getElementById('qemuMonitorOutput') as HTMLTextAreaElement, @@ -482,6 +484,7 @@ function closeVM() { elements.clearQueueBtn.style.display = 'none'; elements.qemuMonitorBtn.style.display = 'none'; elements.indefTurnBtn.style.display = 'none'; + elements.ghostTurnBtn.style.display = 'none'; elements.xssCheckboxContainer.style.display = 'none'; elements.forceVotePanel.style.display = 'none'; elements.voteResetPanel.style.display = 'none'; @@ -813,6 +816,7 @@ function onLogin(_rank: Rank, _perms: Permissions) { if (_rank === Rank.Admin) { elements.qemuMonitorBtn.style.display = 'inline-block'; elements.indefTurnBtn.style.display = 'inline-block'; + elements.ghostTurnBtn.style.display = 'inline-block'; } if (_perms.xss) elements.xssCheckboxContainer.style.display = 'inline-block'; if (_perms.forcevote) elements.forceVotePanel.style.display = 'block'; @@ -876,6 +880,15 @@ elements.forceVoteNoBtn.addEventListener('click', () => VM?.forceVote(false)); elements.forceVoteYesBtn.addEventListener('click', () => VM?.forceVote(true)); elements.indefTurnBtn.addEventListener('click', () => VM?.indefiniteTurn()); + +elements.ghostTurnBtn.addEventListener('click', () => { + w.collabvm.ghostTurn = !w.collabvm.ghostTurn; + if (w.collabvm.ghostTurn) + elements.ghostTurnBtnText.innerText = TheI18n.GetString(I18nStringKey.kAdminVMButtons_GhostTurnOn); + else + elements.ghostTurnBtnText.innerText = TheI18n.GetString(I18nStringKey.kAdminVMButtons_GhostTurnOff); +}); + async function sendQEMUCommand() { if (!elements.qemuMonitorInput.value) return; let cmd = elements.qemuMonitorInput.value; @@ -1212,7 +1225,8 @@ w.collabvm = { closeVM: closeVM, loadList: loadList, multicollab: multicollab, - getVM: () => VM + getVM: () => VM, + ghostTurn: false, }; // Multicollab will stay in the global scope for backwards compatibility w.multicollab = multicollab; @@ -1276,6 +1290,10 @@ document.addEventListener('DOMContentLoaded', async () => { if (darkTheme) elements.toggleThemeBtnText.innerHTML = TheI18n.GetString(I18nStringKey.kSiteButtons_LightMode); else elements.toggleThemeBtnText.innerHTML = TheI18n.GetString(I18nStringKey.kSiteButtons_DarkMode); + if (w.collabvm.ghostTurn) + elements.ghostTurnBtnText.innerText = TheI18n.GetString(I18nStringKey.kAdminVMButtons_GhostTurnOn); + else + elements.ghostTurnBtnText.innerText = TheI18n.GetString(I18nStringKey.kAdminVMButtons_GhostTurnOff); }); // Load theme var _darktheme : boolean; diff --git a/src/ts/protocol/CollabVMClient.ts b/src/ts/protocol/CollabVMClient.ts index dbc5200..bffed35 100644 --- a/src/ts/protocol/CollabVMClient.ts +++ b/src/ts/protocol/CollabVMClient.ts @@ -9,6 +9,7 @@ import GetKeysym from '../keyboard.js'; import VoteStatus from './VoteStatus.js'; import MuteState from './MuteState.js'; import { StringLike } from '../StringLike.js'; +const w = window as any; export interface CollabVMClientEvents { //open: () => void; @@ -98,7 +99,7 @@ export default class CollabVMClient { this.canvas.addEventListener( 'mousedown', (e: MouseEvent) => { - if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return; + if (!this.shouldSendInput()) return; this.mouse.initFromMouseEvent(e); this.sendmouse(this.mouse.x, this.mouse.y, this.mouse.makeMask()); }, @@ -110,7 +111,7 @@ export default class CollabVMClient { this.canvas.addEventListener( 'mouseup', (e: MouseEvent) => { - if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return; + if (!this.shouldSendInput()) return; this.mouse.initFromMouseEvent(e); this.sendmouse(this.mouse.x, this.mouse.y, this.mouse.makeMask()); }, @@ -122,7 +123,7 @@ export default class CollabVMClient { this.canvas.addEventListener( 'mousemove', (e: MouseEvent) => { - if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return; + if (!this.shouldSendInput()) return; this.mouse.initFromMouseEvent(e); this.sendmouse(this.mouse.x, this.mouse.y, this.mouse.makeMask()); }, @@ -135,7 +136,7 @@ export default class CollabVMClient { 'keydown', (e: KeyboardEvent) => { e.preventDefault(); - if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return; + if (!this.shouldSendInput()) return; let keysym = GetKeysym(e.keyCode, e.key, e.location); if (keysym === null) return; this.key(keysym, true); @@ -149,7 +150,7 @@ export default class CollabVMClient { 'keyup', (e: KeyboardEvent) => { e.preventDefault(); - if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return; + if (!this.shouldSendInput()) return; let keysym = GetKeysym(e.keyCode, e.key, e.location); if (keysym === null) return; this.key(keysym, false); @@ -163,7 +164,7 @@ export default class CollabVMClient { 'wheel', (ev: WheelEvent) => { ev.preventDefault(); - if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return; + if (!this.shouldSendInput()) return; this.mouse.initFromWheelEvent(ev); this.sendmouse(this.mouse.x, this.mouse.y, this.mouse.makeMask()); @@ -677,6 +678,10 @@ export default class CollabVMClient { return this.internalEmitter.on(event, callback); } + private shouldSendInput() { + return this.users.find(u => u.username === this.username)?.turn === 0 || (w.collabvm.ghostTurn && this.rank === Rank.Admin); + } + on(event: E, callback: CollabVMClientEvents[E]): Unsubscribe { let unsub = this.publicEmitter.on(event, callback); this.unsubscribeCallbacks.push(unsub); diff --git a/static/lang/en-us.json b/static/lang/en-us.json index 3405815..860e50e 100644 --- a/static/lang/en-us.json +++ b/static/lang/en-us.json @@ -60,6 +60,8 @@ "kAdminVMButtons_ClearTurnQueue": "Clear Turn Queue", "kAdminVMButtons_BypassTurn": "Bypass Turn", "kAdminVMButtons_IndefiniteTurn": "Indefinite Turn", + "kAdminVMButtons_GhostTurnOn": "Ghost Turn (On)", + "kAdminVMButtons_GhostTurnOff": "Ghost Turn (Off)", "kAdminVMButtons_Ban": "Ban", "kAdminVMButtons_Kick": "Kick",