diff --git a/package.json b/package.json index cfea399..3c3acee 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "cvmwebapp", + "name": "collab-vm-1.2-webapp", "version": "2.0.0", - "description": "kill me", + "description": "Official webapp for the CollabVM 1.2.x protocol", "private": true, "scripts": { "build": "parcel build --no-source-maps --dist-dir dist --public-url '.' src/html/index.html", @@ -14,17 +14,25 @@ "author": "Elijah R", "license": "GPL-3.0", "dependencies": { - "@fortawesome/fontawesome-free": "^6.6.0", + "@fortawesome/fontawesome-svg-core": "^6.7.2", + "@fortawesome/free-brands-svg-icons": "^6.7.2", + "@fortawesome/free-regular-svg-icons": "^6.7.2", + "@fortawesome/free-solid-svg-icons": "^6.7.2", + "@fortawesome/vue-fontawesome": "^3.0.8", "@popperjs/core": "^2.11.8", + "@twemoji/api": "^15.1.0", "bootstrap": "^5.3.3", "dayjs": "^1.11.13", "dompurify": "^3.2.4", "msgpackr": "^1.11.2", "nanoevents": "^9.1.0", - "simple-keyboard": "^3.8.14" + "simple-keyboard": "^3.8.14", + "vue": "^3.5.13" }, "devDependencies": { "@hcaptcha/types": "^1.0.4", + "@parcel/transformer-sass": "2.12.0", + "@parcel/transformer-vue": "2.12.0", "@types/bootstrap": "^5.2.10", "@types/cloudflare-turnstile": "^0.2.2", "@types/dompurify": "^3.0.5", diff --git a/src/assets/NotoColorEmoji.ttf b/src/assets/NotoColorEmoji.ttf deleted file mode 100644 index cf7a47e..0000000 Binary files a/src/assets/NotoColorEmoji.ttf and /dev/null differ diff --git a/src/css/style.css b/src/css/style.css deleted file mode 100644 index b64be47..0000000 --- a/src/css/style.css +++ /dev/null @@ -1,351 +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 .userlist-username, .chat-username-admin, .username-admin { - color: #FF0000 !important; -} - -tr.user-moderator .userlist-username, .chat-username-moderator, .username-moderator { - color: #00FF00 !important; -} - -html[data-bs-theme="dark"] { - tr.user-unregistered .userlist-username, .chat-username-unregistered, .username-unregistered { - color: #b1b1b1 !important; - } - - tr.user-registered .userlist-username, .chat-username-registered, .username-registered { - color: #FFFFFF !important; - } - - tr.user-registered.user-turn .userlist-username, tr.user-registered.user-waiting .userlist-username { - color: #000000 !important; - --bs-table-color: #000000 !important; - } - - tr.user-unregistered.user-turn .userlist-username, tr.user-unregistered.user-waiting .userlist-username { - color: #585858 !important; - --bs-table-color: #585858 !important; - } -} - -html[data-bs-theme="light"] { - tr.user-unregistered .userlist-username, .chat-username-unregistered, .username-unregistered { - color: #6b6b6b !important; - } - - tr.user-registered .userlist-username, .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 .userlist-username { - 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'); -} - -.userlist-flag { - padding-right: 0.5rem; - user-select: none; -} - -.userlist-flag:empty { - display: none; -} diff --git a/src/html/index.html b/src/html/index.html index 2306ad3..007552e 100644 --- a/src/html/index.html +++ b/src/html/index.html @@ -4,10 +4,7 @@ CollabVM - - - - + @@ -17,301 +14,11 @@ + + - - - - - -
-
-
-
-
-

- -
- - - - - - -
- - - - - - - - -
-
-
-
- -
-
-
-
- -
-
-
-
-
-
-
-
- - - - - -
()
-
-
-
-
- - - - -
-
-
- - -
- - -
- -
-
-
-
- +
+ diff --git a/src/scss/colors.scss b/src/scss/colors.scss new file mode 100644 index 0000000..4104106 --- /dev/null +++ b/src/scss/colors.scss @@ -0,0 +1,32 @@ +// Misc +$color_vm_card_outline_hover: #0879fa; +$color_nsfw_warning: #ff0000; + +// Rank +$color_admin: #ff0000; +$color_moderator: #00ff00; + +$color_unregistered: #b1b1b1; +$color_registered: #ffffff; +$color_unregistered_turn_active: #585858; +$color_registered_turn_active: #000000; + +html[data-bs-theme='light'] { + $color_unregistered: #6b6b6b; + $color_registered: #000000; + $color_unregistered_turn_active: $color_unregistered; + $color_registered_turn_active: $color_registered; +} + +// Turn state +$color_has_turn_outline: #2dd5ffbf; +$color_waiting_turn_outline: #f2ff3fbf; + +$color_has_turn_hover: #bacbe6; +$color_waiting_turn_hover: #ece1be; + +$color_has_turn_user_bg: #cfe2ff; +$color_has_turn_user_fg: #000000; + +$color_waiting_turn_user_bg: #fff3cd; +$color_waiting_turn_user_fg: #000000; diff --git a/src/scss/keyboard.scss b/src/scss/keyboard.scss new file mode 100644 index 0000000..72de3f0 --- /dev/null +++ b/src/scss/keyboard.scss @@ -0,0 +1,149 @@ +.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; +} + +.simple-keyboard { + &.osk-main { + max-width: 640px; + background: none; + .hg-row:first-child { + margin-bottom: 8.51px; /* wtf? */ + } + } + + &.osk-arrows { + align-self: flex-end; + background: none; + .hg-row { + justify-content: center; + } + .hg-button { + width: 50px; + flex-grow: 0 !important; + justify-content: center !important; + display: flex !important; + align-items: center !important; + } + } + + &.osk-control { + background: none; + .hg-row:first-child { + margin-bottom: 8.51px; + } + .hg-button { + width: 50px; + flex-grow: 0 !important; + justify-content: center !important; + display: flex !important; + align-items: center !important; + } + } + + &.osk-numpad { + background: none; + width: 160px; + .hg-button { + width: 50px; + justify-content: center; + display: flex; + align-items: center; + } + } + + &.osk-numpadEnd { + width: 50px; + background: none; + margin: 0; + padding: 5px 5px 5px 0; + .hg-button { + align-items: center; + justify-content: center; + display: flex; + &.hg-standardBtn.hg-button-plus { + height: 85px; + } + &.hg-button-enter { + height: 85px; + } + } + } + + .hg-button { + &.selectedButton { + background: rgba(5, 25, 70, 0.53); + color: white; + } + &.emptySpace { + pointer-events: none; + background: none; + border: none; + box-shadow: none; + } + &.hg-functionBtn.hg-button-space { + width: 350px; + } + &.hg-button-depressed { + background-color: #1c4995 !important; + } + } + + &.hg-theme-default { + .hg-button.hg-selectedButton { + background: rgba(5, 25, 70, 0.53); + color: white; + } + } + + &.cvmDark { + .hg-button { + border-bottom: none; + background: rgba(0, 0, 0, 0.5); + color: white; + } + + .hg-button:active { + background: #1c4995; + color: white; + } + + + .simple-keyboard-preview { + background: #1c4995; + } + } + + &.cvmDisabled .hg-button { + border-bottom: none; + pointer-events: none; + background: gray; + color: white; + } +} + +.controlArrows { + display: flex; + align-items: center; + justify-content: space-between; + flex-flow: column; +} + +.numPad { + display: flex; + align-items: flex-end; +} + +@media screen and (max-width: 640px) { + .hg-button:not(:last-child) { + margin-right: 1px !important; + } +} diff --git a/src/scss/main.scss b/src/scss/main.scss new file mode 100644 index 0000000..242c296 --- /dev/null +++ b/src/scss/main.scss @@ -0,0 +1,6 @@ +@use './vendor'; +@use './colors'; +@use './vmlist'; +@use './vmview'; +@use './keyboard'; +@use './navbar'; \ No newline at end of file diff --git a/src/scss/navbar.scss b/src/scss/navbar.scss new file mode 100644 index 0000000..5fe1624 --- /dev/null +++ b/src/scss/navbar.scss @@ -0,0 +1,4 @@ +.language-flag > img { + width: 1.5rem; + height: 1.5rem; +} \ No newline at end of file diff --git a/src/scss/vendor.scss b/src/scss/vendor.scss new file mode 100644 index 0000000..a80505c --- /dev/null +++ b/src/scss/vendor.scss @@ -0,0 +1 @@ +@use '../../node_modules/bootstrap/dist/css/bootstrap.min.css'; \ No newline at end of file diff --git a/src/scss/vmlist.scss b/src/scss/vmlist.scss new file mode 100644 index 0000000..72adfc3 --- /dev/null +++ b/src/scss/vmlist.scss @@ -0,0 +1,25 @@ +@use './colors'; + +.vm-card-col { + padding-bottom: 10px; +} + +.vm-card { + text-decoration: none; +} + +.vm-card:hover { + cursor: pointer; + border-color: colors.$color_vm_card_outline_hover; +} + +.cvm-nsfw { + & img { + filter: blur(40px) !important; + } + + & h5::before { + content: '[NSFW] '; + color: #ff0000; + } +} diff --git a/src/scss/vmview.scss b/src/scss/vmview.scss new file mode 100644 index 0000000..c499a6b --- /dev/null +++ b/src/scss/vmview.scss @@ -0,0 +1,113 @@ +@use './colors'; + +// General + +.vm-display, +.vm-btns { + display: block; + margin-bottom: 10px; +} + +.vm-display, +.vm-btns, +.vm-turn-status, +.vote-reset-panel { + text-align: center; +} + +// Turn state +.vm-display-turn > canvas { + box-shadow: 0 0 9px 0 colors.$color_has_turn_outline; +} + +.vm-display-waiting > canvas { + box-shadow: 0 0 9px 0 colors.$color_waiting_turn_outline; +} + +.user-turn { + background-color: colors.$color_has_turn_user_bg !important; + --bs-table-bg: colors.$color_has_turn_user_bg !important; + --bs-table-bg-state: colors.$color_has_turn_user_bg !important; + + color: colors.$color_has_turn_user_fg; + --bs-table-color: colors.$color_has_turn_user_fg; + + &:hover { + background-color: colors.$color_has_turn_hover !important; + --bs-table-bg: colors.$color_has_turn_hover !important; + --bs-table-bg-state: colors.$color_has_turn_hover !important; + } +} + +.user-waiting { + background-color: colors.$color_waiting_turn_user_bg !important; + --bs-table-bg: colors.$color_waiting_turn_user_bg !important; + --bs-table-bg-state: colors.$color_waiting_turn_user_bg !important; + + color: colors.$color_waiting_turn_user_fg; + --bs-table-color: colors.$color_waiting_turn_user_fg; + + &:hover { + background-color: colors.$color_waiting_turn_hover !important; + --bs-table-bg: colors.$color_waiting_turn_hover !important; + --bs-table-bg-state: colors.$color_waiting_turn_hover !important; + } +} + +// Chat and username tables +.chat-table, +.vm-username-table { + overflow-y: auto; + border: 1px solid #575757; +} + +.chat-table { + height: 30vh; +} + +.vm-username-table { + max-height: 30vh; + + table > thead { + position: sticky; + top: 0; + } +} + +.chat-message-username::after { + content: '▸ '; +} + +.user-current { + font-style: italic; +} + +// Rank colors +.user-unregistered { + color: colors.$color_unregistered !important; + + &.user-turn { + color: colors.$color_unregistered_turn_active !important; + } +} + +.user-registered { + color: colors.$color_registered !important; + + &.user-turn { + color: colors.$color_registered_turn_active !important; + } +} + +.user-moderator { + color: colors.$color_moderator !important; +} + +.user-admin { + color: colors.$color_admin !important; +} + +// Admin +.qemu-monitor-output { + height: 180px; +} diff --git a/src/ts/ThemeManager.ts b/src/ts/ThemeManager.ts new file mode 100644 index 0000000..5bfa540 --- /dev/null +++ b/src/ts/ThemeManager.ts @@ -0,0 +1,26 @@ +export class ThemeManager { + isDarkTheme: boolean; + + constructor() { + // Check if dark theme is set in local storage + if (localStorage.getItem("cvm-dark-theme") !== null) + this.isDarkTheme = localStorage.getItem("cvm-dark-theme") === "1"; + // Otherwise, try to detect the system theme + else if (window.matchMedia('(prefers-color-scheme: dark)').matches) + this.isDarkTheme = true; + else + this.isDarkTheme = false; + + this.setDarkTheme(this.isDarkTheme); + } + + setDarkTheme(dark: boolean) { + this.isDarkTheme = dark; + document.children[0].setAttribute("data-bs-theme", this.isDarkTheme ? "dark" : "light"); + localStorage.setItem("cvm-dark-theme", this.isDarkTheme ? "1" : "0"); + } + + toggleTheme() { + this.setDarkTheme(!this.isDarkTheme); + } +} \ No newline at end of file diff --git a/src/ts/flags.ts b/src/ts/flags.ts new file mode 100644 index 0000000..c1a4c03 --- /dev/null +++ b/src/ts/flags.ts @@ -0,0 +1,6 @@ +import twemoji from '@twemoji/api'; + +export function GetCountryFlag(countryCode: string) { + if (countryCode.length !== 2) throw new Error("Country code must be two characters."); + return twemoji.parse(String.fromCodePoint(...countryCode.toUpperCase().split('').map(char => 127397 + char.charCodeAt(0)))); +} \ No newline at end of file diff --git a/src/ts/i18n.ts b/src/ts/i18n.ts index 64d53f1..430b06a 100644 --- a/src/ts/i18n.ts +++ b/src/ts/i18n.ts @@ -4,106 +4,105 @@ import { Emitter, Unsubscribe, createNanoEvents } from 'nanoevents'; import Config from '../../config.json'; /// All string keys. -export enum I18nStringKey { +export type 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', + 'kGeneric_CollabVM' | + 'kGeneric_Yes' | + 'kGeneric_No' | + 'kGeneric_Ok' | + 'kGeneric_Cancel' | + 'kGeneric_Send' | + 'kGeneric_Understood' | + 'kGeneric_Username' | + 'kGeneric_Password' | + 'kGeneric_Login' | + 'kGeneric_Register' | + 'kGeneric_EMail' | + 'kGeneric_DateOfBirth' | + 'kGeneric_VerificationCode' | + 'kGeneric_Verify' | + 'kGeneric_Update' | + 'kGeneric_Logout' | + + 'kWelcomeModal_Header' | + 'kWelcomeModal_Body' | + + 'kSiteButtons_Home' | + 'kSiteButtons_FAQ' | + 'kSiteButtons_Rules' | + 'kSiteButtons_DarkMode' | + 'kSiteButtons_LightMode' | + 'kSiteButtons_Languages' | + + 'kVM_UsersOnlineText' | + + 'kVM_TurnTimeTimer' | + 'kVM_WaitingTurnTimer' | + 'kVM_VoteCooldownTimer' | + + 'kVM_VoteForResetTitle' | + 'kVM_VoteForResetTimer' | + + 'kVMButtons_TakeTurn' | + 'kVMButtons_EndTurn' | + 'kVMButtons_ChangeUsername' | + 'kVMButtons_Keyboard' | + 'KVMButtons_CtrlAltDel' | + + 'kVMButtons_VoteForReset' | + '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_GhostTurnOn = 'kAdminVMButtons_GhostTurnOn', - kAdminVMButtons_GhostTurnOff = 'kAdminVMButtons_GhostTurnOff', - - kAdminVMButtons_Ban = 'kAdminVMButtons_Ban', - kAdminVMButtons_Kick = 'kAdminVMButtons_Kick', - kAdminVMButtons_TempMute = 'kAdminVMButtons_TempMute', - kAdminVMButtons_IndefMute = 'kAdminVMButtons_IndefMute', - kAdminVMButtons_Unmute = 'kAdminVMButtons_Unmute', - kAdminVMButtons_GetIP = 'kAdminVMButtons_GetIP', + 'kQEMUMonitor' | + 'kAdminVMButtons_PassVote' | + 'kAdminVMButtons_CancelVote' | + + 'kAdminVMButtons_Restore' | + 'kAdminVMButtons_Reboot' | + 'kAdminVMButtons_ClearTurnQueue' | + 'kAdminVMButtons_BypassTurn' | + 'kAdminVMButtons_IndefiniteTurn' | + 'kAdminVMButtons_GhostTurnOn' | + 'kAdminVMButtons_GhostTurnOff' | + + 'kAdminVMButtons_Ban' | + 'kAdminVMButtons_Kick' | + 'kAdminVMButtons_TempMute' | + 'kAdminVMButtons_IndefMute' | + 'kAdminVMButtons_Unmute' | + 'kAdminVMButtons_GetIP' | // prompts - kVMPrompts_AdminChangeUsernamePrompt = 'kVMPrompts_AdminChangeUsernamePrompt', - kVMPrompts_AdminRestoreVMPrompt = 'kVMPrompts_AdminRestoreVMPrompt', - kVMPrompts_EnterNewUsernamePrompt = 'kVMPrompts_EnterNewUsernamePrompt', + 'kVMPrompts_AdminChangeUsernamePrompt' | + 'kVMPrompts_AdminRestoreVMPrompt' | + 'kVMPrompts_EnterNewUsernamePrompt' | // error messages - kError_UnexpectedDisconnection = 'kError_UnexpectedDisconnection', + 'kError_UnexpectedDisconnection' | - kError_UsernameTaken = 'kError_UsernameTaken', - kError_UsernameInvalid = 'kError_UsernameInvalid', - kError_UsernameBlacklisted = 'kError_UsernameBlacklisted', - kError_IncorrectPassword = 'kError_IncorrectPassword', + 'kError_UsernameTaken' | + 'kError_UsernameInvalid' | + 'kError_UsernameBlacklisted' | + '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_HideFlag = 'kAccountModal_HideFlag', - - kAccountModal_VerifyText = 'kAccountModal_VerifyText', - kAccountModal_VerifyPasswordResetText = 'kAccountModal_VerifyPasswordResetText', - kAccountModal_PasswordResetSuccess = 'kAccountModal_PasswordResetSuccess', - kMissingCaptcha = 'kMissingCaptcha', - kPasswordsMustMatch = 'kPasswordsMustMatch', - - kNotLoggedIn = 'kNotLoggedIn', -} + 'kAccountModal_Verify' | + 'kAccountModal_AccountSettings' | + 'kAccountModal_ResetPassword' | + + 'kAccountModal_NewPassword' | + 'kAccountModal_ConfirmNewPassword' | + 'kAccountModal_CurrentPassword' | + 'kAccountModal_ConfirmPassword' | + 'kAccountModal_HideFlag' | + + 'kAccountModal_VerifyText' | + 'kAccountModal_VerifyPasswordResetText' | + 'kAccountModal_PasswordResetSuccess' | + 'kMissingCaptcha' | + 'kPasswordsMustMatch' | + + 'kNotLoggedIn'; export interface I18nEvents { // Called when the language is changed @@ -153,9 +152,8 @@ interface StringKeyMap { /// our fancy internationalization helper. export class I18n { // The language data itself - private langs : Map = new Map(); - private lang: Language = fallbackLanguage; - private languageDropdown: HTMLSpanElement = document.getElementById('languageDropdown') as HTMLSpanElement; + langs : Map = new Map(); + lang: Language = fallbackLanguage; private emitter: Emitter = createNanoEvents(); CurrentLanguage = () => this.langId; @@ -171,26 +169,13 @@ export class I18n { if (!res.ok) { alert("Failed to load languages.json: " + res.statusText); await this.SetLanguage(fallbackId); - this.ReplaceStaticStrings(); return; } var langData = await res.json() as LanguagesJson; for (const langId in langData.languages) { this.langs.set(langId, langData.languages[langId]); } - 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', async e => { - e.preventDefault(); - await this.SetLanguage(langId); - this.ReplaceStaticStrings(); - }); - this.languageDropdown.appendChild(a); - }); + let lang = null; let lsLang = window.localStorage.getItem('i18n-lang'); var browserLang = navigator.language.toLowerCase(); @@ -210,14 +195,13 @@ export class I18n { // If all else fails, use the default language if (lang === null) lang = langData.defaultLanguage; await this.SetLanguage(lang); - this.ReplaceStaticStrings(); } getCountryName(code: string) : string { return this.regionNameRenderer.of(code) || code; } - private async SetLanguage(id: string) { + async SetLanguage(id: string) { let lastId = this.langId; this.langId = id; @@ -237,9 +221,6 @@ export class I18n { this.lang = lang; if (this.langId != lastId) { - // Replace static strings - this.ReplaceStaticStrings(); - // Update region name renderer target language this.regionNameRenderer = new Intl.DisplayNames([this.langId], {type: 'region'}); }; @@ -253,196 +234,11 @@ export class I18n { 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, - ghostTurnBtnText: I18nStringKey.kAdminVMButtons_GhostTurnOff, - - // 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, - hideFlagCheckboxLabel: I18nStringKey.kAccountModal_HideFlag, - 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; + if (key === 'kGeneric_CollabVM' && Config.SiteNameOverride) return Config.SiteNameOverride; + if (key === 'kWelcomeModal_Header' && Config.WelcomeModalTitleOverride) return Config.WelcomeModalTitleOverride; + if (key === '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 @@ -465,6 +261,4 @@ export class I18n { on(event: e, cb: I18nEvents[e]): Unsubscribe { return this.emitter.on(event, cb); } -} - -export let TheI18n = new I18n(); \ No newline at end of file +} \ No newline at end of file diff --git a/src/ts/icons.ts b/src/ts/icons.ts new file mode 100644 index 0000000..7b73517 --- /dev/null +++ b/src/ts/icons.ts @@ -0,0 +1,29 @@ +// Fontawesome icon library +import { library } from "@fortawesome/fontawesome-svg-core"; + +import * as faSolid from "@fortawesome/free-solid-svg-icons"; +import * as faBrands from "@fortawesome/free-brands-svg-icons"; + +export function fontAwesomeLibrary() { + library.add( + // fa-solid + faSolid.faCamera, + faSolid.faCircleQuestion, + faSolid.faClipboardCheck, + faSolid.faComputerMouse, + faSolid.faGear, + faSolid.faGlobe, + faSolid.faHouse, + faSolid.faKeyboard, + faSolid.faMoon, + faSolid.faPaperPlane, + faSolid.faRotateLeft, + faSolid.faSignature, + faSolid.faSun, + faSolid.faUser, + // fa-brands + faBrands.faDiscord, + faBrands.faMastodon, + faBrands.faReddit, + ) +} \ No newline at end of file diff --git a/src/ts/index.ts b/src/ts/index.ts new file mode 100644 index 0000000..7fca3c9 --- /dev/null +++ b/src/ts/index.ts @@ -0,0 +1,13 @@ +import { createApp } from "vue"; +import App from "../vue/app.vue"; +import { fontAwesomeLibrary } from "./icons"; +import * as bootstrap from 'bootstrap'; + +// init fontawesome +fontAwesomeLibrary(); + +// force parcel to bundle bootstrap +bootstrap; + +const app = createApp(App); +app.mount("#app"); \ No newline at end of file diff --git a/src/ts/main.ts b/src/ts/main.ts deleted file mode 100644 index 6930ad7..0000000 --- a/src/ts/main.ts +++ /dev/null @@ -1,1598 +0,0 @@ -import CollabVMClient from './protocol/CollabVMClient.js'; -import VM from './protocol/VM.js'; -import Config from '../../config.json'; -import { Permissions, Rank } from './protocol/Permissions.js'; -import { User } from './protocol/User.js'; -import TurnStatus from './protocol/TurnStatus.js'; -import Keyboard from 'simple-keyboard'; -import { OSK_buttonToKeysym } from './keyboard'; -import 'simple-keyboard/build/css/index.css'; -import VoteStatus from './protocol/VoteStatus.js'; -import * as bootstrap from 'bootstrap'; -import MuteState from './protocol/MuteState.js'; -import { I18nStringKey, TheI18n } from './i18n.js'; -import { Format } from './format.js'; -import AuthManager from './AuthManager.js'; -import dayjs from 'dayjs'; -import * as dompurify from 'dompurify'; -const _eval = window.eval; - -// 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, - rulesBtn: document.getElementById('rulesBtn') 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, - takeTurnBtn: document.getElementById('takeTurnBtn') as HTMLButtonElement, - changeUsernameBtn: document.getElementById('changeUsernameBtn') as HTMLButtonElement, - turnBtnText: document.getElementById('turnBtnText') as HTMLSpanElement, - turnstatus: document.getElementById('turnstatus') as HTMLParagraphElement, - osk: window.document.getElementById('oskBtn') as HTMLButtonElement, - oskContainer: document.getElementById('osk-container') as HTMLDivElement, - screenshotButton: document.getElementById('screenshotButton') as HTMLButtonElement, - voteResetButton: document.getElementById('voteResetButton') as HTMLButtonElement, - voteResetPanel: document.getElementById('voteResetPanel') as HTMLDivElement, - voteYesBtn: document.getElementById('voteYesBtn') as HTMLButtonElement, - voteNoBtn: document.getElementById('voteNoBtn') as HTMLButtonElement, - voteYesLabel: document.getElementById('voteYesLabel') as HTMLSpanElement, - voteNoLabel: document.getElementById('voteNoLabel') as HTMLSpanElement, - voteTimeText: document.getElementById('voteTimeText') as HTMLSpanElement, - loginModal: document.getElementById('loginModal') as HTMLDivElement, - adminPassword: document.getElementById('adminPassword') as HTMLInputElement, - loginButton: document.getElementById('loginButton') as HTMLButtonElement, - adminInputVMID: document.getElementById('adminInputVMID') as HTMLInputElement, - badPasswordAlert: document.getElementById('badPasswordAlert') as HTMLDivElement, - incorrectPasswordDismissBtn: document.getElementById('incorrectPasswordDismissBtn') as HTMLButtonElement, - ctrlAltDelBtn: document.getElementById('ctrlAltDelBtn') as HTMLButtonElement, - toggleThemeBtn: document.getElementById('toggleThemeBtn') as HTMLAnchorElement, - toggleThemeIcon: document.getElementById('toggleThemeIcon') as HTMLElement, - toggleThemeBtnText: document.getElementById('toggleThemeBtnText') as HTMLSpanElement, - // Admin - staffbtns: document.getElementById('staffbtns') as HTMLDivElement, - restoreBtn: document.getElementById('restoreBtn') as HTMLButtonElement, - rebootBtn: document.getElementById('rebootBtn') as HTMLButtonElement, - clearQueueBtn: document.getElementById('clearQueueBtn') as HTMLButtonElement, - bypassTurnBtn: document.getElementById('bypassTurnBtn') as HTMLButtonElement, - endTurnBtn: document.getElementById('endTurnBtn') as HTMLButtonElement, - qemuMonitorBtn: document.getElementById('qemuMonitorBtn') as HTMLButtonElement, - xssCheckboxContainer: document.getElementById('xssCheckboxContainer') as HTMLDivElement, - xssCheckbox: document.getElementById('xssCheckbox') as HTMLInputElement, - forceVotePanel: document.getElementById('forceVotePanel') as HTMLDivElement, - 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, - // Auth - accountDropdownUsername: document.getElementById("accountDropdownUsername") as HTMLSpanElement, - accountDropdownMenuLink: document.getElementById("accountDropdownMenuLink") as HTMLDivElement, - accountLoginButton: document.getElementById("accountLoginButton") as HTMLAnchorElement, - accountRegisterButton: document.getElementById("accountRegisterButton") as HTMLAnchorElement, - accountSettingsButton: document.getElementById("accountSettingsButton") as HTMLAnchorElement, - accountLogoutButton: document.getElementById("accountLogoutButton") as HTMLAnchorElement, - accountModal: document.getElementById("accountModal") as HTMLDivElement, - accountModalError: document.getElementById("accountModalError") as HTMLDivElement, - accountModalErrorText: document.getElementById("accountModalErrorText") as HTMLSpanElement, - accountModalErrorDismiss: document.getElementById("accountModalErrorDismiss") as HTMLButtonElement, - accountModalSuccess: document.getElementById("accountModalSuccess") as HTMLDivElement, - accountModalSuccessText: document.getElementById("accountModalSuccessText") as HTMLSpanElement, - accountModalSuccessDismiss: document.getElementById("accountModalSuccessDismiss") as HTMLButtonElement, - accountLoginSection: document.getElementById("accountLoginSection") as HTMLDivElement, - accountRegisterSection: document.getElementById("accountRegisterSection") as HTMLDivElement, - accountVerifyEmailSection: document.getElementById("accountVerifyEmailSection") as HTMLDivElement, - accountVerifyEmailText: document.getElementById("accountVerifyEmailText") as HTMLParagraphElement, - accountModalTitle: document.getElementById("accountModalTitle") as HTMLHeadingElement, - accountLoginForm: document.getElementById("accountLoginForm") as HTMLFormElement, - accountRegisterForm: document.getElementById("accountRegisterForm") as HTMLFormElement, - accountVerifyEmailForm: document.getElementById("accountVerifyEmailForm") as HTMLFormElement, - accountLoginCaptchaContainer: document.getElementById("accountLoginCaptchaContainer") as HTMLDivElement, - accountLoginRecaptchaContainer: document.getElementById("accountLoginReCaptchaContainer") as HTMLDivElement, - accountLoginTurnstileContainer: document.getElementById("accountLoginTurnstileContainer") as HTMLDivElement, - accountRegisterCaptchaContainer: document.getElementById("accountRegisterCaptchaContainer") as HTMLDivElement, - accountRegisterRecaptchaContainer: document.getElementById("accountRegisterReCaptchaContainer") as HTMLDivElement, - accountRegisterTurnstileContainer: document.getElementById("accountRegisterTurnstileContainer") as HTMLDivElement, - - accountLoginUsername: document.getElementById("accountLoginUsername") as HTMLInputElement, - accountLoginPassword: document.getElementById("accountLoginPassword") as HTMLInputElement, - accountRegisterEmail: document.getElementById("accountRegisterEmail") as HTMLInputElement, - accountRegisterUsername: document.getElementById("accountRegisterUsername") as HTMLInputElement, - accountRegisterPassword: document.getElementById("accountRegisterPassword") as HTMLInputElement, - accountRegisterConfirmPassword: document.getElementById("accountRegisterConfirmPassword") as HTMLInputElement, - accountRegisterDateOfBirth: document.getElementById("accountRegisterDateOfBirth") as HTMLInputElement, - accountVerifyEmailCode: document.getElementById("accountVerifyEmailCode") as HTMLInputElement, - accountVerifyEmailPassword: document.getElementById("accountVerifyEmailPassword") as HTMLInputElement, - - accountSettingsSection: document.getElementById("accountSettingsSection") as HTMLDivElement, - accountSettingsForm: document.getElementById("accountSettingsForm") as HTMLFormElement, - accountSettingsEmail: document.getElementById("accountSettingsEmail") as HTMLInputElement, - accountSettingsUsername: document.getElementById("accountSettingsUsername") as HTMLInputElement, - accountSettingsNewPassword: document.getElementById("accountSettingsNewPassword") as HTMLInputElement, - accountSettingsConfirmNewPassword: document.getElementById("accountSettingsConfirmNewPassword") as HTMLInputElement, - accountSettingsCurrentPassword: document.getElementById("accountSettingsCurrentPassword") as HTMLInputElement, - hideFlagCheckbox: document.getElementById("hideFlagCheckbox") as HTMLInputElement, - - accountResetPasswordSection: document.getElementById("accountResetPasswordSection") as HTMLDivElement, - accountResetPasswordForm: document.getElementById("accountResetPasswordForm") as HTMLFormElement, - accountResetPasswordEmail: document.getElementById("accountResetPasswordEmail") as HTMLInputElement, - accountResetPasswordUsername: document.getElementById("accountResetPasswordUsername") as HTMLInputElement, - accountResetPasswordCaptchaContainer: document.getElementById("accountResetPasswordCaptchaContainer") as HTMLDivElement, - accountResetPasswordRecaptchaContainer: document.getElementById("accountResetPasswordReCaptchaContainer") as HTMLDivElement, - accountResetPasswordTurnstileContainer: document.getElementById("accountResetPasswordTurnstileContainer") as HTMLDivElement, - - accountResetPasswordVerifySection: document.getElementById("accountResetPasswordVerifySection") as HTMLDivElement, - accountVerifyPasswordResetText: document.getElementById("accountVerifyPasswordResetText") as HTMLParagraphElement, - accountResetPasswordVerifyForm: document.getElementById("accountResetPasswordVerifyForm") as HTMLFormElement, - accountResetPasswordCode: document.getElementById("accountResetPasswordCode") as HTMLInputElement, - accountResetPasswordNewPassword: document.getElementById("accountResetPasswordNewPassword") as HTMLInputElement, - accountResetPasswordConfirmNewPassword: document.getElementById("accountResetPasswordConfirmNewPassword") as HTMLInputElement, - accountForgotPasswordButton: document.getElementById("accountForgotPasswordButton") as HTMLButtonElement, -}; - -let auth : AuthManager|null = null; - -/* Start OSK */ -let commonKeyboardOptions = { - onKeyPress: (button: string) => onKeyPress(button), - theme: 'simple-keyboard hg-theme-default cvmDark cvmDisabled hg-layout-default', - syncInstanceInputs: true, - mergeDisplay: true -}; - -let keyboard = new Keyboard('.osk-main', { - ...commonKeyboardOptions, - layout: { - default: [ - '{escape} {f1} {f2} {f3} {f4} {f5} {f6} {f7} {f8} {f9} {f10} {f11} {f12}', - '` 1 2 3 4 5 6 7 8 9 0 - = {backspace}', - '{tab} q w e r t y u i o p [ ] \\', - "{capslock} a s d f g h j k l ; ' {enter}", - '{shiftleft} z x c v b n m , . / {shiftright}', - '{controlleft} {metaleft} {altleft} {space} {altright} {metaright} {controlright}' - ], - shift: [ - '{escape} {f1} {f2} {f3} {f4} {f5} {f6} {f7} {f8} {f9} {f10} {f11} {f12}', - '~ ! @ # $ % ^ & * ( ) _ + {backspace}', - '{tab} Q W E R T Y U I O P { } |', - '{capslock} A S D F G H J K L : " {enter}', - '{shiftleft} Z X C V B N M < > ? {shiftright}', - '{controlleft} {metaleft} {altleft} {space} {altright} {metaright} {controlright}' - ], - capslock: [ - '{escape} {f1} {f2} {f3} {f4} {f5} {f6} {f7} {f8} {f9} {f10} {f11} {f12}', - '` 1 2 3 4 5 6 7 8 9 0 - = {backspace}', - '{tab} Q W E R T Y U I O P [ ] \\', - "{capslock} A S D F G H J K L ; ' {enter}", - '{shiftleft} Z X C V B N M , . / {shiftright}', - '{controlleft} {metaleft} {altleft} {space} {altright} {metaright} {controlright}' - ], - shiftcaps: [ - '{escape} {f1} {f2} {f3} {f4} {f5} {f6} {f7} {f8} {f9} {f10} {f11} {f12}', - '~ ! @ # $ % ^ & * ( ) _ + {backspace}', - '{tab} q w e r t y u i o p { } |', - '{capslock} a s d f g h j k l : " {enter}', - '{shiftleft} z x c v b n m < > ? {shiftright}', - '{controlleft} {metaleft} {altleft} {space} {altright} {metaright} {controlright}' - ] - }, - display: { - '{escape}': 'Esc', - '{tab}': 'Tab', - '{backspace}': 'Back', - '{enter}': 'Enter', - '{capslock}': 'Caps', - '{shiftleft}': 'Shift', - '{shiftright}': 'Shift', - '{controlleft}': 'Ctrl', - '{controlright}': 'Ctrl', - '{altleft}': 'Alt', - '{altright}': 'Alt', - '{metaleft}': 'Super', - '{metaright}': 'Menu' - } -}); - -let keyboardControlPad = new Keyboard('.osk-control', { - ...commonKeyboardOptions, - layout: { - default: ['{prtscr} {scrolllock} {pause}', '{insert} {home} {pageup}', '{delete} {end} {pagedown}'] - }, - display: { - '{prtscr}': 'Print', - '{scrolllock}': 'Scroll', - '{pause}': 'Pause', - '{insert}': 'Ins', - '{home}': 'Home', - '{pageup}': 'PgUp', - '{delete}': 'Del', - '{end}': 'End', - '{pagedown}': 'PgDn' - } -}); - -let keyboardArrows = new Keyboard('.osk-arrows', { - ...commonKeyboardOptions, - layout: { - default: ['{arrowup}', '{arrowleft} {arrowdown} {arrowright}'] - } -}); - -let keyboardNumPad = new Keyboard('.osk-numpad', { - ...commonKeyboardOptions, - layout: { - default: ['{numlock} {numpaddivide} {numpadmultiply}', '{numpad7} {numpad8} {numpad9}', '{numpad4} {numpad5} {numpad6}', '{numpad1} {numpad2} {numpad3}', '{numpad0} {numpaddecimal}'] - } -}); - -let keyboardNumPadEnd = new Keyboard('.osk-numpadEnd', { - ...commonKeyboardOptions, - layout: { - default: ['{numpadsubtract}', '{numpadadd}', '{numpadenter}'] - } -}); - -let shiftHeld = false; -let ctrlHeld = false; -let capsHeld = false; -let altHeld = false; -let metaHeld = false; - -const setButtonBackground = (selectors: string, condition: boolean) => { - for (let button of document.querySelectorAll(selectors) as NodeListOf) { - button.style.backgroundColor = condition ? '#1c4995' : 'rgba(0, 0, 0, 0.5)'; - } -}; - -const enableOSK = (enable: boolean) => { - const theme = `simple-keyboard hg-theme-default cvmDark ${enable ? '' : 'cvmDisabled'} hg-layout-default`; - [keyboard, keyboardControlPad, keyboardArrows, keyboardNumPad, keyboardNumPadEnd].forEach((part) => { - part.setOptions({ - theme: theme - }); - }); - - if (enable) updateOSKStyle(); -}; - -const updateOSKStyle = () => { - setButtonBackground('.hg-button-shiftleft, .hg-button-shiftright', shiftHeld); - setButtonBackground('.hg-button-controlleft, .hg-button-controlright', ctrlHeld); - setButtonBackground('.hg-button-capslock', capsHeld); - setButtonBackground('.hg-button-altleft, .hg-button-altright', altHeld); - setButtonBackground('.hg-button-metaleft, .hg-button-metaright', metaHeld); -}; - -function onKeyPress(button: string) { - if (VM === null) return; - let keysym = OSK_buttonToKeysym(button); - if (!keysym) { - console.error(`no keysym for ${button}, report this!`); - return; - } - - switch (true) { - case button.startsWith('{shift'): - shiftHeld = !shiftHeld; - VM.key(keysym, shiftHeld); - break; - case button.startsWith('{control'): - ctrlHeld = !ctrlHeld; - VM.key(keysym, ctrlHeld); - break; - case button === '{capslock}': - capsHeld = !capsHeld; - VM.key(keysym, capsHeld); - break; - case button.startsWith('{alt'): - altHeld = !altHeld; - VM.key(keysym, altHeld); - break; - case button.startsWith('{meta'): - metaHeld = !metaHeld; - VM.key(keysym, metaHeld); - break; - default: - VM.key(keysym, true); - VM.key(keysym, false); - } - - keyboard.setOptions({ - layoutName: shiftHeld && capsHeld ? 'shiftcaps' : shiftHeld ? 'shift' : capsHeld ? 'capslock' : 'default' - }); - - updateOSKStyle(); -} - -/* End OSK */ - -let expectedClose = false; -let turn = -1; -// Listed VMs -const vms: VM[] = []; -const cards: HTMLDivElement[] = []; -const users: { - user: User; - usernameElement: HTMLSpanElement; - flagElement: HTMLSpanElement; - element: HTMLTableRowElement; -}[] = []; -let turnInterval: number | undefined = undefined; -let voteInterval: number | undefined = undefined; -let turnTimer = 0; -let voteTimer = 0; -let rank: Rank = Rank.Unregistered; -let perms: Permissions = new Permissions(0); -const chatsound = new Audio(Config.ChatSound); - -// Active VM -let VM: CollabVMClient | null = null; - -async function multicollab(url: string) { - // Create the client - let client = new CollabVMClient(url); - - await client.WaitForOpen(); - - // Get the list of VMs - let list = await client.list(); - - // Get the number of online users - let online = client.getUsers().length; - - // Close the client - client.close(); - - // Add to the list - vms.push(...list); - - // Add to the DOM - for (let vm of list) { - let div = document.createElement('div'); - div.classList.add('col-sm-5', 'col-md-3'); - let card = document.createElement('div'); - card.classList.add('card'); - if (Config.NSFWVMs.indexOf(vm.id) !== -1) card.classList.add('cvm-nsfw'); - card.setAttribute('data-cvm-node', vm.id); - card.addEventListener('click', async () => { - try { - await openVM(vm); - } catch (e) { - alert((e as Error).message); - } - }); - vm.thumbnail.classList.add('card-img-top'); - let cardBody = document.createElement('div'); - cardBody.classList.add('card-body'); - let cardTitle = document.createElement('h5'); - cardTitle.innerHTML = Config.RawMessages.VMTitles ? vm.displayName : dompurify.sanitize(vm.displayName); - let usersOnline = document.createElement('span'); - usersOnline.innerHTML = `( ${online})`; - cardBody.appendChild(cardTitle); - cardBody.appendChild(usersOnline); - card.appendChild(vm.thumbnail); - card.appendChild(cardBody); - div.appendChild(card); - cards.push(div); - sortVMList(); - } -} - -async function openVM(vm: VM): Promise { - // 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 - - VM!.on('chat', (username, message) => chatMessage(username, message)); - VM!.on('adduser', (user) => addUser(user)); - VM!.on('flag', () => flag()); - VM!.on('remuser', (user) => remUser(user)); - VM!.on('rename', (oldname, newname, selfrename) => userRenamed(oldname, newname, selfrename)); - - VM!.on('renamestatus', (status) => { - // TODO: i18n these - switch (status) { - case 'taken': - alert(TheI18n.GetString(I18nStringKey.kError_UsernameTaken)); - break; - case 'invalid': - alert(TheI18n.GetString(I18nStringKey.kError_UsernameInvalid)); - break; - case 'blacklisted': - alert(TheI18n.GetString(I18nStringKey.kError_UsernameBlacklisted)); - break; - } - }); - - VM!.on('turn', (status) => turnUpdate(status)); - VM!.on('vote', (status: VoteStatus) => voteUpdate(status)); - VM!.on('voteend', () => voteEnd()); - VM!.on('votecd', (voteCooldown) => window.alert(TheI18n.GetString(I18nStringKey.kVM_VoteCooldownTimer, voteCooldown))); - VM!.on('login', (rank: Rank, perms: Permissions) => onLogin(rank, perms)); - - VM!.on('close', () => { - if (!expectedClose) alert(TheI18n.GetString(I18nStringKey.kError_UnexpectedDisconnection)); - closeVM(); - }); - - // auth - VM!.on('auth', async server => { - elements.changeUsernameBtn.style.display = "none"; - if (Config.Auth.Enabled && Config.Auth.APIEndpoint === server && auth!.account) { - VM!.loginAccount(auth!.account.sessionToken); - } else if (!Config.Auth.Enabled || Config.Auth.APIEndpoint !== server) { - auth = new AuthManager(server); - await renderAuth(); - } - }); - - // Wait for the client to open - await VM!.WaitForOpen(); - - // Connect to node - chatMessage('', `${vm.id}
`); - let username = Config.Auth.Enabled ? (auth!.account?.username ?? null) : localStorage.getItem('username'); - let connected = await VM.connect(vm.id, username); - elements.adminInputVMID.value = vm.id; - w.VMName = vm.id; - if (!connected) { - // just give up - closeVM(); - throw new Error('Failed to connect to node'); - } - // Set the title - document.title = Format('{0} - {1}', vm.id, TheI18n.GetString(I18nStringKey.kGeneric_CollabVM)); - // Append canvas - elements.vmDisplay.appendChild(VM!.canvas); - // Switch to the VM view - elements.vmlist.style.display = 'none'; - elements.vmview.style.display = 'block'; - return; -} - -function closeVM() { - if (VM === null) return; - expectedClose = true; - // Close the VM - VM.close(); - VM = null; - document.title = TheI18n.GetString(I18nStringKey.kGeneric_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 = ''; - rank = Rank.Unregistered; - perms.set(0); - w.VMName = null; - // Reset admin and vote panels - elements.staffbtns.style.display = 'none'; - elements.restoreBtn.style.display = 'none'; - elements.rebootBtn.style.display = 'none'; - elements.bypassTurnBtn.style.display = 'none'; - elements.endTurnBtn.style.display = 'none'; - 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'; - elements.voteYesLabel.innerText = '0'; - elements.voteNoLabel.innerText = '0'; - elements.xssCheckbox.checked = false; - elements.username.classList.remove('username-admin', 'username-moderator', 'username-registered'); - elements.username.classList.add('username-unregistered'); - // Reset rename button - elements.changeUsernameBtn.style.display = "inline-block"; - // Reset auth if it was changed by the VM - if (Config.Auth.Enabled && auth?.apiEndpoint !== Config.Auth.APIEndpoint) { - auth = new AuthManager(Config.Auth.APIEndpoint); - renderAuth(); - } else if (auth && !Config.Auth.Enabled) { - auth = null; - elements.accountDropdownMenuLink.style.display = "none"; - } -} - -async function loadList() { - var jsonVMs = Config.ServerAddressesListURI === null ? [] : await (await fetch(Config.ServerAddressesListURI)).json(); - await Promise.all( - [Config.ServerAddresses, jsonVMs].flat().map((url) => { - return multicollab(url); - }) - ); - - // automatically join the vm that's in the url if it exists in the node list - let v = vms.find((v) => v.id === window.location.hash.substring(1)); - try { - if (v !== undefined) await openVM(v); - } catch (e) { - alert((e as Error).message); - } -} - -function sortVMList() { - cards.sort((a, b) => { - return a.children[0].getAttribute('data-cvm-node')! > b.children[0].getAttribute('data-cvm-node')! ? 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 && b.user.turn !== 0) return -1; - if (b.user.username === w.username && b.user.turn >= a.user.turn && a.user.turn !== 0) 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) { - let tr = document.createElement('tr'); - let td = document.createElement('td'); - if (!Config.RawMessages.Messages) message = dompurify.sanitize(message); - // System message - if (username === '') td.innerHTML = message; - else { - let user = VM!.getUsers().find((u) => u.username === username); - let rank; - if (user !== undefined) rank = user.rank; - else rank = Rank.Unregistered; - let userclass; - let msgclass; - switch (rank) { - case Rank.Unregistered: - userclass = 'chat-username-unregistered'; - msgclass = 'chat-unregistered'; - break; - case Rank.Registered: - userclass = 'chat-username-registered'; - msgclass = 'chat-registered'; - 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 = `${username}▸ ${message}`; - // hacky way to allow scripts - if (Config.RawMessages.Messages) 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; - chatsound.play(); -} - -function addUser(user: User) { - let olduser = users.find((u) => u.user === user); - if (olduser !== undefined) elements.userlist.removeChild(olduser.element); - let tr = document.createElement('tr'); - tr.setAttribute('data-cvm-turn', '-1'); - let td = document.createElement('td'); - let flagSpan = document.createElement('span'); - let usernameSpan = document.createElement('span'); - flagSpan.classList.add("userlist-flag"); - usernameSpan.classList.add("userlist-username"); - td.appendChild(flagSpan); - if (user.countryCode !== null) { - flagSpan.innerHTML = getFlagEmoji(user.countryCode); - flagSpan.title = TheI18n.getCountryName(user.countryCode); - }; - td.appendChild(usernameSpan); - usernameSpan.innerText = 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.Registered: - tr.classList.add('user-registered'); - break; - case Rank.Unregistered: - tr.classList.add('user-unregistered'); - break; - } - if (user.username === w.username) tr.classList.add('user-current'); - tr.appendChild(td); - let u = { user: user, element: tr, usernameElement: usernameSpan, flagElement: flagSpan }; - if (rank === Rank.Admin || rank === Rank.Moderator) userModOptions(u); - elements.userlist.appendChild(tr); - if (olduser !== undefined) olduser.element = tr; - else users.push(u); - elements.onlineusercount.innerHTML = VM!.getUsers().length.toString(); -} - -function remUser(user: User) { - let 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 getFlagEmoji(countryCode: string) { - if (countryCode.length !== 2) throw new Error('Invalid country code'); - return String.fromCodePoint(...countryCode.toUpperCase().split('').map(char => 127397 + char.charCodeAt(0))); -} - -function flag() { - for (let user of users.filter(u => u.user.countryCode !== null)) { - user.flagElement.innerHTML = getFlagEmoji(user.user.countryCode!); - user.flagElement.title = TheI18n.getCountryName(user.user.countryCode!); - } -} - -function userRenamed(oldname: string, newname: string, selfrename: boolean) { - let user = users.find((u) => u.user.username === newname); - if (user) { - user.usernameElement.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; - VM!.canvas.classList.remove('focused', 'waiting'); - clearInterval(turnInterval); - turnTimer = 0; - for (const user of users) { - user.element.classList.remove('user-turn', 'user-waiting'); - user.element.setAttribute('data-cvm-turn', '-1'); - } - elements.turnBtnText.innerHTML = TheI18n.GetString(I18nStringKey.kVMButtons_TakeTurn); - enableOSK(false); - - if (status.user !== null) { - let 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) { - let 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; - turnTimer = status.turnTime! / 1000; - elements.turnBtnText.innerHTML = TheI18n.GetString(I18nStringKey.kVMButtons_EndTurn); - VM!.canvas.classList.add('focused'); - enableOSK(true); - } - if (status.queue.some((u) => u.username === w.username)) { - turn = status.queue.findIndex((u) => u.username === w.username) + 1; - turnTimer = status.queueTime! / 1000; - elements.turnBtnText.innerHTML = TheI18n.GetString(I18nStringKey.kVMButtons_EndTurn); - VM!.canvas.classList.add('waiting'); - } - if (turn === -1) elements.turnstatus.innerText = ''; - else { - //@ts-ignore - turnInterval = setInterval(() => turnIntervalCb(), 1000); - setTurnStatus(); - } - sortUserList(); -} - -function voteUpdate(status: VoteStatus) { - clearInterval(voteInterval); - elements.voteResetPanel.style.display = 'block'; - elements.voteYesLabel.innerText = status.yesVotes.toString(); - elements.voteNoLabel.innerText = status.noVotes.toString(); - voteTimer = Math.floor(status.timeToEnd / 1000); - //@ts-ignore - voteInterval = setInterval(() => updateVoteEndTime(), 1000); - updateVoteEndTime(); -} - -function updateVoteEndTime() { - voteTimer--; - elements.voteTimeText.innerText = TheI18n.GetString(I18nStringKey.kVM_VoteForResetTimer, voteTimer); - if (voteTimer === 0) clearInterval(voteInterval); -} - -function voteEnd() { - clearInterval(voteInterval); - elements.voteResetPanel.style.display = 'none'; -} - -function turnIntervalCb() { - turnTimer--; - setTurnStatus(); -} - -function setTurnStatus() { - if (turn === 0) elements.turnstatus.innerText = TheI18n.GetString(I18nStringKey.kVM_TurnTimeTimer, turnTimer); - else elements.turnstatus.innerText = TheI18n.GetString(I18nStringKey.kVM_WaitingTurnTimer, turnTimer); -} - -function sendChat() { - if (VM === null) return; - if (elements.xssCheckbox.checked) VM.xss(elements.chatinput.value); - else 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', () => { - let oldname = w.username.nodeName === undefined ? w.username : w.username.innerText; - let newname = prompt(TheI18n.GetString(I18nStringKey.kVMPrompts_EnterNewUsernamePrompt), oldname); - if (newname === oldname) return; - VM?.rename(newname); -}); -elements.takeTurnBtn.addEventListener('click', () => { - VM?.turn(turn === -1); -}); -elements.screenshotButton.addEventListener('click', () => { - if (!VM) return; - VM.canvas.toBlob((blob) => { - open(URL.createObjectURL(blob!), '_blank'); - }); -}); -elements.ctrlAltDelBtn.addEventListener('click', () => { - if (!VM) return; - // Ctrl - VM?.key(0xffe3, true); - // Alt - VM?.key(0xffe9, true); - // Del - VM?.key(0xffff, true); - // Ctrl - VM?.key(0xffe3, false); - // Alt - VM?.key(0xffe9, false); - // Del - VM?.key(0xffff, false); -}); -elements.voteResetButton.addEventListener('click', () => VM?.vote(true)); -elements.voteYesBtn.addEventListener('click', () => VM?.vote(true)); -elements.voteNoBtn.addEventListener('click', () => VM?.vote(false)); -// Login -let usernameClick = false; -const loginModal = new bootstrap.Modal(elements.loginModal); -elements.loginModal.addEventListener('shown.bs.modal', () => elements.adminPassword.focus()); -elements.username.addEventListener('click', () => { - if (auth) return; - if (!usernameClick) { - usernameClick = true; - setInterval(() => (usernameClick = false), 1000); - return; - } - loginModal.show(); -}); -elements.loginButton.addEventListener('click', () => doLogin()); -elements.adminPassword.addEventListener('keypress', (e) => e.key === 'Enter' && doLogin()); -elements.incorrectPasswordDismissBtn.addEventListener('click', () => (elements.badPasswordAlert.style.display = 'none')); -function doLogin() { - let adminPass = elements.adminPassword.value; - if (adminPass === '') return; - VM?.login(adminPass); - elements.adminPassword.value = ''; - let u = VM?.on('login', () => { - u!(); - loginModal.hide(); - elements.badPasswordAlert.style.display = 'none'; - }); - let _u = VM?.on('badpw', () => { - _u!(); - elements.badPasswordAlert.style.display = 'block'; - }); -} - -function onLogin(_rank: Rank, _perms: Permissions) { - rank = _rank; - perms = _perms; - elements.username.classList.remove('username-unregistered', 'username-registered'); - if (rank === Rank.Admin) elements.username.classList.add('username-admin'); - if (rank === Rank.Moderator) elements.username.classList.add('username-moderator'); - if (rank === Rank.Registered) elements.username.classList.add('username-registered'); - elements.staffbtns.style.display = 'block'; - if (_perms.restore) elements.restoreBtn.style.display = 'inline-block'; - if (_perms.reboot) elements.rebootBtn.style.display = 'inline-block'; - if (_perms.bypassturn) { - elements.bypassTurnBtn.style.display = 'inline-block'; - elements.endTurnBtn.style.display = 'inline-block'; - elements.clearQueueBtn.style.display = 'inline-block'; - } - 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'; - if (rank !== Rank.Registered) - for (const user of users) userModOptions(user); -} - -function userModOptions(user: { user: User; element: HTMLTableRowElement }) { - let tr = user.element; - let td = tr.children[0] as HTMLTableCellElement; - tr.classList.add('dropdown'); - td.classList.add('dropdown-toggle'); - td.setAttribute('data-bs-toggle', 'dropdown'); - td.setAttribute('role', 'button'); - td.setAttribute('aria-expanded', 'false'); - let ul = document.createElement('ul'); - ul.classList.add('dropdown-menu', 'dropdown-menu-dark', 'table-dark', 'text-light'); - if (perms.bypassturn) addUserDropdownItem(ul, TheI18n.GetString(I18nStringKey.kVMButtons_EndTurn), () => VM!.endTurn(user.user.username), "mod-end-turn-btn"); - if (perms.ban) addUserDropdownItem(ul, TheI18n.GetString(I18nStringKey.kAdminVMButtons_Ban), () => VM!.ban(user.user.username), "mod-ban-btn"); - if (perms.kick) addUserDropdownItem(ul, TheI18n.GetString(I18nStringKey.kAdminVMButtons_Kick), () => VM!.kick(user.user.username), "mod-kick-btn"); - if (perms.rename) - addUserDropdownItem(ul, TheI18n.GetString(I18nStringKey.kVMButtons_ChangeUsername), () => { - let newname = prompt(TheI18n.GetString(I18nStringKey.kVMPrompts_AdminChangeUsernamePrompt, user.user.username)); - if (!newname) return; - VM!.renameUser(user.user.username, newname); - }, "mod-rename-btn"); - if (perms.mute) { - addUserDropdownItem(ul, TheI18n.GetString(I18nStringKey.kAdminVMButtons_TempMute), () => VM!.mute(user.user.username, MuteState.Temp), "mod-temp-mute-btn"); - addUserDropdownItem(ul, TheI18n.GetString(I18nStringKey.kAdminVMButtons_IndefMute), () => VM!.mute(user.user.username, MuteState.Perma), "mod-indef-mute-btn"); - addUserDropdownItem(ul, TheI18n.GetString(I18nStringKey.kAdminVMButtons_Unmute), () => VM!.mute(user.user.username, MuteState.Unmuted), "mod-unmute-btn"); - } - if (perms.grabip) - addUserDropdownItem(ul, TheI18n.GetString(I18nStringKey.kAdminVMButtons_GetIP), async () => { - let ip = await VM!.getip(user.user.username); - alert(ip); - }, "mod-get-ip-btn"); - tr.appendChild(ul); -} - -function addUserDropdownItem(ul: HTMLUListElement, text: string, func: () => void, classname: string) { - let li = document.createElement('li'); - let a = document.createElement('a'); - a.href = '#'; - a.classList.add('dropdown-item', classname); - a.innerHTML = text; - a.addEventListener('click', () => func()); - li.appendChild(a); - ul.appendChild(li); -} - -// Admin buttons -elements.restoreBtn.addEventListener('click', () => window.confirm(TheI18n.GetString(I18nStringKey.kVMPrompts_AdminRestoreVMPrompt)) && VM?.restore()); -elements.rebootBtn.addEventListener('click', () => VM?.reboot()); -elements.clearQueueBtn.addEventListener('click', () => VM?.clearQueue()); -elements.bypassTurnBtn.addEventListener('click', () => VM?.bypassTurn()); -elements.endTurnBtn.addEventListener('click', () => { - let user = VM?.getUsers().find((u) => u.turn === 0); - if (user) VM?.endTurn(user.username); -}); -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; - elements.qemuMonitorOutput.innerHTML += `> ${cmd}\n`; - elements.qemuMonitorInput.value = ''; - let response = await VM?.qemuMonitor(cmd); - elements.qemuMonitorOutput.innerHTML += `${response}\n`; - elements.qemuMonitorOutput.scrollTop = elements.qemuMonitorOutput.scrollHeight; -} -elements.qemuMonitorSendBtn.addEventListener('click', () => sendQEMUCommand()); -elements.qemuMonitorInput.addEventListener('keypress', (e) => e.key === 'Enter' && sendQEMUCommand()); - -elements.osk.addEventListener('click', () => elements.oskContainer.classList.toggle('d-none')); -// Auth stuff -async function renderAuth() { - if (auth === null) throw new Error("Cannot renderAuth when auth is null."); - await auth.getAPIInformation(); - elements.accountDropdownUsername.innerText = TheI18n.GetString(I18nStringKey.kNotLoggedIn); - elements.accountDropdownMenuLink.style.display = "block"; - if (!auth!.info!.registrationOpen) - elements.accountRegisterButton.style.display = "none"; - else - elements.accountRegisterButton.style.display = "block"; - elements.accountLoginButton.style.display = "block"; - elements.accountSettingsButton.style.display = "none"; - elements.accountLogoutButton.style.display = "none"; - - for (let element of document.querySelectorAll("[id^=accountRegisterCaptcha-], [id^=accountLoginCaptcha-], [id^=accountResetPasswordCaptcha-]")) { - hcaptcha.remove((element as HTMLElement).parentElement!.getAttribute("data-hcaptcha-widget-id")!); - element.remove(); - } - - for (let element of document.querySelectorAll("[id^=accountRegisterTurnstile-], [id^=accountLoginTurnstile-], [id^=accountResetPasswordTurnstile-]")) { - turnstile.remove((element as HTMLElement).parentElement!.getAttribute("data-turnstile-widget-id")!); - element.remove(); - } - - for (let element of document.querySelectorAll("[id^=accountRegisterRecaptcha-], [id^=accountLoginRecaptcha-], [id^=accountResetPasswordRecaptcha-]")) { - grecaptcha.reset(parseInt((element as HTMLElement).parentElement!.getAttribute("data-recaptcha-widget-id")!)); - element.remove(); - } - - if (auth!.info!.hcaptcha?.required) { - const hconfig = { sitekey: auth!.info!.hcaptcha.siteKey! }; - - let renderHcaptcha = () => { - let uuid = Math.random().toString(36).substring(7); - - let accountRegisterCaptcha = document.createElement("div"); - accountRegisterCaptcha.id = `accountRegisterCaptcha-${uuid}`; - elements.accountRegisterCaptchaContainer.appendChild(accountRegisterCaptcha); - - let accountLoginCaptcha = document.createElement("div"); - accountLoginCaptcha.id = `accountLoginCaptcha-${uuid}`; - elements.accountLoginCaptchaContainer.appendChild(accountLoginCaptcha); - - let accountResetPasswordCaptcha = document.createElement("div"); - accountResetPasswordCaptcha.id = `accountResetPasswordCaptcha-${uuid}`; - elements.accountResetPasswordCaptchaContainer.appendChild(accountResetPasswordCaptcha); - - const hCaptchaRegisterWidgetId = hcaptcha.render(accountRegisterCaptcha, hconfig); - const hCaptchaLoginWidgetId = hcaptcha.render(accountLoginCaptcha, hconfig); - const hCaptchaResetPasswordWidgetId = hcaptcha.render(accountResetPasswordCaptcha, hconfig); - - elements.accountRegisterCaptchaContainer.setAttribute("data-hcaptcha-widget-id", hCaptchaRegisterWidgetId!); - elements.accountLoginCaptchaContainer.setAttribute("data-hcaptcha-widget-id", hCaptchaLoginWidgetId!); - elements.accountResetPasswordCaptchaContainer.setAttribute("data-hcaptcha-widget-id", hCaptchaResetPasswordWidgetId!); - }; - - if (typeof hcaptcha === "undefined") { - let script = document.createElement("script"); - script.src = "https://js.hcaptcha.com/1/api.js?render=explicit&recaptchacompat=off&onload=hCaptchaLoad"; - (window as any).hCaptchaLoad = renderHcaptcha; - document.head.appendChild(script); - } else { - renderHcaptcha(); - } - } - - if (auth!.info?.turnstile?.required) { - const turnstileConfig = { sitekey: auth!.info!.turnstile.siteKey! }; - - let renderTurnstile = () => { - let uuid = Math.random().toString(36).substring(7); - - let accountRegisterTurnstile = document.createElement("div"); - accountRegisterTurnstile.id = `accountRegisterTurnstile-${uuid}`; - elements.accountRegisterTurnstileContainer.appendChild(accountRegisterTurnstile); - - let accountLoginTurnstile = document.createElement("div"); - accountLoginTurnstile.id = `accountLoginTurnstile-${uuid}`; - elements.accountLoginTurnstileContainer.appendChild(accountLoginTurnstile); - - let accountResetPasswordTurnstile = document.createElement("div"); - accountResetPasswordTurnstile.id = `accountResetPasswordTurnstile-${uuid}`; - elements.accountResetPasswordTurnstileContainer.appendChild(accountResetPasswordTurnstile); - - const turnstileRegisterWidgetId = turnstile.render(accountRegisterTurnstile, turnstileConfig); - const turnstileLoginWidgetId = turnstile.render(accountLoginTurnstile, turnstileConfig); - const turnstileResetPasswordWidgetId = turnstile.render(accountResetPasswordTurnstile, turnstileConfig); - - elements.accountRegisterTurnstileContainer.setAttribute("data-turnstile-widget-id", turnstileRegisterWidgetId!); - elements.accountLoginTurnstileContainer.setAttribute("data-turnstile-widget-id", turnstileLoginWidgetId!); - elements.accountResetPasswordTurnstileContainer.setAttribute("data-turnstile-widget-id", turnstileResetPasswordWidgetId!); - }; - - if (typeof turnstile === "undefined") { - let script = document.createElement("script"); - script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=turnstileLoad"; - (window as any).turnstileLoad = renderTurnstile; - document.head.appendChild(script); - } else { - renderTurnstile(); - } - } - - if (auth!.info?.recaptcha?.required) { - const recaptchaConfig = { sitekey: auth!.info!.recaptcha.siteKey! }; - - let renderRecaptcha = () => { - let uuid = Math.random().toString(36).substring(7); - - let accountRegisterRecaptcha = document.createElement("div"); - accountRegisterRecaptcha.id = `accountRegisterRecaptcha-${uuid}`; - elements.accountRegisterRecaptchaContainer.appendChild(accountRegisterRecaptcha); - - let accountLoginRecaptcha = document.createElement("div"); - accountLoginRecaptcha.id = `accountLoginRecaptcha-${uuid}`; - elements.accountLoginRecaptchaContainer.appendChild(accountLoginRecaptcha); - - let accountResetPasswordRecaptcha = document.createElement("div"); - accountResetPasswordRecaptcha.id = `accountResetPasswordRecaptcha-${uuid}`; - elements.accountResetPasswordRecaptchaContainer.appendChild(accountResetPasswordRecaptcha); - - const RecaptchaRegisterWidgetId = grecaptcha.render(accountRegisterRecaptcha, recaptchaConfig); - const RecaptchaLoginWidgetId = grecaptcha.render(accountLoginRecaptcha, recaptchaConfig); - const RecaptchaResetPasswordWidgetId = grecaptcha.render(accountResetPasswordRecaptcha, recaptchaConfig); - - elements.accountRegisterRecaptchaContainer.setAttribute("data-recaptcha-widget-id", RecaptchaRegisterWidgetId!.toString()); - elements.accountLoginRecaptchaContainer.setAttribute("data-recaptcha-widget-id", RecaptchaLoginWidgetId!.toString()); - elements.accountResetPasswordRecaptchaContainer.setAttribute("data-recaptcha-widget-id", RecaptchaResetPasswordWidgetId!.toString()); - }; - - if (typeof grecaptcha === "undefined") { - let script = document.createElement("script"); - script.src = "https://www.google.com/recaptcha/api.js?render=explicit&onload=recaptchaLoad"; - (window as any).recaptchaLoad = renderRecaptcha; - document.head.appendChild(script); - } else { - grecaptcha.ready(renderRecaptcha); - } - } - - var token = localStorage.getItem("collabvm_session_" + new URL(auth!.apiEndpoint).host); - if (token) { - var result = await auth!.loadSession(token); - if (result.success) { - loadAccount(); - } else { - localStorage.removeItem("collabvm_session_" + new URL(auth!.apiEndpoint).host); - } - } -} -function loadAccount() { - if (auth === null || auth.account === null) throw new Error("Cannot loadAccount when auth or auth.account is null."); - elements.accountDropdownUsername.innerText = auth!.account!.username; - elements.accountLoginButton.style.display = "none"; - elements.accountRegisterButton.style.display = "none"; - elements.accountSettingsButton.style.display = "block"; - elements.accountLogoutButton.style.display = "block"; - if (VM) VM.loginAccount(auth.account.sessionToken); -} -const accountModal = new bootstrap.Modal(elements.accountModal); -elements.accountModalErrorDismiss.addEventListener('click', () => elements.accountModalError.style.display = "none"); -elements.accountModalSuccessDismiss.addEventListener('click', () => elements.accountModalSuccess.style.display = "none"); -elements.accountLoginButton.addEventListener("click", () => { - elements.accountModalTitle.innerText = TheI18n.GetString(I18nStringKey.kGeneric_Login); - elements.accountRegisterSection.style.display = "none"; - elements.accountVerifyEmailSection.style.display = "none"; - elements.accountLoginSection.style.display = "block"; - elements.accountSettingsSection.style.display = "none"; - elements.accountResetPasswordSection.style.display = "none"; - elements.accountResetPasswordVerifySection.style.display = "none"; - accountModal.show(); -}); -elements.accountRegisterButton.addEventListener("click", () => { - elements.accountModalTitle.innerText = TheI18n.GetString(I18nStringKey.kGeneric_Register); - elements.accountRegisterSection.style.display = "block"; - elements.accountVerifyEmailSection.style.display = "none"; - elements.accountLoginSection.style.display = "none"; - elements.accountSettingsSection.style.display = "none"; - elements.accountResetPasswordSection.style.display = "none"; - elements.accountResetPasswordVerifySection.style.display = "none"; - accountModal.show(); -}); -elements.accountSettingsButton.addEventListener("click", () => { - elements.accountModalTitle.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_AccountSettings); - elements.accountRegisterSection.style.display = "none"; - elements.accountVerifyEmailSection.style.display = "none"; - elements.accountLoginSection.style.display = "none"; - elements.accountSettingsSection.style.display = "block"; - elements.accountResetPasswordSection.style.display = "none"; - elements.accountResetPasswordVerifySection.style.display = "none"; - // Fill fields - elements.accountSettingsUsername.value = auth!.account!.username; - elements.accountSettingsEmail.value = auth!.account!.email; - accountModal.show(); -}); -elements.accountLogoutButton.addEventListener('click', async () => { - if (!auth?.account) return; - await auth.logout(); - localStorage.removeItem("collabvm_session_" + new URL(auth!.apiEndpoint).host); - if (VM) closeVM(); - renderAuth(); -}); -elements.accountForgotPasswordButton.addEventListener('click', () => { - elements.accountModalTitle.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_ResetPassword); - elements.accountLoginSection.style.display = "none"; - elements.accountResetPasswordSection.style.display = "block"; -}); -// i dont know if theres a better place to put this -let accountBeingVerified; -elements.accountLoginForm.addEventListener('submit', async (e) => { - e.preventDefault(); - var hcaptchaToken = undefined; - var hcaptchaID = undefined; - if (auth!.info!.hcaptcha?.required) { - hcaptchaID = elements.accountLoginCaptchaContainer.getAttribute("data-hcaptcha-widget-id")! - var response = hcaptcha.getResponse(hcaptchaID); - if (response === "") { - elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha); - elements.accountModalError.style.display = "block"; - return false; - } - hcaptchaToken = response; - } - - var turnstileToken = undefined; - var turnstileID = undefined; - - if (auth!.info!.turnstile?.required) { - turnstileID = elements.accountLoginTurnstileContainer.getAttribute("data-turnstile-widget-id")! - var response: string = turnstile.getResponse(turnstileID) || ""; - if (response === "") { - elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha); - elements.accountModalError.style.display = "block"; - return false; - } - turnstileToken = response; - } - - var recaptchaToken = undefined; - var recaptchaID = undefined; - - if (auth!.info!.recaptcha?.required) { - recaptchaID = parseInt(elements.accountLoginRecaptchaContainer.getAttribute("data-recaptcha-widget-id")!) - var response = grecaptcha.getResponse(recaptchaID); - if (response === "") { - elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha); - elements.accountModalError.style.display = "block"; - return false; - } - recaptchaToken = response; - } - - var username = elements.accountLoginUsername.value; - var password = elements.accountLoginPassword.value; - var result = await auth!.login(username, password, hcaptchaToken, turnstileToken, recaptchaToken); - if (auth!.info!.hcaptcha?.required) hcaptcha.reset(hcaptchaID); - if (auth!.info!.turnstile?.required) turnstile.reset(turnstileID); - if (auth!.info!.recaptcha?.required) grecaptcha.reset(recaptchaID); - if (result.success) { - elements.accountLoginUsername.value = ""; - elements.accountLoginPassword.value = ""; - if (result.verificationRequired) { - accountBeingVerified = result.username; - elements.accountVerifyEmailText.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_VerifyText, result.email!); - elements.accountLoginSection.style.display = "none"; - elements.accountVerifyEmailSection.style.display = "block"; - return false; - } - localStorage.setItem("collabvm_session_" + new URL(auth!.apiEndpoint).host, result.token!); - loadAccount(); - accountModal.hide(); - } else { - elements.accountModalErrorText.innerHTML = result.error!; - elements.accountModalError.style.display = "block"; - } - return false; -}); -elements.accountRegisterForm.addEventListener('submit', async (e) => { - e.preventDefault(); - var hcaptchaToken = undefined; - var hcaptchaID = undefined; - if (auth!.info!.hcaptcha?.required) { - hcaptchaID = elements.accountRegisterCaptchaContainer.getAttribute("data-hcaptcha-widget-id")! - var response = hcaptcha.getResponse(hcaptchaID); - if (response === "") { - elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha); - elements.accountModalError.style.display = "block"; - return false; - } - hcaptchaToken = response; - } - - var turnstileToken = undefined; - var turnstileID = undefined; - - if (auth!.info!.turnstile?.required) { - turnstileID = elements.accountRegisterTurnstileContainer.getAttribute("data-turnstile-widget-id")! - var response: string = turnstile.getResponse(turnstileID) || ""; - if (response === "") { - elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha); - elements.accountModalError.style.display = "block"; - return false; - } - turnstileToken = response; - } - - var recaptchaToken = undefined; - var recaptchaID = undefined; - - if (auth!.info!.recaptcha?.required) { - recaptchaID = parseInt(elements.accountRegisterRecaptchaContainer.getAttribute("data-recaptcha-widget-id")!) - var response = grecaptcha.getResponse(recaptchaID); - if (response === "") { - elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha); - elements.accountModalError.style.display = "block"; - return false; - } - recaptchaToken = response; - } - - var username = elements.accountRegisterUsername.value; - var password = elements.accountRegisterPassword.value; - var email = elements.accountRegisterEmail.value; - var dob = dayjs(elements.accountRegisterDateOfBirth.valueAsDate); - if (password !== elements.accountRegisterConfirmPassword.value) { - elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kPasswordsMustMatch); - elements.accountModalError.style.display = "block"; - return false; - } - var result = await auth!.register(username, password, email, dob, hcaptchaToken, turnstileToken, recaptchaToken); - if (auth!.info!.hcaptcha?.required) hcaptcha.reset(hcaptchaID); - if (auth!.info!.turnstile?.required) turnstile.reset(turnstileID); - if (auth!.info!.recaptcha?.required) grecaptcha.reset(recaptchaID); - if (result.success) { - elements.accountRegisterUsername.value = ""; - elements.accountRegisterEmail.value = ""; - elements.accountRegisterPassword.value = ""; - elements.accountRegisterConfirmPassword.value = ""; - elements.accountRegisterDateOfBirth.value = ""; - if (result.verificationRequired) { - accountBeingVerified = result.username; - elements.accountVerifyEmailText.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_VerifyText, result.email!); - elements.accountRegisterSection.style.display = "none"; - elements.accountVerifyEmailSection.style.display = "block"; - return false; - } - localStorage.setItem("collabvm_session_" + new URL(auth!.apiEndpoint).host, result.sessionToken!); - await auth!.loadSession(result.sessionToken!); - loadAccount(); - accountModal.hide(); - } else { - elements.accountModalErrorText.innerHTML = result.error!; - elements.accountModalError.style.display = "block"; - } - return false; -}); -elements.accountVerifyEmailForm.addEventListener('submit', async e => { - e.preventDefault(); - var username = accountBeingVerified!; - var code = elements.accountVerifyEmailCode.value; - var password = elements.accountVerifyEmailPassword.value; - var result = await auth!.verifyEmail(username, password, code); - if (result.success) { - elements.accountVerifyEmailCode.value = ""; - elements.accountVerifyEmailPassword.value = ""; - localStorage.setItem("collabvm_session_" + new URL(auth!.apiEndpoint).host, result.sessionToken!); - await auth!.loadSession(result.sessionToken!); - loadAccount(); - accountModal.hide(); - } else { - elements.accountModalErrorText.innerHTML = result.error!; - elements.accountModalError.style.display = "block"; - } - return false; -}); -elements.accountSettingsForm.addEventListener('submit', async e => { - e.preventDefault(); - var oldUsername = auth!.account!.username; - var oldEmail = auth!.account!.email; - var username = elements.accountSettingsUsername.value === auth!.account!.username ? undefined : elements.accountSettingsUsername.value; - var email = elements.accountSettingsEmail.value === auth!.account!.email ? undefined : elements.accountSettingsEmail.value; - var password = elements.accountSettingsNewPassword.value === "" ? undefined : elements.accountSettingsNewPassword.value; - var currentPassword = elements.accountSettingsCurrentPassword.value; - if (password && password !== elements.accountSettingsConfirmNewPassword.value) { - elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kPasswordsMustMatch); - elements.accountModalError.style.display = "block"; - return false; - } - localStorage.setItem("collabvm-hide-flag", JSON.stringify(elements.hideFlagCheckbox.checked)); - if (!password && !email && !username) { - accountModal.hide(); - return false - } - var result = await auth!.updateAccount(currentPassword, email, username, password); - if (result.success) { - elements.accountSettingsNewPassword.value = ""; - elements.accountSettingsConfirmNewPassword.value = ""; - elements.accountSettingsCurrentPassword.value = ""; - if (result.verificationRequired) { - renderAuth(); - accountBeingVerified = username ?? oldUsername; - elements.accountVerifyEmailText.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_VerifyText, email ?? oldEmail); - elements.accountSettingsSection.style.display = "none"; - elements.accountVerifyEmailSection.style.display = "block"; - return false; - } else if (result.sessionExpired) { - accountModal.hide(); - localStorage.removeItem("collabvm_session_" + new URL(auth!.apiEndpoint).host); - if (VM) closeVM(); - renderAuth(); - } else { - accountModal.hide(); - } - } else { - elements.accountModalErrorText.innerHTML = result.error!; - elements.accountModalError.style.display = "block"; - } - return false; -}); -let resetPasswordUsername; -let resetPasswordEmail; -elements.accountResetPasswordForm.addEventListener('submit', async e => { - e.preventDefault(); - var hcaptchaToken = undefined; - var hcaptchaID = undefined; - if (auth!.info!.hcaptcha?.required) { - hcaptchaID = elements.accountResetPasswordCaptchaContainer.getAttribute("data-hcaptcha-widget-id")! - var response = hcaptcha.getResponse(hcaptchaID); - if (response === "") { - elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha); - elements.accountModalError.style.display = "block"; - return false; - } - hcaptchaToken = response; - } - - var turnstileToken = undefined; - var turnstileID = undefined; - - if (auth!.info!.turnstile?.required) { - turnstileID = elements.accountResetPasswordTurnstileContainer.getAttribute("data-turnstile-widget-id")! - var response: string = turnstile.getResponse(turnstileID) || ""; - if (response === "") { - elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha); - elements.accountModalError.style.display = "block"; - return false; - } - turnstileToken = response; - } - - var recaptchaToken = undefined; - var recaptchaID = undefined; - - if (auth!.info!.recaptcha?.required) { - recaptchaID = parseInt(elements.accountResetPasswordRecaptchaContainer.getAttribute("data-recaptcha-widget-id")!) - var response = grecaptcha.getResponse(recaptchaID); - if (response === "") { - elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kMissingCaptcha); - elements.accountModalError.style.display = "block"; - return false; - } - recaptchaToken = response; - } - - var username = elements.accountResetPasswordUsername.value; - var email = elements.accountResetPasswordEmail.value; - var result = await auth!.sendPasswordResetEmail(username, email, hcaptchaToken, turnstileToken, recaptchaToken); - if (auth!.info!.hcaptcha?.required) hcaptcha.reset(hcaptchaID); - if (auth!.info!.turnstile?.required) turnstile.reset(turnstileID); - if (auth!.info!.recaptcha?.required) grecaptcha.reset(recaptchaID); - if (result.success) { - resetPasswordUsername = username; - resetPasswordEmail = email; - elements.accountResetPasswordUsername.value = ""; - elements.accountResetPasswordEmail.value = ""; - elements.accountVerifyPasswordResetText.innerText = TheI18n.GetString(I18nStringKey.kAccountModal_VerifyPasswordResetText, email); - elements.accountResetPasswordSection.style.display = "none"; - elements.accountResetPasswordVerifySection.style.display = "block"; - } else { - elements.accountModalErrorText.innerHTML = result.error!; - elements.accountModalError.style.display = "block"; - } - return false; -}); -elements.accountResetPasswordVerifyForm.addEventListener('submit', async e => { - e.preventDefault(); - var code = elements.accountResetPasswordCode.value; - var password = elements.accountResetPasswordNewPassword.value; - if (password !== elements.accountResetPasswordConfirmNewPassword.value) { - elements.accountModalErrorText.innerHTML = TheI18n.GetString(I18nStringKey.kPasswordsMustMatch); - elements.accountModalError.style.display = "block"; - return false; - } - var result = await auth!.resetPassword(resetPasswordUsername!, resetPasswordEmail!, code, password); - if (result.success) { - elements.accountResetPasswordCode.value = ""; - elements.accountResetPasswordNewPassword.value = ""; - elements.accountResetPasswordConfirmNewPassword.value = ""; - elements.accountModalSuccessText.innerHTML = TheI18n.GetString(I18nStringKey.kAccountModal_PasswordResetSuccess); - elements.accountModalSuccess.style.display = "block"; - elements.accountResetPasswordVerifySection.style.display = "none"; - elements.accountLoginSection.style.display = "block"; - - } else { - elements.accountModalErrorText.innerHTML = result.error!; - elements.accountModalError.style.display = "block"; - } - return false; -}); - -let darkTheme = true; -function loadColorTheme(dark : boolean) { - if (dark) { - darkTheme = true; - document.children[0].setAttribute("data-bs-theme", "dark"); - elements.toggleThemeBtnText.innerHTML = TheI18n.GetString(I18nStringKey.kSiteButtons_LightMode); - elements.toggleThemeIcon.classList.remove("fa-moon"); - elements.toggleThemeIcon.classList.add("fa-sun"); - } else { - darkTheme = false; - document.children[0].setAttribute("data-bs-theme", "light"); - elements.toggleThemeBtnText.innerHTML = TheI18n.GetString(I18nStringKey.kSiteButtons_DarkMode); - elements.toggleThemeIcon.classList.remove("fa-sun"); - elements.toggleThemeIcon.classList.add("fa-moon"); - } -} -elements.toggleThemeBtn.addEventListener('click', e => { - e.preventDefault(); - loadColorTheme(!darkTheme); - localStorage.setItem("cvm-dark-theme", darkTheme ? "1" : "0"); - return false; -}); - -// Public API -w.collabvm = { - openVM: openVM, - closeVM: closeVM, - loadList: loadList, - multicollab: multicollab, - getVM: () => VM, - ghostTurn: false, -}; -// Multicollab will stay in the global scope for backwards compatibility -w.multicollab = multicollab; -// Same goes for GetAdmin -w.GetAdmin = () => { - if (VM === null) return; - return { - adminInstruction: (...args: string[]) => { - args.unshift('admin'); - VM?.send(...args); - }, - restore: () => VM!.restore(), - reboot: () => VM!.reboot(), - clearQueue: () => VM!.clearQueue(), - bypassTurn: () => VM!.bypassTurn(), - endTurn: (username: string) => VM!.endTurn(username), - ban: (username: string) => VM!.ban(username), - kick: (username: string) => VM!.kick(username), - renameUser: (oldname: string, newname: string) => VM!.renameUser(oldname, newname), - mute: (username: string, state: number) => VM!.mute(username, state), - getip: (username: string) => VM!.getip(username), - qemuMonitor: (cmd: string) => { - VM?.qemuMonitor(cmd); - return; - }, - globalXss: (msg: string) => VM!.xss(msg), - forceVote: (result: boolean) => VM!.forceVote(result) - }; -}; -// more backwards compatibility -w.cvmEvents = { - on: (event: string | number, cb: (...args: any) => void) => { - if (VM === null) return; - VM.on('message', (...args: any) => cb(...args)); - } -}; -w.VMName = null; - -document.addEventListener('DOMContentLoaded', async () => { - // Initalize the i18n system - await TheI18n.Init(); - TheI18n.on('languageChanged', lang => { - // Update all dynamic text - if (VM) { - document.title = Format('{0} - {1}', VM.getNode()!, TheI18n.GetString(I18nStringKey.kGeneric_CollabVM)); - if (turn !== -1) { - if (turn === 0) elements.turnstatus.innerText = TheI18n.GetString(I18nStringKey.kVM_TurnTimeTimer, turnTimer); - else elements.turnstatus.innerText = TheI18n.GetString(I18nStringKey.kVM_WaitingTurnTimer, turnTimer); - elements.turnBtnText.innerText = TheI18n.GetString(I18nStringKey.kVMButtons_EndTurn); - } - else - elements.turnBtnText.innerText = TheI18n.GetString(I18nStringKey.kVMButtons_TakeTurn); - if (VM!.getVoteStatus()) - elements.voteTimeText.innerText = TheI18n.GetString(I18nStringKey.kVM_VoteForResetTimer, voteTimer); - - } - else { - document.title = TheI18n.GetString(I18nStringKey.kGeneric_CollabVM); - } - if (!auth || !auth.account) elements.accountDropdownUsername.innerText = TheI18n.GetString(I18nStringKey.kNotLoggedIn); - 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); - - for (const user of users) { - if (user.user.countryCode !== null) { - user.flagElement.title = TheI18n.getCountryName(user.user.countryCode); - } - } - }); - // Load theme - var _darktheme : boolean; - // Check if dark theme is set in local storage - if (localStorage.getItem("cvm-dark-theme") !== null) - loadColorTheme(localStorage.getItem("cvm-dark-theme") === "1"); - // Otherwise, try to detect the system theme - else if (window.matchMedia('(prefers-color-scheme: dark)').matches) - loadColorTheme(true); - else - loadColorTheme(false); - // Initialize authentication if enabled - if (Config.Auth.Enabled) { - auth = new AuthManager(Config.Auth.APIEndpoint); - renderAuth(); - } - - var hideFlag = JSON.parse(localStorage.getItem("collabvm-hide-flag")!); - if (hideFlag === null) hideFlag = false; - elements.hideFlagCheckbox.checked = hideFlag; - - document.title = TheI18n.GetString(I18nStringKey.kGeneric_CollabVM); - - // Load all VMs - loadList(); - - // Welcome modal - let welcomeModal = new bootstrap.Modal(document.getElementById('welcomeModal') as HTMLDivElement); - let noWelcomeModal = window.localStorage.getItem(Config.WelcomeModalLocalStorageKey); - if (noWelcomeModal !== '1') { - let welcomeModalDismissBtn = document.getElementById('welcomeModalDismiss') as HTMLButtonElement; - welcomeModalDismissBtn.addEventListener('click', () => { - window.localStorage.setItem(Config.WelcomeModalLocalStorageKey, '1'); - }); - welcomeModalDismissBtn.disabled = true; - welcomeModal.show(); - setTimeout(() => { - welcomeModalDismissBtn.disabled = false; - }, 5000); - } - elements.rulesBtn.addEventListener('click', e => { - if (TheI18n.CurrentLanguage() !== "en-us") { - e.preventDefault(); - welcomeModal.show(); - } - }); -}); diff --git a/src/ts/protocol/CollabVMClient.ts b/src/ts/protocol/CollabVMClient.ts index e2b4201..a5c694c 100644 --- a/src/ts/protocol/CollabVMClient.ts +++ b/src/ts/protocol/CollabVMClient.ts @@ -68,8 +68,8 @@ export default class CollabVMClient { private ctx: CanvasRenderingContext2D; private url: string; private connectedToVM: boolean = false; - private users: User[] = []; - private username: string | null = null; + users: User[] = []; + username: string | null = null; private mouse: Mouse = new Mouse(); private rank: Rank = Rank.Unregistered; private perms: Permissions = new Permissions(0); @@ -205,7 +205,7 @@ export default class CollabVMClient { private onBinaryMessage(data: ArrayBuffer) { let msg: CollabVMProtocolMessage; try { - msg = msgpack.decode(data); + msg = msgpack.decode(new Uint8Array(data)); } catch { console.error("Server sent invalid binary message"); return; @@ -523,13 +523,11 @@ export default class CollabVMClient { 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 + thumbnailSrc: 'data:image/jpeg;base64,' + list[i + 2] }); } res(vms); diff --git a/src/ts/protocol/ListNodes.ts b/src/ts/protocol/ListNodes.ts new file mode 100644 index 0000000..5527ad2 --- /dev/null +++ b/src/ts/protocol/ListNodes.ts @@ -0,0 +1,17 @@ +import CollabVMClient from "./CollabVMClient.js"; +import VM from "./VM.js"; + +export async function ListNodes(server: string): Promise { + // Create the client + let client = new CollabVMClient(server); + + await client.WaitForOpen(); + + // Get the list of VMs + let list = await client.list(); + + // Close the client + client.close(); + + return list; +} \ No newline at end of file diff --git a/src/ts/protocol/TurnStatus.ts b/src/ts/protocol/TurnStatus.ts index a39b6ac..98c7a77 100644 --- a/src/ts/protocol/TurnStatus.ts +++ b/src/ts/protocol/TurnStatus.ts @@ -10,3 +10,9 @@ export default interface TurnStatus { // Amount of time until the user gets their turn. Null unless the user is in the queue queueTime: number | null; } + +export enum TurnState { + None, + Waiting, + HasTurn +} \ No newline at end of file diff --git a/src/ts/protocol/VM.ts b/src/ts/protocol/VM.ts index 2c6dfed..486d85e 100644 --- a/src/ts/protocol/VM.ts +++ b/src/ts/protocol/VM.ts @@ -5,5 +5,5 @@ export default interface VM { displayName: string; - thumbnail: HTMLImageElement; + thumbnailSrc: string; } diff --git a/src/vue/app.vue b/src/vue/app.vue new file mode 100644 index 0000000..c832493 --- /dev/null +++ b/src/vue/app.vue @@ -0,0 +1,111 @@ + + + \ No newline at end of file diff --git a/src/vue/bsmodal.vue b/src/vue/bsmodal.vue new file mode 100644 index 0000000..6796348 --- /dev/null +++ b/src/vue/bsmodal.vue @@ -0,0 +1,87 @@ + + + \ No newline at end of file diff --git a/src/vue/langdropdown.vue b/src/vue/langdropdown.vue new file mode 100644 index 0000000..50f834b --- /dev/null +++ b/src/vue/langdropdown.vue @@ -0,0 +1,38 @@ + + + \ No newline at end of file diff --git a/src/vue/localestring.vue b/src/vue/localestring.vue new file mode 100644 index 0000000..278b0ed --- /dev/null +++ b/src/vue/localestring.vue @@ -0,0 +1,42 @@ + + + \ No newline at end of file diff --git a/src/vue/navbar.vue b/src/vue/navbar.vue new file mode 100644 index 0000000..a847efe --- /dev/null +++ b/src/vue/navbar.vue @@ -0,0 +1,79 @@ + + + \ No newline at end of file diff --git a/src/vue/vmlist/vmlist.vue b/src/vue/vmlist/vmlist.vue new file mode 100644 index 0000000..9e13a69 --- /dev/null +++ b/src/vue/vmlist/vmlist.vue @@ -0,0 +1,53 @@ + + + \ No newline at end of file diff --git a/src/vue/vmlist/vmlistcard.vue b/src/vue/vmlist/vmlistcard.vue new file mode 100644 index 0000000..6d7be64 --- /dev/null +++ b/src/vue/vmlist/vmlistcard.vue @@ -0,0 +1,22 @@ + + + \ No newline at end of file diff --git a/src/vue/vmview/turncounter.vue b/src/vue/vmview/turncounter.vue new file mode 100644 index 0000000..a186d84 --- /dev/null +++ b/src/vue/vmview/turncounter.vue @@ -0,0 +1,51 @@ + + + \ No newline at end of file diff --git a/src/vue/vmview/vmbtns.vue b/src/vue/vmview/vmbtns.vue new file mode 100644 index 0000000..b1e2aab --- /dev/null +++ b/src/vue/vmview/vmbtns.vue @@ -0,0 +1,120 @@ + + + \ No newline at end of file diff --git a/src/vue/vmview/vmchat.vue b/src/vue/vmview/vmchat.vue new file mode 100644 index 0000000..11e6542 --- /dev/null +++ b/src/vue/vmview/vmchat.vue @@ -0,0 +1,63 @@ + + + \ No newline at end of file diff --git a/src/vue/vmview/vmusertable.vue b/src/vue/vmview/vmusertable.vue new file mode 100644 index 0000000..0764c74 --- /dev/null +++ b/src/vue/vmview/vmusertable.vue @@ -0,0 +1,96 @@ + + + \ No newline at end of file diff --git a/src/vue/vmview/vmview.vue b/src/vue/vmview/vmview.vue new file mode 100644 index 0000000..4800622 --- /dev/null +++ b/src/vue/vmview/vmview.vue @@ -0,0 +1,83 @@ + + + \ No newline at end of file diff --git a/src/vue/welcomemodal.vue b/src/vue/welcomemodal.vue new file mode 100644 index 0000000..7a14d7c --- /dev/null +++ b/src/vue/welcomemodal.vue @@ -0,0 +1,64 @@ + + + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 9bbe535..4b9eb7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -127,11 +127,21 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + "@babel/helper-validator-identifier@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + "@babel/helper-validator-option@^7.23.5": version "7.23.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" @@ -160,6 +170,13 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.0.tgz#26a3d1ff49031c53a97d03b604375f028746a9ac" integrity sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg== +"@babel/parser@^7.25.3": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.0.tgz#3d7d6ee268e41d2600091cbd4e145ffee85a44ec" + integrity sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg== + dependencies: + "@babel/types" "^7.27.0" + "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" @@ -292,15 +309,56 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@babel/types@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.0.tgz#ef9acb6b06c3173f6632d993ecb6d4ae470b4559" + integrity sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@fortawesome/fontawesome-free@^6.6.0": - version "6.6.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz#0e984f0f2344ee513c185d87d77defac4c0c8224" - integrity sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow== +"@fortawesome/fontawesome-common-types@6.7.2": + version "6.7.2" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz#7123d74b0c1e726794aed1184795dbce12186470" + integrity sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg== + +"@fortawesome/fontawesome-svg-core@^6.7.2": + version "6.7.2" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz#0ac6013724d5cc327c1eb81335b91300a4fce2f2" + integrity sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA== + dependencies: + "@fortawesome/fontawesome-common-types" "6.7.2" + +"@fortawesome/free-brands-svg-icons@^6.7.2": + version "6.7.2" + resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.2.tgz#4ebee8098f31da5446dda81edc344025eb59b27e" + integrity sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q== + dependencies: + "@fortawesome/fontawesome-common-types" "6.7.2" + +"@fortawesome/free-regular-svg-icons@^6.7.2": + version "6.7.2" + resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz#f1651e55e6651a15589b0569516208f9c65f96db" + integrity sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g== + dependencies: + "@fortawesome/fontawesome-common-types" "6.7.2" + +"@fortawesome/free-solid-svg-icons@^6.7.2": + version "6.7.2" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz#fe25883b5eb8464a82918599950d283c465b57f6" + integrity sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA== + dependencies: + "@fortawesome/fontawesome-common-types" "6.7.2" + +"@fortawesome/vue-fontawesome@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.8.tgz#1e8032df151173d8174ac9f5a28da3c0f5a495e4" + integrity sha512-yyHHAj4G8pQIDfaIsMvQpwKMboIZtcHTUvPqXjOHyldh1O1vZfH4W03VDPv5RvI9P6DLTzJQlmVgj9wCf7c2Fw== "@hcaptcha/types@^1.0.4": version "1.0.4" @@ -539,6 +597,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== +"@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" @@ -1155,6 +1218,15 @@ "@parcel/utils" "2.12.0" react-refresh "^0.9.0" +"@parcel/transformer-sass@2.12.0": + version "2.12.0" + resolved "https://registry.yarnpkg.com/@parcel/transformer-sass/-/transformer-sass-2.12.0.tgz#9132ee78197db04baf51d3024a1bf3c35f1df5ef" + integrity sha512-xLLoSLPST+2AHJwFRLl4foArDjjy6P1RChP3TxMU2MVS1sbKGJnfFhFpHAacH8ASjuGtu5rbpfpHRZePlvoZxw== + dependencies: + "@parcel/plugin" "2.12.0" + "@parcel/source-map" "^2.1.1" + sass "^1.38.0" + "@parcel/transformer-svg@2.12.0": version "2.12.0" resolved "https://registry.yarnpkg.com/@parcel/transformer-svg/-/transformer-svg-2.12.0.tgz#0281e89bf0f438ec161c19b59a8a8978434a3621" @@ -1169,6 +1241,20 @@ posthtml-render "^3.0.0" semver "^7.5.2" +"@parcel/transformer-vue@2.12.0": + version "2.12.0" + resolved "https://registry.yarnpkg.com/@parcel/transformer-vue/-/transformer-vue-2.12.0.tgz#f291a55875a5132b9ff9008f4c131c488a8a1c36" + integrity sha512-WZNlNUDymAD+3b2BOGHRsW66rlmW7E9QXa6CTrkPnR6gXX1c9lY7srxSt1Lb+c8zDJHr4Gw7Y0kz8gj921S/aQ== + dependencies: + "@parcel/diagnostic" "2.12.0" + "@parcel/plugin" "2.12.0" + "@parcel/source-map" "^2.1.1" + "@parcel/utils" "2.12.0" + "@vue/compiler-sfc" "^3.2.27" + consolidate "^0.16.0" + nullthrows "^1.1.1" + semver "^7.5.2" + "@parcel/types@2.12.0": version "2.12.0" resolved "https://registry.yarnpkg.com/@parcel/types/-/types-2.12.0.tgz#caf0af00ee0c7228b350eca5f4d3a5b85ce457ad" @@ -1201,61 +1287,126 @@ resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz#c2c19a3c442313ff007d2d7a9c2c1dd3e1c9ca84" integrity sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg== +"@parcel/watcher-android-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1" + integrity sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA== + "@parcel/watcher-darwin-arm64@2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz#c817c7a3b4f3a79c1535bfe54a1c2818d9ffdc34" integrity sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA== +"@parcel/watcher-darwin-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz#3d26dce38de6590ef79c47ec2c55793c06ad4f67" + integrity sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw== + "@parcel/watcher-darwin-x64@2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz#1a3f69d9323eae4f1c61a5f480a59c478d2cb020" integrity sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg== +"@parcel/watcher-darwin-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz#99f3af3869069ccf774e4ddfccf7e64fd2311ef8" + integrity sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg== + "@parcel/watcher-freebsd-x64@2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz#0d67fef1609f90ba6a8a662bc76a55fc93706fc8" integrity sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w== +"@parcel/watcher-freebsd-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz#14d6857741a9f51dfe51d5b08b7c8afdbc73ad9b" + integrity sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ== + "@parcel/watcher-linux-arm-glibc@2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz#ce5b340da5829b8e546bd00f752ae5292e1c702d" integrity sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA== +"@parcel/watcher-linux-arm-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz#43c3246d6892381db473bb4f663229ad20b609a1" + integrity sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA== + +"@parcel/watcher-linux-arm-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz#663750f7090bb6278d2210de643eb8a3f780d08e" + integrity sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q== + "@parcel/watcher-linux-arm64-glibc@2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz#6d7c00dde6d40608f9554e73998db11b2b1ff7c7" integrity sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA== +"@parcel/watcher-linux-arm64-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz#ba60e1f56977f7e47cd7e31ad65d15fdcbd07e30" + integrity sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w== + "@parcel/watcher-linux-arm64-musl@2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz#bd39bc71015f08a4a31a47cd89c236b9d6a7f635" integrity sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA== +"@parcel/watcher-linux-arm64-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz#f7fbcdff2f04c526f96eac01f97419a6a99855d2" + integrity sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg== + "@parcel/watcher-linux-x64-glibc@2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz#0ce29966b082fb6cdd3de44f2f74057eef2c9e39" integrity sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg== +"@parcel/watcher-linux-x64-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz#4d2ea0f633eb1917d83d483392ce6181b6a92e4e" + integrity sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A== + "@parcel/watcher-linux-x64-musl@2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz#d2ebbf60e407170bb647cd6e447f4f2bab19ad16" integrity sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ== +"@parcel/watcher-linux-x64-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz#277b346b05db54f55657301dd77bdf99d63606ee" + integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg== + "@parcel/watcher-win32-arm64@2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz#eb4deef37e80f0b5e2f215dd6d7a6d40a85f8adc" integrity sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg== +"@parcel/watcher-win32-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz#7e9e02a26784d47503de1d10e8eab6cceb524243" + integrity sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw== + "@parcel/watcher-win32-ia32@2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz#94fbd4b497be39fd5c8c71ba05436927842c9df7" integrity sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw== +"@parcel/watcher-win32-ia32@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz#2d0f94fa59a873cdc584bf7f6b1dc628ddf976e6" + integrity sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ== + "@parcel/watcher-win32-x64@2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz#4bf920912f67cae5f2d264f58df81abfea68dadf" integrity sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A== +"@parcel/watcher-win32-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz#ae52693259664ba6f2228fa61d7ee44b64ea0947" + integrity sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA== + "@parcel/watcher@^2.0.7": version "2.4.1" resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.4.1.tgz#a50275151a1bb110879c6123589dba90c19f1bf8" @@ -1279,6 +1430,30 @@ "@parcel/watcher-win32-ia32" "2.4.1" "@parcel/watcher-win32-x64" "2.4.1" +"@parcel/watcher@^2.4.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.1.tgz#342507a9cfaaf172479a882309def1e991fb1200" + integrity sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg== + dependencies: + detect-libc "^1.0.3" + is-glob "^4.0.3" + micromatch "^4.0.5" + node-addon-api "^7.0.0" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.5.1" + "@parcel/watcher-darwin-arm64" "2.5.1" + "@parcel/watcher-darwin-x64" "2.5.1" + "@parcel/watcher-freebsd-x64" "2.5.1" + "@parcel/watcher-linux-arm-glibc" "2.5.1" + "@parcel/watcher-linux-arm-musl" "2.5.1" + "@parcel/watcher-linux-arm64-glibc" "2.5.1" + "@parcel/watcher-linux-arm64-musl" "2.5.1" + "@parcel/watcher-linux-x64-glibc" "2.5.1" + "@parcel/watcher-linux-x64-musl" "2.5.1" + "@parcel/watcher-win32-arm64" "2.5.1" + "@parcel/watcher-win32-ia32" "2.5.1" + "@parcel/watcher-win32-x64" "2.5.1" + "@parcel/workers@2.12.0": version "2.12.0" resolved "https://registry.yarnpkg.com/@parcel/workers/-/workers-2.12.0.tgz#773182b5006741102de8ae36d18a5a9e3320ebd1" @@ -1406,6 +1581,21 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== +"@twemoji/api@^15.1.0": + version "15.1.0" + resolved "https://registry.yarnpkg.com/@twemoji/api/-/api-15.1.0.tgz#66fc74c74accbcb6376d977560a03bba5215923c" + integrity sha512-CySXR4/e6vY0z9lC2tW9EVjsH6zzz2/LQvLRMwwpHsDI4xcaweSIP+LcESf1Mq7w2/zcrrhG4ozniMvpSsEXag== + dependencies: + "@twemoji/parser" "15.1.0" + fs-extra "^8.0.1" + jsonfile "^5.0.0" + universalify "^0.1.2" + +"@twemoji/parser@15.1.0": + version "15.1.0" + resolved "https://registry.yarnpkg.com/@twemoji/parser/-/parser-15.1.0.tgz#ad83b69fcd1fbcb705022f2b85d73e127ad5f335" + integrity sha512-3HTiSxPvkWUJ4kZeCvwyKlIwkpTUfBOk6igpBBRQni58ceQMv5YK4smkc8vX/eqOlMMNER/9qobv+Q6Q8LVrqA== + "@types/babel__core@^7.1.14": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" @@ -1526,6 +1716,86 @@ dependencies: "@types/yargs-parser" "*" +"@vue/compiler-core@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.13.tgz#b0ae6c4347f60c03e849a05d34e5bf747c9bda05" + integrity sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q== + dependencies: + "@babel/parser" "^7.25.3" + "@vue/shared" "3.5.13" + entities "^4.5.0" + estree-walker "^2.0.2" + source-map-js "^1.2.0" + +"@vue/compiler-dom@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz#bb1b8758dbc542b3658dda973b98a1c9311a8a58" + integrity sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA== + dependencies: + "@vue/compiler-core" "3.5.13" + "@vue/shared" "3.5.13" + +"@vue/compiler-sfc@3.5.13", "@vue/compiler-sfc@^3.2.27": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz#461f8bd343b5c06fac4189c4fef8af32dea82b46" + integrity sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ== + dependencies: + "@babel/parser" "^7.25.3" + "@vue/compiler-core" "3.5.13" + "@vue/compiler-dom" "3.5.13" + "@vue/compiler-ssr" "3.5.13" + "@vue/shared" "3.5.13" + estree-walker "^2.0.2" + magic-string "^0.30.11" + postcss "^8.4.48" + source-map-js "^1.2.0" + +"@vue/compiler-ssr@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz#e771adcca6d3d000f91a4277c972a996d07f43ba" + integrity sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA== + dependencies: + "@vue/compiler-dom" "3.5.13" + "@vue/shared" "3.5.13" + +"@vue/reactivity@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.13.tgz#b41ff2bb865e093899a22219f5b25f97b6fe155f" + integrity sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg== + dependencies: + "@vue/shared" "3.5.13" + +"@vue/runtime-core@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.13.tgz#1fafa4bf0b97af0ebdd9dbfe98cd630da363a455" + integrity sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw== + dependencies: + "@vue/reactivity" "3.5.13" + "@vue/shared" "3.5.13" + +"@vue/runtime-dom@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz#610fc795de9246300e8ae8865930d534e1246215" + integrity sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog== + dependencies: + "@vue/reactivity" "3.5.13" + "@vue/runtime-core" "3.5.13" + "@vue/shared" "3.5.13" + csstype "^3.1.3" + +"@vue/server-renderer@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.13.tgz#429ead62ee51de789646c22efe908e489aad46f7" + integrity sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA== + dependencies: + "@vue/compiler-ssr" "3.5.13" + "@vue/shared" "3.5.13" + +"@vue/shared@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.13.tgz#87b309a6379c22b926e696893237826f64339b6f" + integrity sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ== + abortcontroller-polyfill@^1.1.9: version "1.7.5" resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz#6738495f4e901fbb57b6c0611d0c75f76c485bed" @@ -1664,6 +1934,11 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -1775,6 +2050,13 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +chokidar@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + chrome-trace-event@^1.0.2, chrome-trace-event@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" @@ -1848,6 +2130,13 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +consolidate@^0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.16.0.tgz#a11864768930f2f19431660a65906668f5fbdc16" + integrity sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ== + dependencies: + bluebird "^3.7.2" + convert-source-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" @@ -1916,6 +2205,11 @@ csso@^4.2.0: dependencies: css-tree "^1.1.2" +csstype@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + dayjs@^1.11.13: version "1.11.13" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" @@ -2037,6 +2331,11 @@ entities@^3.0.1: resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== +entities@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -2064,6 +2363,11 @@ esprima@^4.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -2129,6 +2433,15 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +fs-extra@^8.0.1: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -2193,7 +2506,7 @@ globals@^13.2.0: dependencies: type-fest "^0.20.2" -graceful-fs@^4.2.9: +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -2249,6 +2562,11 @@ ieee754@^1.2.1: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== +immutable@^5.0.2: + version "5.1.1" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.1.tgz#d4cb552686f34b076b3dcf23c4384c04424d8354" + integrity sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg== + import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -2793,6 +3111,22 @@ json5@^2.2.0, json5@^2.2.1, json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + +jsonfile@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-5.0.0.tgz#e6b718f73da420d612823996fdf14a03f6ff6922" + integrity sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w== + dependencies: + universalify "^0.1.2" + optionalDependencies: + graceful-fs "^4.1.6" + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -2914,6 +3248,13 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +magic-string@^0.30.11: + version "0.30.17" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453" + integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + make-dir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" @@ -3008,6 +3349,11 @@ nanoevents@^9.1.0: resolved "https://registry.yarnpkg.com/nanoevents/-/nanoevents-9.1.0.tgz#76c8c86ed54a26131ada5a0a1c5682f48690dbb7" integrity sha512-Jd0fILWG44a9luj8v5kED4WI+zfkkgwKyRQKItTtlPfEsh7Lznfi1kr8/iZ+XAIss4Qq5GqRB0qtWbaz9ceO/A== +nanoid@^3.3.8: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -3188,6 +3534,11 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -3210,6 +3561,15 @@ postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== +postcss@^8.4.48: + version "8.5.3" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb" + integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A== + dependencies: + nanoid "^3.3.8" + picocolors "^1.1.1" + source-map-js "^1.2.1" + posthtml-parser@^0.10.1: version "0.10.2" resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.10.2.tgz#df364d7b179f2a6bf0466b56be7b98fd4e97c573" @@ -3281,6 +3641,11 @@ react-refresh@^0.9.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf" integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ== +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== + regenerator-runtime@^0.13.7: version "0.13.11" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" @@ -3332,6 +3697,17 @@ safe-buffer@^5.0.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +sass@^1.38.0: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.86.3.tgz#0a0d9ea97cb6665e73f409639f8533ce057464c9" + integrity sha512-iGtg8kus4GrsGLRDLRBRHY9dNVA78ZaS7xr01cWnS7PEMQyFtTqBiyCrfpTYTZXRWM94akzckYjh8oADfFNTzw== + dependencies: + chokidar "^4.0.0" + immutable "^5.0.2" + source-map-js ">=0.6.2 <2.0.0" + optionalDependencies: + "@parcel/watcher" "^2.4.1" + semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" @@ -3381,6 +3757,11 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.0, source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + source-map-support@0.5.13: version "0.5.13" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" @@ -3575,6 +3956,11 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +universalify@^0.1.0, universalify@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + update-browserslist-db@^1.0.13: version "1.0.13" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" @@ -3597,6 +3983,17 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^2.0.0" +vue@^3.5.13: + version "3.5.13" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.13.tgz#9f760a1a982b09c0c04a867903fc339c9f29ec0a" + integrity sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ== + dependencies: + "@vue/compiler-dom" "3.5.13" + "@vue/compiler-sfc" "3.5.13" + "@vue/runtime-dom" "3.5.13" + "@vue/server-renderer" "3.5.13" + "@vue/shared" "3.5.13" + walker@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f"