diff --git a/src/frontend/character.ts b/src/frontend/character.ts index c38b0d8a..a987e5c5 100644 --- a/src/frontend/character.ts +++ b/src/frontend/character.ts @@ -265,8 +265,8 @@ export const characters: { [characterId: string]: Character } = export const loadCharacters = async (crispMode: boolean) => { - const response = await fetch("/api/characters/" + (crispMode ? "crisp" : "regular") + "?v=" + (window as any).EXPECTED_SERVER_VERSION) + const response = await fetch("/api/characters/" + (crispMode ? "crisp" : "regular") + "?v=" + window.EXPECTED_SERVER_VERSION) const dto = await response.json() Object.keys(characters).forEach(characterId => characters[characterId].setDto(dto[characterId])) -} \ No newline at end of file +} diff --git a/src/frontend/components/chessboard-slot.vue b/src/frontend/components/chessboard-slot.vue index b327b71b..e70e73e9 100644 --- a/src/frontend/components/chessboard-slot.vue +++ b/src/frontend/components/chessboard-slot.vue @@ -2,7 +2,7 @@ import type { PropType } from 'vue' import type { Socket } from 'socket.io-client' -import type { ChessboardStateDto, } from '../types' +import type { ChessboardStateDto } from '../types' import { defineComponent, inject, Ref } from 'vue' @@ -20,9 +20,8 @@ export default defineComponent({ }, data() { - const chessboard: any | null = null return { - chessboard, + chessboard: null as ChessboardInstance | null, visible: false, } }, @@ -57,12 +56,13 @@ export default defineComponent({ { if (!this.chessboardState) return const chessboardElement = document.getElementById("chessboard") + if (!chessboardElement) return const position = this.chessboardState ? (this.chessboardState.fenString || "start") : "start" - this.chessboard = (window as any).Chessboard(chessboardElement, { + this.chessboard = window.Chessboard(chessboardElement, { pieceTheme: 'chess/img/chesspieces/wikipedia/{piece}.png', position, orientation: this.chessboardState.blackUserID == this.myUserId ? "black" : "white", @@ -86,7 +86,7 @@ export default defineComponent({ // if (colorOfMovedPiece != this.chessboardState.turn) // return false }, - onDrop: (source: any, target: any) => + onDrop: (source: string, target: string) => { if (!this.chessboardState) return if (this.chessboardState.blackUserID == this.myUserId diff --git a/src/frontend/components/numeric-value-control.vue b/src/frontend/components/numeric-value-control.vue index 1cc8477d..0f026e3f 100644 --- a/src/frontend/components/numeric-value-control.vue +++ b/src/frontend/components/numeric-value-control.vue @@ -23,10 +23,11 @@ const props = defineProps({ }) const count = ref(props.initialValue) -const emit = defineEmits(['value-changed']) +const emit = defineEmits<{ + 'value-changed': [value: number] +}>() const emitValueChanged = () => { - console.log('emitValueChanged', count.value) emit('value-changed', count.value) } const increment = () => { @@ -60,4 +61,4 @@ const reset = () => { class="value" @click="reset">{{ count }} - \ No newline at end of file + diff --git a/src/frontend/components/voice-changer-control.vue b/src/frontend/components/voice-changer-control.vue index d34573a6..e82de548 100644 --- a/src/frontend/components/voice-changer-control.vue +++ b/src/frontend/components/voice-changer-control.vue @@ -4,23 +4,38 @@ import { AudioProcessor } from '../utils'; import NumericValueControl from './numeric-value-control.vue'; const outboundAudioProcessor = inject('outboundAudioProcessor') as Ref +const pitchShiftMin = -100 +const pitchShiftMax = 200 +const pitchFactorMin = 0.5 +const pitchFactorMax = 2 +const pitchShiftRange = pitchShiftMax - pitchShiftMin +const pitchFactorRange = pitchFactorMax - pitchFactorMin // This component might be destroyed and recreated multiple times, so we need to // get the current pitch factor from the audio processor. // Also the audio processor might not be ready yet, so we need to use a default value. const initialPitchFactor = outboundAudioProcessor.value?.getPitchFactor() ?? 1 -const initialValue = Math.round((initialPitchFactor - 1) * 200) +const initialValue = Math.round(Math.min( + pitchShiftMax, + Math.max( + pitchShiftMin, + ((initialPitchFactor - pitchFactorMin) / pitchFactorRange) * pitchShiftRange + pitchShiftMin, + ), +)) function onPitchShiftChanged(value: number) { - console.log('onPitchShiftChanged', value, outboundAudioProcessor.value) - // convert value (-100~100) to pitch factor (0.5~1.5) - const pitchFactor = 0.5 + (value + 100) / 200 + // Convert slider values (-100..200) to AudioProcessor pitch factors (0.5..2.0). + const pitchFactor = pitchFactorMin + ((value - pitchShiftMin) / pitchShiftRange) * pitchFactorRange outboundAudioProcessor.value?.setPitchFactor(pitchFactor) } \ No newline at end of file + + diff --git a/src/frontend/main.ts b/src/frontend/main.ts index e7066bb1..755f6939 100644 --- a/src/frontend/main.ts +++ b/src/frontend/main.ts @@ -53,8 +53,8 @@ import $ from "jquery"; import "jquery-ui/themes/base/all.css"; // needed to make $ and jQuery available globally for the jquery-ui plugins. -(window as any).$ = $; -(window as any).jQuery = $; +window.$ = $; +window.jQuery = $; await import("jquery-ui/dist/jquery-ui.js"); await import("jquery-ui-touch-punch"); @@ -235,6 +235,59 @@ function setAppLanguage(code: string) i18next.changeLanguage(code, setPageMetadata) } +type PersistedSettingValues = { + uiTheme: string + isCoinSoundEnabled: boolean + language: string + bubbleOpacity: number + showNotifications: boolean + isLowQualityEnabled: boolean + isCrispModeEnabled: boolean + isIdleAnimationDisabled: boolean + isNameMentionSoundEnabled: boolean + customMentionSoundPattern: string + enableTextToSpeech: boolean + ttsVoiceURI: string + voiceVolume: number +} + +type VerticalSliderConfig = { + selector: string + min: number + max: number + step: number + value: number + onSlide: (value: number) => void +} + +function initializeVerticalSlider({ selector, min, max, step, value, onSlide }: VerticalSliderConfig) +{ + $(selector).slider({ + orientation: "vertical", + range: "min", + min, + max, + step, + value, + slide: (_event, ui) => { + onSlide(ui.value) + } + }) +} + +function makeResizable(target: string | HTMLElement, options?: JQueryUiResizableOptions) +{ + $(target).resizable(options) +} + +function makeVideoContainerResizable(target: string | HTMLElement) +{ + makeResizable(target, { + aspectRatio: true, + resize: adjustNiconicoMessagesFontSize + }) +} + const vueApp = createApp(defineComponent({ components: { ChessboardSlot, @@ -580,33 +633,28 @@ const vueApp = createApp(defineComponent({ } } - // @ts-ignore - $( "#sound-effect-volume" ).slider({ - orientation: "vertical", - range: "min", + initializeVerticalSlider({ + selector: "#sound-effect-volume", min: 0, max: 1, step: 0.01, value: this.soundEffectVolume, - slide: ( event: any, ui: any ) => { - this.changeSoundEffectVolume(ui.value); + onSlide: (value) => { + this.changeSoundEffectVolume(value); } }); - // @ts-ignore - $( "#voice-volume" ).slider({ - orientation: "vertical", - range: "min", + initializeVerticalSlider({ + selector: "#voice-volume", min: 0, max: 100, step: 1, value: this.voiceVolume, - slide: ( event: any, ui: any ) => { - this.changeVoiceVolume(ui.value); + onSlide: (value) => { + this.changeVoiceVolume(value); } }); - // @ts-ignore - $( "#main-section" ).resizable({ + makeResizable("#main-section", { handles: "e" }) @@ -910,11 +958,12 @@ const vueApp = createApp(defineComponent({ }, initializeSocket() { - // @ts-ignore - this.socket = io({ + const socketOptions = { extraHeaders: {"private-user-id": this.myPrivateUserID}, closeOnBeforeunload: false, - }); + } as Parameters[0] + + this.socket = io(socketOptions); const immanentizeConnection = async () => { @@ -2751,7 +2800,6 @@ const vueApp = createApp(defineComponent({ rtcPeer.conn.addEventListener("iceconnectionstatechange", (ev) => { const state = rtcPeer.conn!.iceConnectionState; - console.log("RTC Connection state", state) logToServer(new Date() + " " + this.myUserID + " RTC Connection state " + state) if (state == "connected") @@ -2855,11 +2903,7 @@ const vueApp = createApp(defineComponent({ else this.takeStream(slotId); - // @ts-ignore - $( "#video-container-" + slotId ).resizable({ - aspectRatio: true, - resize: adjustNiconicoMessagesFontSize - }) + makeVideoContainerResizable("#video-container-" + slotId) if (this.slotVolume[slotId] === undefined) this.slotVolume[slotId] = 1 @@ -3234,11 +3278,7 @@ const vueApp = createApp(defineComponent({ const stream = event.streams[0] videoElement.srcObject = stream; - // @ts-ignore - $( "#video-container-" + streamSlotId ).resizable({ - aspectRatio: true, - resize: adjustNiconicoMessagesFontSize - }) + makeVideoContainerResizable("#video-container-" + streamSlotId) if (this.inboundAudioProcessors[streamSlotId]) { @@ -3521,8 +3561,7 @@ const vueApp = createApp(defineComponent({ this.checkBackgroundColor(); for (const knobElement of (document.getElementsByClassName("input-knob") as HTMLCollectionOf)) { - // @ts-ignore what's refresh from? can't find it in docs - knobElement.refresh() + knobElement.refresh?.() } this.isRedrawRequired = true }, @@ -3535,11 +3574,11 @@ const vueApp = createApp(defineComponent({ this.storeSet('language'); this.setLanguage(); }, - storeSet(itemName: string, value?: any) + storeSet(itemName: K, value?: PersistedSettingValues[K]) { - // @ts-ignore - if (value != undefined) this[itemName] = value; - localStorage.setItem(itemName, this[itemName]); + const persistedSettings = this as unknown as PersistedSettingValues + if (value !== undefined) persistedSettings[itemName] = value; + localStorage.setItem(itemName, String(persistedSettings[itemName])); }, handleBubbleOpacity() { @@ -3721,12 +3760,10 @@ const vueApp = createApp(defineComponent({ if (videoContainer.classList.contains("unpinned-video")) { - // @ts-ignore $(videoContainer).draggable() } else { - // @ts-ignore $(videoContainer).draggable("destroy") // Reset 'top' and 'left' styles to snap the container back to its original position videoContainer.setAttribute("style", "") diff --git a/src/frontend/rtcpeer.ts b/src/frontend/rtcpeer.ts index f5cf6160..7a4166a3 100644 --- a/src/frontend/rtcpeer.ts +++ b/src/frontend/rtcpeer.ts @@ -1,5 +1,6 @@ -export type SendCallback = (type: string, msg: string | RTCIceCandidate) => void -export type ErrorCallback = (error: any, event: any) => void +export type RTCSignalType = "offer" | "answer" | "candidate" +export type SendCallback = (type: RTCSignalType, msg: string | RTCIceCandidate) => void +export type ErrorCallback = (error: unknown, event: Event) => void export const defaultIceConfig: RTCConfiguration = { diff --git a/src/frontend/tts.ts b/src/frontend/tts.ts index f027777d..c9df6ce3 100644 --- a/src/frontend/tts.ts +++ b/src/frontend/tts.ts @@ -2,7 +2,7 @@ import { i18n } from "i18next"; import { kanaToRomajiMap, kanjiToKanaMap, katakanaToHiragana } from "./japanese-tools"; import { debounceWithDelayedExecution, urlRegex } from "./utils"; -const synth = new (window as any).Animalese('animalese.wav', function () { }); +const synth = new window.Animalese('animalese.wav', function () { }); function speakAnimalese(text: string, pitch: number | null, volume: number) { // replace every japanese character with a random roman letter diff --git a/src/frontend/types/vendor.d.ts b/src/frontend/types/vendor.d.ts index 02e19f24..0adbb254 100644 --- a/src/frontend/types/vendor.d.ts +++ b/src/frontend/types/vendor.d.ts @@ -1,4 +1,68 @@ -/// - declare module "jquery-ui-touch-punch"; declare module "jquery-ui/dist/jquery-ui.js"; + +interface JQueryUiSliderOptions { + orientation?: "horizontal" | "vertical" + range?: boolean | "min" | "max" + min?: number + max?: number + step?: number + value?: number + slide?: (event: Event, ui: { value: number }) => void +} + +interface JQueryUiResizableOptions { + aspectRatio?: boolean + handles?: string + resize?: (event: Event, ui: unknown) => void +} + +interface JQueryUiDraggableOptions { + containment?: string | Element +} + +interface JQuery { + slider(options: JQueryUiSliderOptions): this + resizable(options?: JQueryUiResizableOptions): this + draggable(options?: JQueryUiDraggableOptions): this + draggable(methodName: "destroy"): this +} + +interface AnimaleseOutput { + dataURI: string +} + +interface AnimaleseInstance { + Animalese(script: string, shorten: boolean, pitch: number): AnimaleseOutput +} + +interface ChessboardInstance { + position(position: string): void +} + +interface ChessboardConfig { + pieceTheme: string + position: string + orientation: "white" | "black" + draggable: boolean + onDragStart?: (...args: unknown[]) => boolean | void + onDrop?: (source: string, target: string) => void + onSnapEnd?: () => void +} + +declare global { + interface HTMLInputElement { + refresh?: () => void + } + + interface Window { + $: JQueryStatic + jQuery: JQueryStatic + Animalese: new (lettersFile: string, onload: () => void) => AnimaleseInstance + Chessboard: (container: string | HTMLElement | null, config: ChessboardConfig) => ChessboardInstance | null + ChessBoard: Window["Chessboard"] + webkitAudioContext?: typeof AudioContext + } +} + +export {} diff --git a/src/frontend/utils.ts b/src/frontend/utils.ts index bf6adda1..127043d9 100644 --- a/src/frontend/utils.ts +++ b/src/frontend/utils.ts @@ -128,9 +128,10 @@ export function calculateRealCoordinates(room: ClientRoom, x: number, y: number) return { x: realX, y: realY } } -export const sleep = (milliseconds: number) => new Promise(resolve => setTimeout(resolve, milliseconds)); +export const sleep = (milliseconds: number): Promise => + new Promise(resolve => setTimeout(resolve, milliseconds)); -export function postJson(url: string, data: any): Promise +export function postJson(url: string, data: T): Promise { return fetch(url, { method: "POST", @@ -166,36 +167,40 @@ export function safeDecodeURI(str: string): string } } -export const debounceWithDelayedExecution = (func: any, wait: number): ((...args: any) => void) => { - let timeout: number; - - return function executedFunction(...args) { - const later = () => { - window.clearTimeout(timeout); - func(...args); - }; - - window.clearTimeout(timeout); - timeout = window.setTimeout(later, wait); +export const debounceWithDelayedExecution = ( + func: (...args: TArgs) => void, + wait: number +): ((...args: TArgs) => void) => { + let timeout: number | undefined; + + return function executedFunction(...args: TArgs) { + if (timeout !== undefined) + window.clearTimeout(timeout); + + timeout = window.setTimeout(() => { + timeout = undefined; + func(...args); + }, wait); }; - }; +}; -// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -export function debounceWithImmediateExecution(func: Function, wait: number) { +export function debounceWithImmediateExecution( + func: (...args: TArgs) => void, + wait: number +) { let lastExecution: number | null = null; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function executedFunction(...args: any[]) { - if (Date.now() - (lastExecution || 0) > wait) - { - lastExecution = Date.now() - func(...args); - } + + return function executedFunction(...args: TArgs) { + if (Date.now() - (lastExecution || 0) > wait) + { + lastExecution = Date.now() + func(...args); + } }; - }; +}; -const AudioContext = window.AudioContext // Default - || (window as any).webkitAudioContext; // Safari and old versions of Chrome +const AudioContextConstructor = window.AudioContext + || window.webkitAudioContext export type VuMeterCallback = (level: number) => void @@ -232,7 +237,10 @@ export class AudioProcessor this.volume = volume; this.vuMeterCallback = vuMeterCallback - this.context = new AudioContext(); + if (!AudioContextConstructor) + throw new Error("AudioContext is not supported in this browser") + + this.context = new AudioContextConstructor(); this.source = this.context.createMediaStreamSource(this.stream); this.destination = this.context.createMediaStreamDestination() this.compressor = this.context.createDynamicsCompressor(); @@ -576,4 +584,4 @@ export function makeUrlsClickable(html: string): string anchor.rel = "noopener noreferrer"; return anchor.outerHTML; }) -} \ No newline at end of file +}