diff --git a/front/src/Api/Events/ui/TriggerMessageEventHandler.ts b/front/src/Api/Events/ui/TriggerMessageEventHandler.ts index fb64c742..f7da0ad2 100644 --- a/front/src/Api/Events/ui/TriggerMessageEventHandler.ts +++ b/front/src/Api/Events/ui/TriggerMessageEventHandler.ts @@ -3,9 +3,9 @@ import { isTriggerActionMessageEvent, removeActionMessage, triggerActionMessage, -} from './TriggerActionMessageEvent'; +} from "./TriggerActionMessageEvent"; -import * as tg from 'generic-type-guard'; +import * as tg from "generic-type-guard"; const isTriggerMessageEventObject = new tg.IsInterface() .withProperties({ diff --git a/front/src/Components/App.svelte b/front/src/Components/App.svelte index ec644c93..d65f699e 100644 --- a/front/src/Components/App.svelte +++ b/front/src/Components/App.svelte @@ -35,6 +35,8 @@ import WarningContainer from "./WarningContainer/WarningContainer.svelte"; import {layoutManagerVisibilityStore} from "../Stores/LayoutManagerStore"; import LayoutManager from "./LayoutManager/LayoutManager.svelte"; + import {audioManagerVisibilityStore} from "../Stores/AudioManagerStore"; + import AudioManager from "./AudioManager/AudioManager.svelte" export let game: Game; @@ -81,6 +83,11 @@ {/if} + {#if $audioManagerVisibilityStore} +
+ +
+ {/if} {#if $layoutManagerVisibilityStore}
diff --git a/front/src/Components/AudioManager/AudioManager.svelte b/front/src/Components/AudioManager/AudioManager.svelte new file mode 100644 index 00000000..a78b4bde --- /dev/null +++ b/front/src/Components/AudioManager/AudioManager.svelte @@ -0,0 +1,119 @@ + + + +
+
+ player volume + +
+
+ +
+ +
+
+
+ + + diff --git a/front/src/Components/images/audio-mute.svg b/front/src/Components/images/audio-mute.svg new file mode 100644 index 00000000..c2ad1eca --- /dev/null +++ b/front/src/Components/images/audio-mute.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/front/src/Components/images/audio.svg b/front/src/Components/images/audio.svg new file mode 100644 index 00000000..190f7612 --- /dev/null +++ b/front/src/Components/images/audio.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/front/src/Phaser/Components/TextInput.ts b/front/src/Phaser/Components/TextInput.ts deleted file mode 100644 index a8ea772f..00000000 --- a/front/src/Phaser/Components/TextInput.ts +++ /dev/null @@ -1,90 +0,0 @@ - -const IGNORED_KEYS = new Set([ - 'Esc', - 'Escape', - 'Alt', - 'Meta', - 'Control', - 'Ctrl', - 'Space', - 'Backspace' -]) - -export class TextInput extends Phaser.GameObjects.BitmapText { - private minUnderLineLength = 4; - private underLine: Phaser.GameObjects.Text; - private domInput = document.createElement('input'); - - constructor(scene: Phaser.Scene, x: number, y: number, maxLength: number, text: string, - onChange: (text: string) => void) { - super(scene, x, y, 'main_font', text, 32); - this.setOrigin(0.5).setCenterAlign(); - this.scene.add.existing(this); - - const style = {fontFamily: 'Arial', fontSize: "32px", color: '#ffffff'}; - this.underLine = this.scene.add.text(x, y+1, this.getUnderLineBody(text.length), style); - this.underLine.setOrigin(0.5); - - this.domInput.maxLength = maxLength; - this.domInput.style.opacity = "0"; - if (text) { - this.domInput.value = text; - } - - this.domInput.addEventListener('keydown', event => { - if (IGNORED_KEYS.has(event.key)) { - return; - } - - if (!/[a-zA-Z0-9:.!&?()+-]/.exec(event.key)) { - event.preventDefault(); - } - }); - - this.domInput.addEventListener('input', (event) => { - if (event.defaultPrevented) { - return; - } - this.text = this.domInput.value; - this.underLine.text = this.getUnderLineBody(this.text.length); - onChange(this.text); - }); - - document.body.append(this.domInput); - this.focus(); - } - - private getUnderLineBody(textLength:number): string { - if (textLength < this.minUnderLineLength) textLength = this.minUnderLineLength; - let text = '_______'; - for (let i = this.minUnderLineLength; i < textLength; i++) { - text += '__'; - } - return text; - } - - getText(): string { - return this.text; - } - - setX(x: number): this { - super.setX(x); - this.underLine.x = x; - return this; - } - - setY(y: number): this { - super.setY(y); - this.underLine.y = y+1; - return this; - } - - focus() { - this.domInput.focus(); - } - - destroy(): void { - super.destroy(); - this.domInput.remove(); - } -} diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 37d18acf..ce947224 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -32,7 +32,6 @@ import type { RoomConnection } from "../../Connexion/RoomConnection"; import { Room } from "../../Connexion/Room"; import { jitsiFactory } from "../../WebRtc/JitsiFactory"; import { urlManager } from "../../Url/UrlManager"; -import { audioManager } from "../../WebRtc/AudioManager"; import { TextureError } from "../../Exception/TextureError"; import { localUserStore } from "../../Connexion/LocalUserStore"; import { HtmlUtils } from "../../WebRtc/HtmlUtils"; @@ -84,6 +83,11 @@ import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStor import { SharedVariablesManager } from "./SharedVariablesManager"; import { playersStore } from "../../Stores/PlayersStore"; import { chatVisibilityStore } from "../../Stores/ChatStore"; +import { + audioManagerFileStore, + audioManagerVisibilityStore, + audioManagerVolumeStore, +} from "../../Stores/AudioManagerStore"; import { PropertyUtils } from "../Map/PropertyUtils"; import Tileset = Phaser.Tilemaps.Tileset; import { userIsAdminStore } from "../../Stores/GameStore"; @@ -727,12 +731,12 @@ export class GameScene extends DirtyScene { this.simplePeer.registerPeerConnectionListener({ onConnect(peer) { //self.openChatIcon.setVisible(true); - audioManager.decreaseVolume(); + audioManagerVolumeStore.setTalking(true); }, onDisconnect(userId: number) { if (self.simplePeer.getNbConnections() === 0) { //self.openChatIcon.setVisible(false); - audioManager.restoreVolume(); + audioManagerVolumeStore.setTalking(false); } }, }); @@ -898,14 +902,16 @@ export class GameScene extends DirtyScene { const volume = allProps.get(AUDIO_VOLUME_PROPERTY) as number | undefined; const loop = allProps.get(AUDIO_LOOP_PROPERTY) as boolean | undefined; newValue === undefined - ? audioManager.unloadAudio() - : audioManager.playAudio(newValue, this.getMapDirUrl(), volume, loop); + ? audioManagerFileStore.unloadAudio() + : audioManagerFileStore.playAudio(newValue, this.getMapDirUrl(), volume, loop); + audioManagerVisibilityStore.set(!(newValue === undefined)); }); // TODO: This legacy property should be removed at some point this.gameMap.onPropertyChange("playAudioLoop", (newValue, oldValue) => { newValue === undefined - ? audioManager.unloadAudio() - : audioManager.playAudio(newValue, this.getMapDirUrl(), undefined, true); + ? audioManagerFileStore.unloadAudio() + : audioManagerFileStore.playAudio(newValue, this.getMapDirUrl(), undefined, true); + audioManagerVisibilityStore.set(!(newValue === undefined)); }); this.gameMap.onPropertyChange("zone", (newValue, oldValue) => { @@ -1323,7 +1329,7 @@ ${escapedMessage} } this.stopJitsi(); - audioManager.unloadAudio(); + audioManagerFileStore.unloadAudio(); // We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map. this.connection?.closeConnection(); this.simplePeer?.closeAllConnections(); diff --git a/front/src/Stores/AudioManagerStore.ts b/front/src/Stores/AudioManagerStore.ts new file mode 100644 index 00000000..1bf30ea3 --- /dev/null +++ b/front/src/Stores/AudioManagerStore.ts @@ -0,0 +1,105 @@ +import { get, writable } from "svelte/store"; + +export interface audioManagerVolume { + muted: boolean; + volume: number; + decreaseWhileTalking: boolean; + volumeReduced: boolean; + loop: boolean; + talking: boolean; +} + +function createAudioManagerVolumeStore() { + const { subscribe, update } = writable({ + muted: false, + volume: 1, + decreaseWhileTalking: true, + volumeReduced: false, + loop: false, + talking: false, + }); + + return { + subscribe, + setMuted: (newMute: boolean): void => { + update((audioPlayerVolume: audioManagerVolume) => { + audioPlayerVolume.muted = newMute; + return audioPlayerVolume; + }); + }, + setVolume: (newVolume: number): void => { + update((audioPlayerVolume: audioManagerVolume) => { + audioPlayerVolume.volume = newVolume; + return audioPlayerVolume; + }); + }, + setDecreaseWhileTalking: (newDecrease: boolean): void => { + update((audioManagerVolume: audioManagerVolume) => { + audioManagerVolume.decreaseWhileTalking = newDecrease; + return audioManagerVolume; + }); + }, + setVolumeReduced: (newVolumeReduced: boolean): void => { + update((audioManagerVolume: audioManagerVolume) => { + audioManagerVolume.volumeReduced = newVolumeReduced; + return audioManagerVolume; + }); + }, + setLoop: (newLoop: boolean): void => { + update((audioManagerVolume: audioManagerVolume) => { + audioManagerVolume.loop = newLoop; + return audioManagerVolume; + }); + }, + setTalking: (newTalk: boolean): void => { + update((audioManagerVolume: audioManagerVolume) => { + audioManagerVolume.talking = newTalk; + return audioManagerVolume; + }); + }, + }; +} + +function createAudioManagerFileStore() { + const { subscribe, update } = writable(""); + + return { + subscribe, + playAudio: ( + url: string | number | boolean, + mapDirUrl: string, + volume: number | undefined, + loop = false + ): void => { + update((file: string) => { + const audioPath = url as string; + + if (audioPath.indexOf("://") > 0) { + // remote file or stream + file = audioPath; + } else { + // local file, include it relative to map directory + file = mapDirUrl + "/" + url; + } + audioManagerVolumeStore.setVolume( + volume ? Math.min(volume, get(audioManagerVolumeStore).volume) : get(audioManagerVolumeStore).volume + ); + audioManagerVolumeStore.setLoop(loop); + + return file; + }); + }, + unloadAudio: () => { + update((file: string) => { + audioManagerVolumeStore.setLoop(false); + return ""; + }); + }, + }; +} + +export const audioManagerVisibilityStore = writable(false); + +export const audioManagerVolumeStore = createAudioManagerVolumeStore(); + +export const audioManagerFileStore = createAudioManagerFileStore(); diff --git a/front/src/WebRtc/AudioManager.ts b/front/src/WebRtc/AudioManager.ts deleted file mode 100644 index 60255a77..00000000 --- a/front/src/WebRtc/AudioManager.ts +++ /dev/null @@ -1,188 +0,0 @@ -import {HtmlUtils} from "./HtmlUtils"; -import {isUndefined} from "generic-type-guard"; -import {localUserStore} from "../Connexion/LocalUserStore"; - -enum audioStates { - closed = 0, - loading = 1, - playing = 2 -} - -const audioPlayerDivId = "audioplayer"; -const audioPlayerCtrlId = "audioplayerctrl"; -const audioPlayerVolId = "audioplayer_volume"; -const audioPlayerMuteId = "audioplayer_volume_icon_playing"; -const animationTime = 500; - -class AudioManager { - private opened = audioStates.closed; - - private audioPlayerDiv: HTMLDivElement; - private audioPlayerCtrl: HTMLDivElement; - private audioPlayerElem: HTMLAudioElement | undefined; - private audioPlayerVol: HTMLInputElement; - private audioPlayerMute: HTMLInputElement; - - private volume = 1; - private muted = false; - private decreaseWhileTalking = true; - private volumeReduced = false; - - constructor() { - this.audioPlayerDiv = HtmlUtils.getElementByIdOrFail(audioPlayerDivId); - this.audioPlayerCtrl = HtmlUtils.getElementByIdOrFail(audioPlayerCtrlId); - this.audioPlayerVol = HtmlUtils.getElementByIdOrFail(audioPlayerVolId); - this.audioPlayerMute = HtmlUtils.getElementByIdOrFail(audioPlayerMuteId); - - this.volume = localUserStore.getAudioPlayerVolume(); - this.audioPlayerVol.value = '' + this.volume; - - this.muted = localUserStore.getAudioPlayerMuted(); - if (this.muted) { - this.audioPlayerMute.classList.add('muted'); - } - } - - public playAudio(url: string|number|boolean, mapDirUrl: string, volume: number|undefined, loop=false): void { - const audioPath = url as string; - let realAudioPath = ''; - - if (audioPath.indexOf('://') > 0) { - // remote file or stream - realAudioPath = audioPath; - } else { - // local file, include it relative to map directory - realAudioPath = mapDirUrl + '/' + url; - } - - this.loadAudio(realAudioPath, volume); - - if (loop) { - this.loop(); - } - } - - private close(): void { - this.audioPlayerCtrl.classList.remove('loading'); - this.audioPlayerCtrl.classList.add('hidden'); - this.opened = audioStates.closed; - } - - private load(): void { - this.audioPlayerCtrl.classList.remove('hidden'); - this.audioPlayerCtrl.classList.add('loading'); - this.opened = audioStates.loading; - } - - private open(): void { - this.audioPlayerCtrl.classList.remove('hidden', 'loading'); - this.opened = audioStates.playing; - } - - private changeVolume(talking = false): void { - if (isUndefined(this.audioPlayerElem)) { - return; - } - - const reduceVolume = talking && this.decreaseWhileTalking; - if (reduceVolume && !this.volumeReduced) { - this.volume *= 0.5; - } else if (!reduceVolume && this.volumeReduced) { - this.volume *= 2.0; - } - this.volumeReduced = reduceVolume; - - this.audioPlayerElem.volume = this.volume; - this.audioPlayerVol.value = '' + this.volume; - this.audioPlayerElem.muted = this.muted; - } - - private setVolume(volume: number): void { - this.volume = volume; - localUserStore.setAudioPlayerVolume(volume); - } - - private loadAudio(url: string, volume: number|undefined): void { - this.load(); - - /* Solution 1, remove whole audio player */ - this.audioPlayerDiv.innerHTML = ''; // necessary, if switching from one audio context to another! (else both streams would play simultaneously) - - this.audioPlayerElem = document.createElement('audio'); - this.audioPlayerElem.id = 'audioplayerelem'; - this.audioPlayerElem.controls = false; - this.audioPlayerElem.preload = 'none'; - - const srcElem = document.createElement('source'); - srcElem.type = "audio/mp3"; - srcElem.src = url; - - this.audioPlayerElem.append(srcElem); - - this.audioPlayerDiv.append(this.audioPlayerElem); - this.volume = volume ? Math.min(volume, this.volume) : this.volume; - this.changeVolume(); - this.audioPlayerElem.play(); - - const muteElem = HtmlUtils.getElementByIdOrFail('audioplayer_mute'); - muteElem.onclick = (ev: Event) => { - this.muted = !this.muted; - this.changeVolume(); - localUserStore.setAudioPlayerMuted(this.muted); - - if (this.muted) { - this.audioPlayerMute.classList.add('muted'); - } else { - this.audioPlayerMute.classList.remove('muted'); - } - } - - this.audioPlayerVol.oninput = (ev: Event)=> { - this.setVolume(parseFloat((ev.currentTarget).value)); - this.changeVolume(); - - (ev.currentTarget).blur(); - } - - const decreaseElem = HtmlUtils.getElementByIdOrFail('audioplayer_decrease_while_talking'); - decreaseElem.oninput = (ev: Event)=> { - this.decreaseWhileTalking = (ev.currentTarget).checked; - this.changeVolume(); - } - - this.open(); - } - - private loop(): void { - if (this.audioPlayerElem !== undefined) { - this.audioPlayerElem.loop = true; - } - } - - public unloadAudio(): void { - try { - const audioElem = HtmlUtils.getElementByIdOrFail('audioplayerelem'); - this.volume = audioElem.volume; - this.muted = audioElem.muted; - audioElem.pause(); - audioElem.loop = false; - audioElem.src = ""; - audioElem.innerHTML = ""; - audioElem.load(); - } catch (e) { - console.log('No audio element loaded to unload'); - } - - this.close(); - } - - public decreaseVolume(): void { - this.changeVolume(true); - } - - public restoreVolume(): void { - this.changeVolume(false); - } -} - -export const audioManager = new AudioManager();