diff --git a/front/src/Components/Menu/SettingsSubMenu.svelte b/front/src/Components/Menu/SettingsSubMenu.svelte index 40b3b82d..64a6e0ac 100644 --- a/front/src/Components/Menu/SettingsSubMenu.svelte +++ b/front/src/Components/Menu/SettingsSubMenu.svelte @@ -17,9 +17,14 @@ let valueGame: number = localUserStore.getGameQualityValue(); let valueVideo: number = localUserStore.getVideoQualityValue(); let valueLocale: string = $locale; + let valueCameraPrivacySettings = localUserStore.getCameraPrivacySettings(); + let valueMicrophonePrivacySettings = localUserStore.getMicrophonePrivacySettings(); + let previewValueGame = valueGame; let previewValueVideo = valueVideo; let previewValueLocale = valueLocale; + let previewCameraPrivacySettings = valueCameraPrivacySettings; + let previewMicrophonePrivacySettings = valueMicrophonePrivacySettings; function saveSetting() { let change = false; @@ -40,6 +45,16 @@ change = true; } + if (valueCameraPrivacySettings !== previewCameraPrivacySettings) { + previewCameraPrivacySettings = valueCameraPrivacySettings; + localUserStore.setCameraPrivacySettings(valueCameraPrivacySettings); + } + + if (valueMicrophonePrivacySettings !== previewMicrophonePrivacySettings) { + previewMicrophonePrivacySettings = valueMicrophonePrivacySettings; + localUserStore.setMicrophonePrivacySettings(valueMicrophonePrivacySettings); + } + audioManagerVolumeStore.setDecreaseWhileTalking(decreaseAudioPlayerVolumeWhileTalking); if (change) { @@ -162,6 +177,19 @@ + + + {$LL.menu.settings.privacySettings.title()} + {$LL.menu.settings.privacySettings.explanation()} + + + {$LL.menu.settings.privacySettings.cameraToggle()} + + + + {$LL.menu.settings.privacySettings.microphoneToggle()} + + {$LL.menu.settings.save.warning()} {$LL.menu.settings.ignoreFollowRequest()} - - - - {$LL.audio.manager.reduce()} + + + {$LL.audio.manager.reduce()} + @@ -234,12 +262,15 @@ outline: none; } } + section.settings-section-save { text-align: center; + p { margin: 16px 0; } } + section.settings-section-noSaveOption { display: flex; align-items: center; diff --git a/front/src/Connexion/LocalUserStore.ts b/front/src/Connexion/LocalUserStore.ts index 7753fd7b..fafeb10d 100644 --- a/front/src/Connexion/LocalUserStore.ts +++ b/front/src/Connexion/LocalUserStore.ts @@ -25,11 +25,14 @@ const code = "code"; const cameraSetup = "cameraSetup"; const cacheAPIIndex = "workavdenture-cache"; const userProperties = "user-properties"; +const cameraPrivacySettings = "cameraPrivacySettings"; +const microphonePrivacySettings = "microphonePrivacySettings"; class LocalUserStore { saveUser(localUser: LocalUser) { localStorage.setItem("localUser", JSON.stringify(localUser)); } + getLocalUser(): LocalUser | null { const data = localStorage.getItem("localUser"); return data ? JSON.parse(data) : null; @@ -38,6 +41,7 @@ class LocalUserStore { setName(name: string): void { localStorage.setItem(playerNameKey, name); } + getName(): string | null { const value = localStorage.getItem(playerNameKey) || ""; return isUserNameValid(value) ? value : null; @@ -46,6 +50,7 @@ class LocalUserStore { setPlayerCharacterIndex(playerCharacterIndex: number): void { localStorage.setItem(selectedPlayerKey, "" + playerCharacterIndex); } + getPlayerCharacterIndex(): number { return parseInt(localStorage.getItem(selectedPlayerKey) || ""); } @@ -53,6 +58,7 @@ class LocalUserStore { setCustomCursorPosition(activeRow: number, selectedLayers: number[]): void { localStorage.setItem(customCursorPositionKey, JSON.stringify({ activeRow, selectedLayers })); } + getCustomCursorPosition(): { activeRow: number; selectedLayers: number[] } | null { return JSON.parse(localStorage.getItem(customCursorPositionKey) || "null"); } @@ -60,6 +66,7 @@ class LocalUserStore { setCharacterLayers(layers: string[]): void { localStorage.setItem(characterLayersKey, JSON.stringify(layers)); } + getCharacterLayers(): string[] | null { const value = JSON.parse(localStorage.getItem(characterLayersKey) || "null"); return areCharacterLayersValid(value) ? value : null; @@ -68,6 +75,7 @@ class LocalUserStore { setCompanion(companion: string | null): void { return localStorage.setItem(companionKey, JSON.stringify(companion)); } + getCompanion(): string | null { const companion = JSON.parse(localStorage.getItem(companionKey) || "null"); @@ -77,6 +85,7 @@ class LocalUserStore { return companion; } + wasCompanionSet(): boolean { return localStorage.getItem(companionKey) ? true : false; } @@ -84,6 +93,7 @@ class LocalUserStore { setGameQualityValue(value: number): void { localStorage.setItem(gameQualityKey, "" + value); } + getGameQualityValue(): number { return parseInt(localStorage.getItem(gameQualityKey) || "60"); } @@ -91,6 +101,7 @@ class LocalUserStore { setVideoQualityValue(value: number): void { localStorage.setItem(videoQualityKey, "" + value); } + getVideoQualityValue(): number { return parseInt(localStorage.getItem(videoQualityKey) || "20"); } @@ -98,6 +109,7 @@ class LocalUserStore { setAudioPlayerVolume(value: number): void { localStorage.setItem(audioPlayerVolumeKey, "" + value); } + getAudioPlayerVolume(): number { return parseFloat(localStorage.getItem(audioPlayerVolumeKey) || "1"); } @@ -105,6 +117,7 @@ class LocalUserStore { setAudioPlayerMuted(value: boolean): void { localStorage.setItem(audioPlayerMuteKey, value.toString()); } + getAudioPlayerMuted(): boolean { return localStorage.getItem(audioPlayerMuteKey) === "true"; } @@ -112,6 +125,7 @@ class LocalUserStore { setHelpCameraSettingsShown(): void { localStorage.setItem(helpCameraSettingsShown, "1"); } + getHelpCameraSettingsShown(): boolean { return localStorage.getItem(helpCameraSettingsShown) === "1"; } @@ -119,6 +133,7 @@ class LocalUserStore { setFullscreen(value: boolean): void { localStorage.setItem(fullscreenKey, value.toString()); } + getFullscreen(): boolean { return localStorage.getItem(fullscreenKey) === "true"; } @@ -126,6 +141,7 @@ class LocalUserStore { setForceCowebsiteTrigger(value: boolean): void { localStorage.setItem(forceCowebsiteTriggerKey, value.toString()); } + getForceCowebsiteTrigger(): boolean { return localStorage.getItem(forceCowebsiteTriggerKey) === "true"; } @@ -133,6 +149,7 @@ class LocalUserStore { setIgnoreFollowRequests(value: boolean): void { localStorage.setItem(ignoreFollowRequests, value.toString()); } + getIgnoreFollowRequests(): boolean { return localStorage.getItem(ignoreFollowRequests) === "true"; } @@ -155,11 +172,13 @@ class LocalUserStore { } } } + getLastRoomUrl(): string { return ( localStorage.getItem(lastRoomUrl) ?? window.location.protocol + "//" + window.location.host + START_ROOM_URL ); } + getLastRoomUrlCacheApi(): Promise { if (!("caches" in window)) { return Promise.resolve(undefined); @@ -176,6 +195,7 @@ class LocalUserStore { setAuthToken(value: string | null) { value ? localStorage.setItem(authToken, value) : localStorage.removeItem(authToken); } + getAuthToken(): string | null { return localStorage.getItem(authToken); } @@ -202,23 +222,29 @@ class LocalUserStore { } return oldValue === value; } + setState(value: string) { localStorage.setItem(state, value); } + getState(): string | null { return localStorage.getItem(state); } + generateNonce(): string { const newNonce = uuidv4(); localStorage.setItem(nonce, newNonce); return newNonce; } + getNonce(): string | null { return localStorage.getItem(nonce); } + setCode(value: string): void { localStorage.setItem(code, value); } + getCode(): string | null { return localStorage.getItem(code); } @@ -226,11 +252,36 @@ class LocalUserStore { setCameraSetup(cameraId: string) { localStorage.setItem(cameraSetup, cameraId); } + getCameraSetup(): { video: unknown; audio: unknown } | undefined { const cameraSetupValues = localStorage.getItem(cameraSetup); return cameraSetupValues != undefined ? JSON.parse(cameraSetupValues) : undefined; } + setCameraPrivacySettings(option: boolean) { + localStorage.setItem(cameraPrivacySettings, option.toString()); + } + + getCameraPrivacySettings() { + //if this setting doesn't exist in LocalUserStore, we set a default value + if (localStorage.getItem(cameraPrivacySettings) == null) { + localStorage.setItem(cameraPrivacySettings, "false"); + } + return localStorage.getItem(cameraPrivacySettings) === "true"; + } + + setMicrophonePrivacySettings(option: boolean) { + localStorage.setItem(microphonePrivacySettings, option.toString()); + } + + getMicrophonePrivacySettings() { + //if this setting doesn't exist in LocalUserStore, we set a default value + if (localStorage.getItem(microphonePrivacySettings) == null) { + localStorage.setItem(microphonePrivacySettings, "true"); + } + return localStorage.getItem(microphonePrivacySettings) === "true"; + } + getAllUserProperties(): Map { const result = new Map(); for (let i = 0; i < localStorage.length; i++) { diff --git a/front/src/Stores/MediaStore.ts b/front/src/Stores/MediaStore.ts index b86a97ce..557cbcd8 100644 --- a/front/src/Stores/MediaStore.ts +++ b/front/src/Stores/MediaStore.ts @@ -11,7 +11,7 @@ import { peerStore } from "./PeerStore"; import { privacyShutdownStore } from "./PrivacyShutdownStore"; import { MediaStreamConstraintsError } from "./Errors/MediaStreamConstraintsError"; import { SoundMeter } from "../Phaser/Components/SoundMeter"; -import { AudioContext } from "standardized-audio-context"; +import { visibilityStore } from "./VisibilityStore"; /** * A store that contains the camera state requested by the user (on or off). @@ -242,6 +242,7 @@ export const mediaStreamConstraintsStore = derived( privacyShutdownStore, cameraEnergySavingStore, isSilentStore, + visibilityStore, ], ( [ @@ -254,6 +255,7 @@ export const mediaStreamConstraintsStore = derived( $privacyShutdownStore, $cameraEnergySavingStore, $isSilentStore, + $visibilityStore, ], set ) => { @@ -292,7 +294,14 @@ export const mediaStreamConstraintsStore = derived( // Disable webcam for privacy reasons (the game is not visible and we were talking to no one) if ($privacyShutdownStore === true) { - currentVideoConstraint = false; + const userMicrophonePrivacySetting = localUserStore.getMicrophonePrivacySettings(); + const userCameraPrivacySetting = localUserStore.getCameraPrivacySettings(); + if (!userMicrophonePrivacySetting) { + currentAudioConstraint = false; + } + if (!userCameraPrivacySetting) { + currentVideoConstraint = false; + } } // Disable webcam for energy reasons (the user is not moving and we are talking to no one) diff --git a/front/src/i18n/de-DE/menu.ts b/front/src/i18n/de-DE/menu.ts index c1b7bc18..ffc9823d 100644 --- a/front/src/i18n/de-DE/menu.ts +++ b/front/src/i18n/de-DE/menu.ts @@ -57,6 +57,13 @@ const menu: NonNullable = { language: { title: "Sprache", }, + privacySettings: { + title: "Einstellungen Abwesenheitsmodus", + explanation: + "Falls der WorkAdventure Tab nicht aktiv ist wird in den Abwesenheitsmodus umgeschaltet. Für diesen Modus kann eingestellt werden, ob die Kamera und/oder das Mikrofon deaktiviert sind solange der Tab nicht sichtbar ist.", + cameraToggle: "Kamera", + microphoneToggle: "Mikrofon", + }, save: { warning: "(Das Spiel wird nach dem Speichern neugestartet)", button: "Speichern", diff --git a/front/src/i18n/en-US/menu.ts b/front/src/i18n/en-US/menu.ts index 0883fb15..6eb6de21 100644 --- a/front/src/i18n/en-US/menu.ts +++ b/front/src/i18n/en-US/menu.ts @@ -57,6 +57,13 @@ const menu: BaseTranslation = { language: { title: "Language", }, + privacySettings: { + title: "Away mode settings", + explanation: + 'When the WorkAdventure tab is not visible, it switches to "away mode". In this mode, you can decide to automatically disable your webcam and/or microphone for as long as the tab stays hidden.', + cameraToggle: "Camera", + microphoneToggle: "Microphone", + }, save: { warning: "(Saving these settings will restart the game)", button: "Save", diff --git a/front/src/i18n/fr-FR/menu.ts b/front/src/i18n/fr-FR/menu.ts index f8c58990..1515ea8f 100644 --- a/front/src/i18n/fr-FR/menu.ts +++ b/front/src/i18n/fr-FR/menu.ts @@ -57,6 +57,13 @@ const menu: NonNullable = { language: { title: "Langage", }, + privacySettings: { + title: "Paramètres du mode absent", + explanation: + "Quand l'onglet WorkAdventure n'est pas visible, vous passez en \"mode absent\". Lorsque ce mode est actif, vous pouvez décider de garder vos webcam et/ou micro désactivés tant que vous ne revenez pas sur l'onglet", + cameraToggle: "Camera", + microphoneToggle: "Microphone", + }, save: { warning: "(La sauvegarde de ces paramètres redémarre le jeu)", button: "Sauvegarder", diff --git a/maps/tests/AwayModeSettings/away_mode_settings.json b/maps/tests/AwayModeSettings/away_mode_settings.json new file mode 100644 index 00000000..28c40940 --- /dev/null +++ b/maps/tests/AwayModeSettings/away_mode_settings.json @@ -0,0 +1,97 @@ +{ "compressionlevel":-1, + "height":20, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":20, + "id":42, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "height":20, + "id":39, + "name":"floor", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":20, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":43, + "name":"Test", + "objects":[ + { + "height":225.333333333333, + "id":13, + "name":"", + "rotation":0, + "text": + { + "text":"Test: \n- Open two windows (you can use Private Mode) so that you control two Wokas.\n\n- On woka A window: go to Menu > Settings and set your away mode options\n- On woka A window: open a new tab in the browser, so that your WA tab is not visible\n\n- On woka B window: move to woka A to check that the options were applied", + "wrap":true + }, + "type":"", + "visible":true, + "width":434.773333333333, + "x":97.9466666666667, + "y":33.8366666666667 + }, + { + "height":155, + "id":16, + "name":"", + "rotation":0, + "text": + { + "color":"#00007f", + "text":"Reminder: \nThere are 4 cases to test for your away mode (WA tab hidden) settings. \nCamera and microphone stay enabled\nOnly camera stays enabled\nOnly microphone stays enabled\nBoth are disabled", + "wrap":true + }, + "type":"", + "visible":true, + "width":407.4375, + "x":96.9479166666667, + "y":322.5 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":44, + "nextobjectid":17, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.7.2", + "tileheight":32, + "tilesets":[ + { + "columns":11, + "firstgid":1, + "image":"..\/tileset1.png", + "imageheight":352, + "imagewidth":352, + "margin":0, + "name":"tileset1", + "spacing":0, + "tilecount":121, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":"1.6", + "width":20 +} \ No newline at end of file diff --git a/maps/tests/index.html b/maps/tests/index.html index 3a1ef520..e625aa6d 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -463,6 +463,14 @@ Testing zoom via mouse wheel + + + Success Failure Pending + + + Away mode settings + +
{$LL.menu.settings.privacySettings.explanation()}
{$LL.menu.settings.save.warning()}