Merge branch 'develop' of github.com:thecodingmachine/workadventure

This commit is contained in:
_Bastler 2021-12-07 08:38:21 +01:00
commit 13e854c4c4
80 changed files with 3438 additions and 1792 deletions

View File

@ -20,6 +20,15 @@ jobs:
- name: "Checkout"
uses: "actions/checkout@v2.0.0"
- name: "Setup NodeJS"
uses: actions/setup-node@v1
with:
node-version: '14.x'
- name: "Install dependencies"
run: npm install
working-directory: "tests"
- name: "Setup .env file"
run: cp .env.template .env
@ -27,10 +36,10 @@ jobs:
run: sudo chown 1000:1000 -R .
- name: "Start environment"
run: docker-compose up -d
run: LIVE_RELOAD=0 docker-compose up -d
- name: "Wait for environment to build (and downloading testcafe image)"
run: (docker-compose -f docker-compose.testcafe.yml pull &) && docker-compose logs -f --tail=0 front | grep -q "Compiled successfully"
run: (docker-compose -f docker-compose.testcafe.yml build &) && docker-compose logs -f --tail=0 front | grep -q "Compiled successfully"
# - name: "temp debug: display logs"
# run: docker-compose logs
@ -42,7 +51,7 @@ jobs:
# run: docker-compose logs -f pusher | grep -q "WorkAdventure starting on port"
- name: "Run tests"
run: docker-compose -f docker-compose.testcafe.yml up --exit-code-from testcafe
run: PROJECT_DIR=$(pwd) docker-compose -f docker-compose.testcafe.yml up --exit-code-from testcafe
- name: Upload failed tests
if: ${{ failure() }}

View File

@ -59,9 +59,43 @@ $ docker-compose exec back yarn run pretty
WorkAdventure is based on a video game engine (Phaser), and video games are not the easiest programs to unit test.
Nevertheless, if your code can be unit tested, please provide a unit test (we use Jasmine).
Nevertheless, if your code can be unit tested, please provide a unit test (we use Jasmine), or an end-to-end test (we use Testcafe).
If you are providing a new feature, you should setup a test map in the `maps/tests` directory. The test map should contain
some description text describing how to test the feature. Finally, you should modify the `maps/tests/index.html` file
to add a reference to your newly created test map.
some description text describing how to test the feature.
* if the features is meant to be manually tested, you should modify the `maps/tests/index.html` file to add a reference
to your newly created test map
* if the features can be automatically tested, please provide a testcafe test
#### Running testcafe tests
End-to-end tests are available in the "/tests" directory.
To run these tests locally:
```console
$ LIVE_RELOAD=0 docker-compose up -d
$ cd tests
$ npm install
$ npm run test
```
Note: If your tests fail on a Javascript error in "sockjs", this is due to the
Webpack live reload. The Webpack live reload feature is conflicting with testcafe. This is why we recommend starting
WorkAdventure with the `LIVE_RELOAD=0` environment variable.
End-to-end tests can take a while to run. To run only one test, use:
```console
$ npm run test -- tests/[name of the test file].ts
```
You can also run the tests inside a container (but you will not have visual feedbacks on your test, so we recommend using
the local tests).
```console
$ LIVE_RELOAD=0 docker-compose up -d
# Wait 2-3 minutes for the environment to start, then:
$ PROJECT_DIR=$(pwd) docker-compose -f docker-compose.testcafe.yml up
```

View File

@ -12,43 +12,52 @@ export class DebugController {
getDump() {
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
const query = parse(req.getQuery());
(async () => {
const query = parse(req.getQuery());
if (query.token !== ADMIN_API_TOKEN) {
return res.writeStatus("401 Unauthorized").end("Invalid token sent!");
}
if (query.token !== ADMIN_API_TOKEN) {
return res.writeStatus("401 Unauthorized").end("Invalid token sent!");
}
return res
.writeStatus("200 OK")
.writeHeader("Content-Type", "application/json")
.end(
stringify(socketManager.getWorlds(), (key: unknown, value: unknown) => {
if (key === "listeners") {
return "Listeners";
}
if (key === "socket") {
return "Socket";
}
if (key === "batchedMessages") {
return "BatchedMessages";
}
if (value instanceof Map) {
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
for (const [mapKey, mapValue] of value.entries()) {
obj[mapKey] = mapValue;
return res
.writeStatus("200 OK")
.writeHeader("Content-Type", "application/json")
.end(
stringify(
await Promise.all(socketManager.getWorlds().values()),
(key: unknown, value: unknown) => {
if (key === "listeners") {
return "Listeners";
}
if (key === "socket") {
return "Socket";
}
if (key === "batchedMessages") {
return "BatchedMessages";
}
if (value instanceof Map) {
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
for (const [mapKey, mapValue] of value.entries()) {
obj[mapKey] = mapValue;
}
return obj;
} else if (value instanceof Set) {
const obj: Array<unknown> = [];
for (const [setKey, setValue] of value.entries()) {
obj.push(setValue);
}
return obj;
} else {
return value;
}
}
return obj;
} else if (value instanceof Set) {
const obj: Array<unknown> = [];
for (const [setKey, setValue] of value.entries()) {
obj.push(setValue);
}
return obj;
} else {
return value;
}
})
);
)
);
})().catch((e) => {
console.error(e);
res.writeStatus("500");
res.end("An error occurred");
});
});
}
}

View File

@ -1,12 +1,19 @@
version: "3"
version: "3.5"
services:
testcafe:
image: testcafe/testcafe:1.17.1
working_dir: /tests
build: tests/
working_dir: /project/tests
command:
- --dev
# Run as root to have the right to access /var/run/docker.sock
user: root
environment:
BROWSER: "chromium --use-fake-device-for-media-stream"
PROJECT_DIR: ${PROJECT_DIR}
ADMIN_API_TOKEN: ${ADMIN_API_TOKEN}
volumes:
- ./tests:/tests
- ./:/project
- ./maps:/maps
- /var/run/docker.sock:/var/run/docker.sock
# security_opt:
# - seccomp:unconfined

View File

@ -1,20 +1,21 @@
version: "3"
version: "3.5"
services:
reverse-proxy:
image: traefik:v2.0
image: traefik:v2.5
command:
- --api.insecure=true
- --providers.docker
- --entryPoints.web.address=:80
- --entryPoints.websecure.address=:443
- "--providers.docker.exposedbydefault=false"
ports:
- "80:80"
- "443:443"
# The Web UI (enabled by --api.insecure=true)
- "8080:8080"
depends_on:
- back
- front
#depends_on:
# - back
# - front
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
@ -51,10 +52,12 @@ services:
MAX_USERNAME_LENGTH: "$MAX_USERNAME_LENGTH"
DISABLE_ANONYMOUS: "$DISABLE_ANONYMOUS"
OPID_LOGIN_SCREEN_PROVIDER: "$OPID_LOGIN_SCREEN_PROVIDER"
LIVE_RELOAD: "$LIVE_RELOAD:-true"
command: yarn run start
volumes:
- ./front:/usr/src/app
labels:
- "traefik.enable=true"
- "traefik.http.routers.front.rule=Host(`play.workadventure.localhost`)"
- "traefik.http.routers.front.entryPoints=web"
- "traefik.http.services.front.loadbalancer.server.port=8080"
@ -87,6 +90,7 @@ services:
volumes:
- ./pusher:/usr/src/app
labels:
- "traefik.enable=true"
- "traefik.http.routers.pusher.rule=Host(`pusher.workadventure.localhost`)"
- "traefik.http.routers.pusher.entryPoints=web"
- "traefik.http.services.pusher.loadbalancer.server.port=8080"
@ -111,6 +115,7 @@ services:
volumes:
- ./maps:/var/www/html
labels:
- "traefik.enable=true"
- "traefik.http.routers.maps.rule=Host(`maps.workadventure.localhost`)"
- "traefik.http.routers.maps.entryPoints=web,traefik"
- "traefik.http.services.maps.loadbalancer.server.port=80"
@ -142,6 +147,7 @@ services:
volumes:
- ./back:/usr/src/app
labels:
- "traefik.enable=true"
- "traefik.http.routers.back.rule=Host(`api.workadventure.localhost`)"
- "traefik.http.routers.back.entryPoints=web"
- "traefik.http.services.back.loadbalancer.server.port=8080"
@ -160,6 +166,7 @@ services:
volumes:
- ./uploader:/usr/src/app
labels:
- "traefik.enable=true"
- "traefik.http.routers.uploader.rule=Host(`uploader.workadventure.localhost`)"
- "traefik.http.routers.uploader.entryPoints=web"
- "traefik.http.services.uploader.loadbalancer.server.port=8080"
@ -187,6 +194,7 @@ services:
redisinsight:
image: redislabs/redisinsight:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.redisinsight.rule=Host(`redis.workadventure.localhost`)"
- "traefik.http.routers.redisinsight.entryPoints=web"
- "traefik.http.services.redisinsight.loadbalancer.server.port=8001"
@ -198,6 +206,7 @@ services:
icon:
image: matthiasluedtke/iconserver:v3.13.0
labels:
- "traefik.enable=true"
- "traefik.http.routers.icon.rule=Host(`icon.workadventure.localhost`)"
- "traefik.http.routers.icon.entryPoints=web"
- "traefik.http.services.icon.loadbalancer.server.port=8080"

View File

@ -1,4 +1,4 @@
{
module.exports = {
"root": true,
"env": {
"browser": true,
@ -18,10 +18,18 @@
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module",
"project": "./tsconfig.json"
"project": "./tsconfig.json",
"extraFileExtensions": [".svelte"]
},
"plugins": [
"@typescript-eslint"
"@typescript-eslint",
"svelte3"
],
"overrides": [
{
"files": ["*.svelte"],
"processor": "svelte3/svelte3"
}
],
"rules": {
"no-unused-vars": "off",
@ -34,5 +42,9 @@
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/restrict-template-expressions": "off"
},
"settings": {
"svelte3/typescript": true,
"svelte3/ignore-styles": () => true
}
}

View File

@ -1,4 +1,5 @@
{
"printWidth": 120,
"tabWidth": 4
"tabWidth": 4,
"plugins": ["prettier-plugin-svelte"]
}

View File

@ -16,7 +16,7 @@
"@typescript-eslint/parser": "^4.23.0",
"css-loader": "^5.2.4",
"eslint": "^7.26.0",
"file-loader": "^6.2.0",
"eslint-plugin-svelte3": "^3.2.1",
"fork-ts-checker-webpack-plugin": "^6.2.9",
"html-webpack-plugin": "^5.3.1",
"jasmine": "^3.5.0",
@ -25,6 +25,7 @@
"node-polyfill-webpack-plugin": "^1.1.2",
"npm-run-all": "^4.1.5",
"prettier": "^2.3.1",
"prettier-plugin-svelte": "^2.5.0",
"sass": "^1.32.12",
"sass-loader": "^11.1.0",
"svelte": "^3.38.2",
@ -56,6 +57,7 @@
"queue-typescript": "^1.0.1",
"quill": "1.3.6",
"quill-delta-to-html": "^0.12.0",
"retry-axios": "^2.6.0",
"rxjs": "^6.6.3",
"sanitize-html": "^2.5.0",
"simple-peer": "^9.11.0",
@ -70,17 +72,21 @@
"build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack",
"build-typings": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production BUILD_TYPINGS=1 webpack",
"test": "cross-env TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
"lint": "node_modules/.bin/eslint src/ . --ext .ts",
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts",
"lint": "node_modules/.bin/eslint src/ tests/ --ext .ts,.svelte",
"fix": "node_modules/.bin/eslint --fix src/ tests/ --ext .ts,.svelte",
"precommit": "lint-staged",
"svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\" --watch",
"svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\"",
"pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'",
"pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'"
"pretty": "yarn prettier --write 'src/**/*.{ts,svelte}'",
"pretty-check": "yarn prettier --check 'src/**/*.{ts,svelte}'"
},
"lint-staged": {
"*.ts": [
"prettier --write"
"*.svelte": [
"yarn run svelte-check"
],
"*.{ts,svelte}": [
"yarn run fix",
"yarn run pretty"
]
}
}

View File

@ -1,147 +1,146 @@
<script lang="typescript">
import MenuIcon from "./Menu/MenuIcon.svelte";
import {menuIconVisiblilityStore, menuVisiblilityStore} from "../Stores/MenuStore";
import {emoteMenuStore} from "../Stores/EmoteStore";
import {enableCameraSceneVisibilityStore} from "../Stores/MediaStore";
import { menuIconVisiblilityStore, menuVisiblilityStore } from "../Stores/MenuStore";
import { emoteMenuStore } from "../Stores/EmoteStore";
import { enableCameraSceneVisibilityStore } from "../Stores/MediaStore";
import CameraControls from "./CameraControls.svelte";
import MyCamera from "./MyCamera.svelte";
import SelectCompanionScene from "./SelectCompanion/SelectCompanionScene.svelte";
import {selectCompanionSceneVisibleStore} from "../Stores/SelectCompanionStore";
import {selectCharacterSceneVisibleStore} from "../Stores/SelectCharacterStore";
import { selectCompanionSceneVisibleStore } from "../Stores/SelectCompanionStore";
import { selectCharacterSceneVisibleStore } from "../Stores/SelectCharacterStore";
import SelectCharacterScene from "./selectCharacter/SelectCharacterScene.svelte";
import {customCharacterSceneVisibleStore} from "../Stores/CustomCharacterStore";
import {errorStore} from "../Stores/ErrorStore";
import { customCharacterSceneVisibleStore } from "../Stores/CustomCharacterStore";
import { errorStore } from "../Stores/ErrorStore";
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
import LoginScene from "./Login/LoginScene.svelte";
import Chat from "./Chat/Chat.svelte";
import {loginSceneVisibleStore} from "../Stores/LoginSceneStore";
import { loginSceneVisibleStore } from "../Stores/LoginSceneStore";
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
import VisitCard from "./VisitCard/VisitCard.svelte";
import {requestVisitCardsStore} from "../Stores/GameStore";
import { requestVisitCardsStore } from "../Stores/GameStore";
import type {Game} from "../Phaser/Game/Game";
import {chatVisibilityStore} from "../Stores/ChatStore";
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
import type { Game } from "../Phaser/Game/Game";
import { chatVisibilityStore } from "../Stores/ChatStore";
import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
import AudioPlaying from "./UI/AudioPlaying.svelte";
import {soundPlayingStore} from "../Stores/SoundPlayingStore";
import { soundPlayingStore } from "../Stores/SoundPlayingStore";
import ErrorDialog from "./UI/ErrorDialog.svelte";
import Menu from "./Menu/Menu.svelte";
import EmoteMenu from "./EmoteMenu/EmoteMenu.svelte";
import VideoOverlay from "./Video/VideoOverlay.svelte";
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility";
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
import AdminMessage from "./TypeMessage/BanMessage.svelte";
import TextMessage from "./TypeMessage/TextMessage.svelte";
import {banMessageVisibleStore} from "../Stores/TypeMessageStore/BanMessageStore";
import {textMessageVisibleStore} from "../Stores/TypeMessageStore/TextMessageStore";
import {warningContainerStore} from "../Stores/MenuStore";
import { banMessageVisibleStore } from "../Stores/TypeMessageStore/BanMessageStore";
import { textMessageVisibleStore } from "../Stores/TypeMessageStore/TextMessageStore";
import { warningContainerStore } from "../Stores/MenuStore";
import WarningContainer from "./WarningContainer/WarningContainer.svelte";
import {layoutManagerVisibilityStore} from "../Stores/LayoutManagerStore";
import { layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore";
import LayoutManager from "./LayoutManager/LayoutManager.svelte";
import {audioManagerVisibilityStore} from "../Stores/AudioManagerStore";
import AudioManager from "./AudioManager/AudioManager.svelte"
import { audioManagerVisibilityStore } from "../Stores/AudioManagerStore";
import AudioManager from "./AudioManager/AudioManager.svelte";
import { showReportScreenStore, userReportEmpty } from "../Stores/ShowReportScreenStore";
import ReportMenu from "./ReportMenu/ReportMenu.svelte";
export let game: Game;
</script>
<div>
{#if $loginSceneVisibleStore}
<div class="scrollable">
<LoginScene game={game}></LoginScene>
<LoginScene {game} />
</div>
{/if}
{#if $selectCharacterSceneVisibleStore}
<div>
<SelectCharacterScene game={ game }></SelectCharacterScene>
<SelectCharacterScene {game} />
</div>
{/if}
{#if $customCharacterSceneVisibleStore}
<div>
<CustomCharacterScene game={ game }></CustomCharacterScene>
<CustomCharacterScene {game} />
</div>
{/if}
{#if $selectCompanionSceneVisibleStore}
<div>
<SelectCompanionScene game={ game }></SelectCompanionScene>
<SelectCompanionScene {game} />
</div>
{/if}
{#if $enableCameraSceneVisibilityStore}
<div class="scrollable">
<EnableCameraScene game={game}></EnableCameraScene>
<EnableCameraScene {game} />
</div>
{/if}
{#if $banMessageVisibleStore}
<div>
<AdminMessage></AdminMessage>
<AdminMessage />
</div>
{/if}
{#if $textMessageVisibleStore}
<div>
<TextMessage></TextMessage>
<TextMessage />
</div>
{/if}
{#if $soundPlayingStore}
<div>
<AudioPlaying url={$soundPlayingStore} />
</div>
<div>
<AudioPlaying url={$soundPlayingStore} />
</div>
{/if}
{#if $audioManagerVisibilityStore}
<div>
<AudioManager></AudioManager>
</div>
<div>
<AudioManager />
</div>
{/if}
{#if $layoutManagerVisibilityStore}
<div>
<LayoutManager></LayoutManager>
<LayoutManager />
</div>
{/if}
{#if $showReportScreenStore !== userReportEmpty}
<div>
<ReportMenu></ReportMenu>
<ReportMenu />
</div>
{/if}
{#if $menuIconVisiblilityStore}
<div>
<MenuIcon></MenuIcon>
<MenuIcon />
</div>
{/if}
{#if $menuVisiblilityStore}
<div>
<Menu></Menu>
<Menu />
</div>
{/if}
{#if $emoteMenuStore}
<div>
<EmoteMenu></EmoteMenu>
<EmoteMenu />
</div>
{/if}
{#if $gameOverlayVisibilityStore}
<div>
<VideoOverlay></VideoOverlay>
<MyCamera></MyCamera>
<CameraControls></CameraControls>
<VideoOverlay />
<MyCamera />
<CameraControls />
</div>
{/if}
{#if $helpCameraSettingsVisibleStore}
<div>
<HelpCameraSettingsPopup></HelpCameraSettingsPopup>
<HelpCameraSettingsPopup />
</div>
{/if}
{#if $requestVisitCardsStore}
<VisitCard visitCardUrl={$requestVisitCardsStore}></VisitCard>
<VisitCard visitCardUrl={$requestVisitCardsStore} />
{/if}
{#if $errorStore.length > 0}
<div>
<ErrorDialog></ErrorDialog>
</div>
<div>
<ErrorDialog />
</div>
{/if}
{#if $chatVisibilityStore}
<Chat></Chat>
<Chat />
{/if}
{#if $warningContainerStore}
<WarningContainer></WarningContainer>
<WarningContainer />
{/if}
</div>

View File

@ -1,10 +1,7 @@
<script lang="ts">
import { localUserStore } from "../../Connexion/LocalUserStore";
import type { audioManagerVolume } from "../../Stores/AudioManagerStore";
import {
audioManagerFileStore,
audioManagerVolumeStore,
} from "../../Stores/AudioManagerStore";
import { audioManagerFileStore, audioManagerVolumeStore } from "../../Stores/AudioManagerStore";
import { get } from "svelte/store";
import type { Unsubscriber } from "svelte/store";
import { onDestroy, onMount } from "svelte";
@ -41,8 +38,8 @@
HTMLAudioPlayer.muted = audioManager.muted;
HTMLAudioPlayer.loop = audioManager.loop;
updateVolumeUI();
})
})
});
});
onDestroy(() => {
if (unsubscriberFileStore) {
@ -51,24 +48,24 @@
if (unsubscriberVolumeStore) {
unsubscriberVolumeStore();
}
})
});
function updateVolumeUI() {
if (get(audioManagerVolumeStore).muted) {
audioPlayerVolumeIcon.classList.add('muted');
audioPlayerVolumeIcon.classList.add("muted");
audioPlayerVol.value = "0";
} else {
let volume = HTMLAudioPlayer.volume;
audioPlayerVol.value = "" + volume;
audioPlayerVolumeIcon.classList.remove('muted');
audioPlayerVolumeIcon.classList.remove("muted");
if (volume < 0.3) {
audioPlayerVolumeIcon.classList.add('low');
audioPlayerVolumeIcon.classList.add("low");
} else if (volume < 0.7) {
audioPlayerVolumeIcon.classList.remove('low');
audioPlayerVolumeIcon.classList.add('mid');
audioPlayerVolumeIcon.classList.remove("low");
audioPlayerVolumeIcon.classList.add("mid");
} else {
audioPlayerVolumeIcon.classList.remove('low');
audioPlayerVolumeIcon.classList.remove('mid');
audioPlayerVolumeIcon.classList.remove("low");
audioPlayerVolumeIcon.classList.remove("mid");
}
}
}
@ -97,45 +94,67 @@
}
</script>
<div class="main-audio-manager nes-container is-rounded">
<div class="audio-manager-player-volume">
<span id="audioplayer_volume_icon_playing" alt="player volume" bind:this={audioPlayerVolumeIcon}
on:click={onMute}>
<svg width="2em" height="2em" viewBox="0 0 16 16" class="bi bi-volume-up" fill="white"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04z" />
<span
id="audioplayer_volume_icon_playing"
alt="player volume"
bind:this={audioPlayerVolumeIcon}
on:click={onMute}
>
<svg
width="2em"
height="2em"
viewBox="0 0 16 16"
class="bi bi-volume-up"
fill="white"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04z"
/>
<g id="audioplayer_volume_icon_playing_high">
<path
d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z" />
d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z"
/>
</g>
<g id="audioplayer_volume_icon_playing_mid">
<path
d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z" />
d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z"
/>
</g>
<g id="audioplayer_volume_icon_playing_low">
<path
d="M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707z" />
d="M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707z"
/>
</g>
</svg>
</span>
<input type="range" min="0" max="1" step="0.025" bind:this={audioPlayerVol} on:change={setVolume} on:keydown={disallowKeys}>
<input
type="range"
min="0"
max="1"
step="0.025"
bind:this={audioPlayerVol}
on:change={setVolume}
on:keydown={disallowKeys}
/>
</div>
<div class="audio-manager-reduce-conversation">
<label>
reduce in conversations
<input type="checkbox" bind:checked={decreaseWhileTalking} on:change={setDecrease}>
<input type="checkbox" bind:checked={decreaseWhileTalking} on:change={setDecrease} />
</label>
<section class="audio-manager-file">
<!-- svelte-ignore a11y-media-has-caption -->
<audio class="audio-manager-audioplayer" bind:this={HTMLAudioPlayer}>
<source src={$audioManagerFileStore}>
<source src={$audioManagerFileStore} />
</audio>
</section>
</div>
</div>
<style lang="scss">
div.main-audio-manager.nes-container.is-rounded {
position: relative;

View File

@ -1,6 +1,6 @@
<script lang="typescript">
import {requestedScreenSharingState, screenSharingAvailableStore} from "../Stores/ScreenSharingStore";
import {isSilentStore, requestedCameraState, requestedMicrophoneState} from "../Stores/MediaStore";
import { requestedScreenSharingState, screenSharingAvailableStore } from "../Stores/ScreenSharingStore";
import { isSilentStore, requestedCameraState, requestedMicrophoneState } from "../Stores/MediaStore";
import monitorImg from "./images/monitor.svg";
import monitorCloseImg from "./images/monitor-close.svg";
import cinemaImg from "./images/cinema.svg";
@ -9,10 +9,10 @@
import microphoneCloseImg from "./images/microphone-close.svg";
import layoutPresentationImg from "./images/layout-presentation.svg";
import layoutChatImg from "./images/layout-chat.svg";
import {layoutModeStore} from "../Stores/StreamableCollectionStore";
import {LayoutMode} from "../WebRtc/LayoutManager";
import {peerStore} from "../Stores/PeerStore";
import {onDestroy} from "svelte";
import { layoutModeStore } from "../Stores/StreamableCollectionStore";
import { LayoutMode } from "../WebRtc/LayoutManager";
import { peerStore } from "../Stores/PeerStore";
import { onDestroy } from "svelte";
function screenSharingClick(): void {
if (isSilent) return;
@ -50,7 +50,7 @@
}
let isSilent: boolean;
const unsubscribeIsSilent = isSilentStore.subscribe(value => {
const unsubscribeIsSilent = isSilentStore.subscribe((value) => {
isSilent = value;
});
onDestroy(unsubscribeIsSilent);
@ -58,36 +58,38 @@
<div>
<div class="btn-cam-action">
<div class="btn-layout nes-btn is-dark" on:click={switchLayoutMode} class:hide={$peerStore.size===0}>
{#if $layoutModeStore === LayoutMode.Presentation }
<img src={layoutPresentationImg} style="padding: 2px" alt="Switch to mosaic mode">
<div class="btn-layout nes-btn is-dark" on:click={switchLayoutMode} class:hide={$peerStore.size === 0}>
{#if $layoutModeStore === LayoutMode.Presentation}
<img src={layoutPresentationImg} style="padding: 2px" alt="Switch to mosaic mode" />
{:else}
<img src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode">
<img src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode" />
{/if}
</div>
<div class="btn-micro nes-btn is-dark" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState ||
isSilent}>
{#if $requestedMicrophoneState && !isSilent}
<img src={microphoneImg} alt="Turn on microphone">
<div
class="btn-monitor nes-btn is-dark"
on:click={screenSharingClick}
class:hide={!$screenSharingAvailableStore || isSilent}
class:enabled={$requestedScreenSharingState}
>
{#if $requestedScreenSharingState && !isSilent}
<img src={monitorImg} alt="Start screen sharing" />
{:else}
<img src={microphoneCloseImg} alt="Turn off microphone">
<img src={monitorCloseImg} alt="Stop screen sharing" />
{/if}
</div>
<div class="btn-video nes-btn is-dark" on:click={cameraClick} class:disabled={!$requestedCameraState ||
isSilent}>
{#if $requestedCameraState && !isSilent}
<img src={cinemaImg} alt="Turn on webcam">
<img src={cinemaImg} alt="Turn on webcam" />
{:else}
<img src={cinemaCloseImg} alt="Turn off webcam">
<img src={cinemaCloseImg} alt="Turn off webcam" />
{/if}
</div>
<div class="btn-monitor nes-btn is-dark" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore
|| isSilent} class:enabled={$requestedScreenSharingState}>
{#if $requestedScreenSharingState && !isSilent}
<img src={monitorImg} alt="Start screen sharing">
<div class="btn-micro nes-btn is-dark" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState || isSilent}>
{#if $requestedMicrophoneState && !isSilent}
<img src={microphoneImg} alt="Turn on microphone" />
{:else}
<img src={monitorCloseImg} alt="Stop screen sharing">
<img src={microphoneCloseImg} alt="Turn off microphone" />
{/if}
</div>
</div>

View File

@ -1,23 +1,23 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { fly } from "svelte/transition";
import { chatMessagesStore, chatVisibilityStore } from "../../Stores/ChatStore";
import ChatMessageForm from './ChatMessageForm.svelte';
import ChatElement from './ChatElement.svelte';
import {afterUpdate, beforeUpdate, onMount} from "svelte";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import ChatMessageForm from "./ChatMessageForm.svelte";
import ChatElement from "./ChatElement.svelte";
import { afterUpdate, beforeUpdate, onMount } from "svelte";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
let listDom: HTMLElement;
let chatWindowElement: HTMLElement;
let handleFormBlur: { blur():void };
let handleFormBlur: { blur(): void };
let autoscroll: boolean;
beforeUpdate(() => {
autoscroll = listDom && (listDom.offsetHeight + listDom.scrollTop) > (listDom.scrollHeight - 20);
autoscroll = listDom && listDom.offsetHeight + listDom.scrollTop > listDom.scrollHeight - 20;
});
onMount(() => {
listDom.scrollTo(0, listDom.scrollHeight);
})
});
afterUpdate(() => {
if (autoscroll) listDom.scrollTo(0, listDom.scrollHeight);
@ -32,78 +32,81 @@
function closeChat() {
chatVisibilityStore.set(false);
}
function onKeyDown(e:KeyboardEvent) {
if (e.key === 'Escape') {
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") {
closeChat();
}
}
</script>
<svelte:window on:keydown={onKeyDown} on:click={onClick}/>
<svelte:window on:keydown={onKeyDown} on:click={onClick} />
<aside class="chatWindow nes-container is-rounded is-dark" transition:fly="{{ x: -1000, duration: 500 }}" bind:this={chatWindowElement}>
<aside class="chatWindow nes-container is-rounded is-dark" transition:fly={{ x: -1000, duration: 500 }}
bind:this={chatWindowElement}>
<p class="close-icon" on:click={closeChat}>&times</p>
<section class="messagesList" bind:this={listDom}>
<ul>
<li><p class="system-text">Here is your chat history: </p></li>
{#each $chatMessagesStore as message, i}
<li><ChatElement message={message} line={i}></ChatElement></li>
{/each}
<li><p class="system-text">Here is your chat history:</p></li>
{#each $chatMessagesStore as message, i}
<li><ChatElement {message} line={i} /></li>
{/each}
</ul>
</section>
<section class="messageForm">
<ChatMessageForm bind:handleForm={handleFormBlur}></ChatMessageForm>
<ChatMessageForm bind:handleForm={handleFormBlur} />
</section>
</aside>
<style lang="scss">
p.close-icon {
position: absolute;
padding: 4px;
right: 12px;
font-size: 30px;
line-height: 25px;
cursor: pointer;
position: absolute;
padding: 4px;
right: 12px;
font-size: 30px;
line-height: 25px;
cursor: pointer;
}
p.system-text {
border-radius: 8px;
margin-bottom: 10px;
padding:6px;
overflow-wrap: break-word;
max-width: 100%;
display: inline-block;
border-radius: 8px;
margin-bottom: 10px;
padding: 6px;
overflow-wrap: break-word;
max-width: 100%;
display: inline-block;
}
aside.chatWindow {
z-index:100;
pointer-events: auto;
position: absolute;
top: 0;
left: 0;
height: 100vh;
width:30vw;
min-width: 350px;
color: whitesmoke;
display: flex;
flex-direction: column;
margin: 0;
padding: 10px;
z-index: 100;
pointer-events: auto;
position: absolute;
top: 0;
left: 0;
height: 100vh;
width: 30vw;
min-width: 350px;
color: whitesmoke;
display: flex;
flex-direction: column;
padding: 10px;
margin: 0;
.messagesList {
margin-top: 35px;
overflow-y: auto;
flex: auto;
border-bottom-right-radius: 16px;
border-top-right-radius: 16px;
ul {
list-style-type: none;
padding-left: 0;
.messagesList {
margin-top: 35px;
overflow-y: auto;
flex: auto;
ul {
list-style-type: none;
padding-left: 0;
}
}
.messageForm {
flex: 0 70px;
padding-top: 15px;
}
}
.messageForm {
flex: 0 70px;
padding-top: 15px;
}
}
</style>

View File

@ -1,9 +1,9 @@
<script lang="ts">
import {ChatMessageTypes} from "../../Stores/ChatStore";
import type {ChatMessage} from "../../Stores/ChatStore";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import ChatPlayerName from './ChatPlayerName.svelte';
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
import { ChatMessageTypes } from "../../Stores/ChatStore";
import type { ChatMessage } from "../../Stores/ChatStore";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import ChatPlayerName from "./ChatPlayerName.svelte";
import type { PlayerInterface } from "../../Phaser/Game/PlayerInterface";
export let message: ChatMessage;
export let line: number;
@ -18,30 +18,38 @@
}
function renderDate(date: Date) {
return date.toLocaleTimeString(navigator.language, {
hour: '2-digit',
minute:'2-digit'
hour: "2-digit",
minute: "2-digit",
});
}
function isLastIteration(index: number) {
return targets.length -1 === index;
return targets.length - 1 === index;
}
</script>
<div class="chatElement">
<div class="messagePart">
{#if message.type === ChatMessageTypes.userIncoming}
&gt;&gt; {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} entered <span class="date">({renderDate(message.date)})</span>
&gt;&gt; {#each targets as target, index}<ChatPlayerName
player={target}
{line}
/>{#if !isLastIteration(index)}, {/if}{/each} entered
<span class="date">({renderDate(message.date)})</span>
{:else if message.type === ChatMessageTypes.userOutcoming}
&lt;&lt; {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} left <span class="date">({renderDate(message.date)})</span>
&lt;&lt; {#each targets as target, index}<ChatPlayerName
player={target}
{line}
/>{#if !isLastIteration(index)}, {/if}{/each} left
<span class="date">({renderDate(message.date)})</span>
{:else if message.type === ChatMessageTypes.me}
<h4>Me: <span class="date">({renderDate(message.date)})</span></h4>
{#each texts as text}
<div><p class="my-text nes-balloon from-left is-dark">{@html urlifyText(text)}</p></div>
<div><p class="my-text">{@html urlifyText(text)}</p></div>
{/each}
{:else}
<h4><ChatPlayerName player={author} line={line}></ChatPlayerName>: <span class="date">({renderDate(message.date)})</span></h4>
<h4><ChatPlayerName player={author} {line} />: <span class="date">({renderDate(message.date)})</span></h4>
{#each texts as text}
<div><p class="other-text nes-balloon from-right is-dark">{@html urlifyText(text)}</p></div>
<div><p class="other-text">{@html urlifyText(text)}</p></div>
{/each}
{/if}
</div>
@ -49,29 +57,33 @@
<style lang="scss">
div.chatElement {
display: flex;
margin-bottom: 20px;
display: flex;
margin-bottom: 20px;
.messagePart {
flex-grow:1;
max-width: 100%;
.messagePart {
flex-grow: 1;
max-width: 100%;
span.date {
font-size: 80%;
color: gray;
span.date {
font-size: 80%;
color: gray;
}
div > p {
border-radius: 8px;
margin-bottom: 10px;
padding: 6px;
overflow-wrap: break-word;
max-width: 100%;
display: inline-block;
&.other-text {
background: gray;
}
&.other-text {
float: right;
}
}
}
div > p {
overflow-wrap: break-word;
max-width: 100%;
display: inline-block;
min-width: 75px;
padding: 5px;
&.other-text {
float: right;
}
}
}
}
</style>

View File

@ -1,13 +1,13 @@
<script lang="ts">
import {chatMessagesStore, chatInputFocusStore} from "../../Stores/ChatStore";
import { chatMessagesStore, chatInputFocusStore } from "../../Stores/ChatStore";
export const handleForm = {
blur() {
inputElement.blur();
}
}
},
};
let inputElement: HTMLElement;
let newMessageText = '';
let newMessageText = "";
function onFocus() {
chatInputFocusStore.set(true);
@ -19,14 +19,14 @@
function saveMessage() {
if (!newMessageText) return;
chatMessagesStore.addPersonnalMessage(newMessageText);
newMessageText = '';
newMessageText = "";
}
</script>
<form on:submit|preventDefault={saveMessage}>
<input class="nes-input is-dark" type="text" bind:value={newMessageText} placeholder="Enter your message..." on:focus={onFocus} on:blur={onBlur} bind:this={inputElement}>
<button class="nes-btn" type="submit">
<img src="/static/images/send.png" alt="Send" width="20">
<img src="/static/images/send.png" alt="Send" width="20" />
</button>
</form>

View File

@ -1,8 +1,8 @@
<script lang="ts">
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
import {chatSubMenuVisibilityStore} from "../../Stores/ChatStore";
import {onDestroy, onMount} from "svelte";
import type {Unsubscriber} from "svelte/store";
import type { PlayerInterface } from "../../Phaser/Game/PlayerInterface";
import { chatSubMenuVisibilityStore } from "../../Stores/ChatStore";
import { onDestroy, onMount } from "svelte";
import type { Unsubscriber } from "svelte/store";
import ChatSubMenu from "./ChatSubMenu.svelte";
export let player: PlayerInterface;
@ -17,35 +17,33 @@
onMount(() => {
chatSubMenuVisivilytUnsubcribe = chatSubMenuVisibilityStore.subscribe((newValue) => {
isSubMenuOpen = (newValue === player.name + line);
})
})
isSubMenuOpen = newValue === player.name + line;
});
});
onDestroy(() => {
chatSubMenuVisivilytUnsubcribe();
})
});
</script>
<span class="subMenu">
<span class="chatPlayerName" style="color: {player.color || 'white'}" on:click={openSubMenu}>
{player.name}
{player.name}
</span>
{#if isSubMenuOpen}
<ChatSubMenu player={player}/>
<ChatSubMenu {player} />
{/if}
</span>
<style lang="scss">
span.subMenu {
display: inline-block;
}
span.chatPlayerName {
margin-left: 3px;
}
.chatPlayerName:hover {
text-decoration: underline;
cursor: pointer;
span.subMenu {
display: inline-block;
}
span.chatPlayerName {
margin-left: 3px;
}
.chatPlayerName:hover {
text-decoration: underline;
cursor: pointer;
}
</style>

View File

@ -1,10 +1,9 @@
<script lang="ts">
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
import {requestVisitCardsStore} from "../../Stores/GameStore";
import type { PlayerInterface } from "../../Phaser/Game/PlayerInterface";
import { requestVisitCardsStore } from "../../Stores/GameStore";
export let player: PlayerInterface;
function openVisitCard() {
if (player.visitCardUrl) {
requestVisitCardsStore.set(player.visitCardUrl);
@ -17,17 +16,16 @@
<li><button class="text-btn" disabled>Add friend</button></li>
</ul>
<style lang="scss">
ul.selectMenu {
background-color: whitesmoke;
position: absolute;
padding: 5px;
border-radius: 4px;
list-style-type: none;
ul.selectMenu {
background-color: whitesmoke;
position: absolute;
padding: 5px;
border-radius: 4px;
list-style-type: none;
li {
text-align: center;
li {
text-align: center;
}
}
}
</style>

View File

@ -1,7 +1,7 @@
<script lang="typescript">
import type { Game } from "../../Phaser/Game/Game";
import {CustomizeScene, CustomizeSceneName} from "../../Phaser/Login/CustomizeScene";
import {activeRowStore} from "../../Stores/CustomCharacterStore";
import { CustomizeScene, CustomizeSceneName } from "../../Phaser/Login/CustomizeScene";
import { activeRowStore } from "../../Stores/CustomCharacterStore";
export let game: Game;
@ -30,7 +30,6 @@
function finish() {
customCharacterScene.nextSceneToCamera();
}
</script>
<form class="customCharacterScene">
@ -38,80 +37,100 @@
<h2>Customize your Avatar</h2>
</section>
<section class="action action-move">
<button class="customCharacterSceneButton customCharacterSceneButtonLeft nes-btn" on:click|preventDefault={ selectLeft }> &lt; </button>
<button class="customCharacterSceneButton customCharacterSceneButtonRight nes-btn" on:click|preventDefault={ selectRight }> &gt; </button>
<button
class="customCharacterSceneButton customCharacterSceneButtonLeft nes-btn"
on:click|preventDefault={selectLeft}
>
&lt;
</button>
<button
class="customCharacterSceneButton customCharacterSceneButtonRight nes-btn"
on:click|preventDefault={selectRight}
>
&gt;
</button>
</section>
<section class="action">
{#if $activeRowStore === 0}
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={ previousScene }>Return</button>
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={previousScene}
>Return</button
>
{/if}
{#if $activeRowStore !== 0}
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={ selectUp }>Back <img src="resources/objects/arrow_up_black.png" alt=""/></button>
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={selectUp}
>Back <img src="resources/objects/arrow_up_black.png" alt="" /></button
>
{/if}
{#if $activeRowStore === 5}
<button type="submit" class="customCharacterSceneFormSubmit nes-btn is-primary" on:click|preventDefault={ finish }>Finish</button>
<button
type="submit"
class="customCharacterSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={finish}>Finish</button
>
{/if}
{#if $activeRowStore !== 5}
<button type="submit" class="customCharacterSceneFormSubmit nes-btn is-primary" on:click|preventDefault={ selectDown }>Next <img src="resources/objects/arrow_down.png" alt=""/></button>
<button
type="submit"
class="customCharacterSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={selectDown}>Next <img src="resources/objects/arrow_down.png" alt="" /></button
>
{/if}
</section>
</form>
<style lang="scss">
form.customCharacterScene {
font-family: "Press Start 2P";
pointer-events: auto;
color: #ebeeee;
section {
margin: 10px;
&.action {
text-align: center;
margin-top: 55vh;
}
h2 {
form.customCharacterScene {
font-family: "Press Start 2P";
margin: 1px;
}
pointer-events: auto;
color: #ebeeee;
&.text-center {
text-align: center;
}
section {
margin: 10px;
button.customCharacterSceneButton {
position: absolute;
top: 33vh;
margin: 0;
}
&.action {
text-align: center;
margin-top: 55vh;
}
button.customCharacterSceneFormBack {
color: #292929;
}
h2 {
font-family: "Press Start 2P";
margin: 1px;
}
&.text-center {
text-align: center;
}
button.customCharacterSceneButton {
position: absolute;
top: 33vh;
margin: 0;
}
button.customCharacterSceneFormBack {
color: #292929;
}
}
button {
font-family: "Press Start 2P";
&.customCharacterSceneButtonLeft {
left: 33vw;
}
&.customCharacterSceneButtonRight {
right: 33vw;
}
}
}
button {
font-family: "Press Start 2P";
&.customCharacterSceneButtonLeft {
left: 33vw;
}
&.customCharacterSceneButtonRight {
right: 33vw;
}
@media only screen and (max-width: 800px) {
form.customCharacterScene button.customCharacterSceneButtonLeft {
left: 5vw;
}
form.customCharacterScene button.customCharacterSceneButtonRight {
right: 5vw;
}
}
}
@media only screen and (max-width: 800px) {
form.customCharacterScene button.customCharacterSceneButtonLeft{
left: 5vw;
}
form.customCharacterScene button.customCharacterSceneButtonRight{
right: 5vw;
}
}
</style>

View File

@ -1,76 +1,72 @@
<script lang="typescript">
import type { Unsubscriber } from "svelte/store";
import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore";
import { onDestroy, onMount } from "svelte";
import { EmojiButton } from "@joeattardi/emoji-button";
import { isMobile } from "../../Enum/EnvironmentVariable";
import type { Unsubscriber } from "svelte/store";
import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore";
import { onDestroy, onMount } from "svelte";
import { EmojiButton } from '@joeattardi/emoji-button';
import { isMobile } from "../../Enum/EnvironmentVariable";
let emojiContainer: HTMLElement;
let picker: EmojiButton;
let emojiContainer: HTMLElement;
let picker: EmojiButton;
let unsubscriber: Unsubscriber | null = null;
let unsubscriber: Unsubscriber | null = null;
onMount(() => {
picker = new EmojiButton({
rootElement: emojiContainer,
styleProperties: {
"--font": "Press Start 2P",
},
emojisPerRow: isMobile() ? 6 : 8,
autoFocusSearch: false,
style: "twemoji",
});
//the timeout is here to prevent the menu from flashing
setTimeout(() => picker.showPicker(emojiContainer), 100);
onMount(() => {
picker = new EmojiButton({
rootElement: emojiContainer,
styleProperties: {
'--font': 'Press Start 2P'
},
theme: 'dark',
emojisPerRow: isMobile() ? 6 : 8,
autoFocusSearch: false,
style: 'twemoji',
});
//the timeout is here to prevent the menu from flashing
setTimeout(() => picker.showPicker(emojiContainer), 100);
picker.on("emoji", (selection) => {
emoteStore.set({
unicode: selection.emoji,
url: selection.url,
name: selection.name,
});
});
picker.on("emoji", (selection) => {
emoteStore.set({
unicode: selection.emoji,
url: selection.url,
name: selection.name
});
picker.on("hidden", () => {
emoteMenuStore.closeEmoteMenu();
});
});
picker.on("hidden", () => {
emoteMenuStore.closeEmoteMenu();
});
})
function onKeyDown(e:KeyboardEvent) {
if (e.key === 'Escape') {
emoteMenuStore.closeEmoteMenu();
}
}
onDestroy(() => {
if (unsubscriber) {
unsubscriber();
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") {
emoteMenuStore.closeEmoteMenu();
}
}
picker.destroyPicker();
})
onDestroy(() => {
if (unsubscriber) {
unsubscriber();
}
picker.destroyPicker();
});
</script>
<svelte:window on:keydown={onKeyDown}/>
<svelte:window on:keydown={onKeyDown} />
<div class="emote-menu-container">
<div class="emote-menu" bind:this={emojiContainer}></div>
<div class="emote-menu" bind:this={emojiContainer} />
</div>
<style lang="scss">
.emote-menu-container {
display: flex;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
}
.emote-menu-container {
display: flex;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
}
.emote-menu {
pointer-events: all;
}
.emote-menu {
pointer-events: all;
}
</style>

View File

@ -1,22 +1,22 @@
<script lang="typescript">
import type {Game} from "../../Phaser/Game/Game";
import {EnableCameraScene, EnableCameraSceneName} from "../../Phaser/Login/EnableCameraScene";
import type { Game } from "../../Phaser/Game/Game";
import { EnableCameraScene, EnableCameraSceneName } from "../../Phaser/Login/EnableCameraScene";
import {
audioConstraintStore,
cameraListStore,
localStreamStore,
microphoneListStore,
videoConstraintStore
videoConstraintStore,
} from "../../Stores/MediaStore";
import {onDestroy} from "svelte";
import { onDestroy } from "svelte";
import HorizontalSoundMeterWidget from "./HorizontalSoundMeterWidget.svelte";
import cinemaCloseImg from "../images/cinema-close.svg";
import cinemaImg from "../images/cinema.svg";
import microphoneImg from "../images/microphone.svg";
export let game: Game;
let selectedCamera : string|undefined = undefined;
let selectedMicrophone : string|undefined = undefined;
let selectedCamera: string | undefined = undefined;
let selectedMicrophone: string | undefined = undefined;
const enableCameraScene = game.scene.getScene(EnableCameraSceneName) as EnableCameraScene;
@ -29,16 +29,16 @@
return {
update(newStream: MediaStream) {
if (node.srcObject != newStream) {
node.srcObject = newStream
node.srcObject = newStream;
}
}
}
},
};
}
let stream: MediaStream | null;
const unsubscribe = localStreamStore.subscribe(value => {
if (value.type === 'success') {
const unsubscribe = localStreamStore.subscribe((value) => {
if (value.type === "success") {
stream = value.stream;
if (stream !== null) {
@ -62,7 +62,7 @@
function normalizeDeviceName(label: string): string {
// remove IDs (that can appear in Chrome, like: "HD Pro Webcam (4df7:4eda)"
return label.replace(/(\([[0-9a-f]{4}:[0-9a-f]{4}\))/g, '').trim();
return label.replace(/(\([[0-9a-f]{4}:[0-9a-f]{4}\))/g, "").trim();
}
function selectCamera() {
@ -72,28 +72,27 @@
function selectMicrophone() {
audioConstraintStore.setDeviceId(selectedMicrophone);
}
</script>
<form class="enableCameraScene" on:submit|preventDefault={submit}>
<section class="text-center">
<h2>Turn on your camera and microphone</h2>
</section>
{#if $localStreamStore.type === 'success' && $localStreamStore.stream}
<video class="myCamVideoSetup" use:srcObject={$localStreamStore.stream} autoplay muted playsinline></video>
{:else }
<div class="webrtcsetup">
<img class="background-img" src={cinemaCloseImg} alt="">
</div>
{/if}
<HorizontalSoundMeterWidget stream={stream}></HorizontalSoundMeterWidget>
{#if $localStreamStore.type === "success" && $localStreamStore.stream}
<video class="myCamVideoSetup" use:srcObject={$localStreamStore.stream} autoplay muted playsinline />
{:else}
<div class="webrtcsetup">
<img class="background-img" src={cinemaCloseImg} alt="" />
</div>
{/if}
<HorizontalSoundMeterWidget {stream} />
<section class="selectWebcamForm">
{#if $cameraListStore.length > 1 }
{#if $cameraListStore.length > 1}
<div class="control-group">
<img src={cinemaImg} alt="Camera" />
<div class="nes-select is-dark">
<!-- svelte-ignore a11y-no-onchange -->
<select bind:value={selectedCamera} on:change={selectCamera}>
{#each $cameraListStore as camera}
<option value={camera.deviceId}>
@ -105,10 +104,11 @@
</div>
{/if}
{#if $microphoneListStore.length > 1 }
{#if $microphoneListStore.length > 1}
<div class="control-group">
<img src={microphoneImg} alt="Microphone" />
<div class="nes-select is-dark">
<!-- svelte-ignore a11y-no-onchange -->
<select bind:value={selectedMicrophone} on:change={selectMicrophone}>
{#each $microphoneListStore as microphone}
<option value={microphone.deviceId}>
@ -119,111 +119,109 @@
</div>
</div>
{/if}
</section>
<section class="action">
<button type="submit" class="nes-btn is-primary letsgo" >Let's go!</button>
<button type="submit" class="nes-btn is-primary letsgo">Let's go!</button>
</section>
</form>
<style lang="scss">
.enableCameraScene {
pointer-events: auto;
margin: 20px auto 0;
color: #ebeeee;
pointer-events: auto;
margin: 20px auto 0;
color: #ebeeee;
section.selectWebcamForm {
margin-top: 3vh;
margin-bottom: 3vh;
min-height: 10vh;
width: 50vw;
margin-left: auto;
margin-right: auto;
section.selectWebcamForm {
margin-top: 3vh;
margin-bottom: 3vh;
min-height: 10vh;
width: 50vw;
margin-left: auto;
margin-right: auto;
select {
font-family: "Press Start 2P";
margin-top: 1vh;
margin-bottom: 1vh;
select {
font-family: "Press Start 2P";
margin-top: 1vh;
margin-bottom: 1vh;
}
option {
font-family: "Press Start 2P";
}
}
option {
section.action {
text-align: center;
margin: 0;
width: 100%;
}
h2 {
font-family: "Press Start 2P";
}
margin: 1px;
}
section.action{
text-align: center;
margin: 0;
width: 100%;
}
h2{
font-family: "Press Start 2P";
margin: 1px;
}
section.text-center{
text-align: center;
}
button.letsgo {
font-size: 200%;
}
.control-group {
display: flex;
flex-direction: row;
max-height: 60px;
margin-top: 10px;
img {
width: 30px;
margin-right: 10px;
section.text-center {
text-align: center;
}
}
.webrtcsetup{
margin-top: 2vh;
margin-left: auto;
margin-right: auto;
height: 28.125vw;
width: 50vw;
border: white 6px solid;
display: flex;
align-items: center;
justify-content: center;
img.background-img {
width: 40%;
button.letsgo {
font-size: 200%;
}
}
.myCamVideoSetup {
margin-top: 2vh;
margin-left: auto;
margin-right: auto;
max-height: 50vh;
width: 50vw;
border: white 6px solid;
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
display: flex;
align-items: center;
justify-content: center;
}
.control-group {
display: flex;
flex-direction: row;
max-height: 60px;
margin-top: 10px;
img {
width: 30px;
margin-right: 10px;
}
}
.webrtcsetup {
margin-top: 2vh;
margin-left: auto;
margin-right: auto;
height: 28.125vw;
width: 50vw;
border: white 6px solid;
display: flex;
align-items: center;
justify-content: center;
img.background-img {
width: 40%;
}
}
.myCamVideoSetup {
margin-top: 2vh;
margin-left: auto;
margin-right: auto;
max-height: 50vh;
width: 50vw;
border: white 6px solid;
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
display: flex;
align-items: center;
justify-content: center;
}
}
@media only screen and (max-width: 800px) {
.enableCameraScene h2 {
font-size: 80%;
}
.enableCameraScene .control-group .nes-select {
font-size: 80%;
}
.enableCameraScene button.letsgo {
font-size: 160%;
}
.enableCameraScene h2 {
font-size: 80%;
}
.enableCameraScene .control-group .nes-select {
font-size: 80%;
}
.enableCameraScene button.letsgo {
font-size: 160%;
}
}
</style>

View File

@ -1,7 +1,7 @@
<script lang="typescript">
import { AudioContext } from 'standardized-audio-context';
import {SoundMeter} from "../../Phaser/Components/SoundMeter";
import {onDestroy} from "svelte";
import { AudioContext } from "standardized-audio-context";
import { SoundMeter } from "../../Phaser/Components/SoundMeter";
import { onDestroy } from "svelte";
export let stream: MediaStream | null;
let volume = 0;
@ -11,6 +11,7 @@
let timeout: ReturnType<typeof setTimeout>;
const soundMeter = new SoundMeter();
let display = false;
let error = false;
$: {
if (stream && stream.getAudioTracks().length > 0) {
@ -19,17 +20,19 @@
if (timeout) {
clearInterval(timeout);
error = false;
}
timeout = setInterval(() => {
try{
volume = parseInt((soundMeter.getVolume() / 100 * NB_BARS).toFixed(0));
//console.log(volume);
}catch(err){
try {
volume = parseInt(((soundMeter.getVolume() / 100) * NB_BARS).toFixed(0));
} catch (err) {
if (!error) {
console.error(err);
error = true;
}
}
}, 100);
} else {
display = false;
}
@ -40,11 +43,10 @@
if (timeout) {
clearInterval(timeout);
}
})
});
function color(i: number, volume: number) {
const red = 255 * i / NB_BARS;
const red = (255 * i) / NB_BARS;
const green = 255 * (1 - i / NB_BARS);
let alpha = 1;
@ -52,31 +54,29 @@
alpha = 0.5;
}
return 'background-color:rgba('+red+', '+green+', 0, '+alpha+')';
return "background-color:rgba(" + red + ", " + green + ", 0, " + alpha + ")";
}
</script>
<div class="horizontal-sound-meter" class:active={display}>
{#each [...Array(NB_BARS).keys()] as i (i)}
<div style={color(i, volume)}></div>
<div style={color(i, volume)} />
{/each}
</div>
<style lang="scss">
.horizontal-sound-meter {
display: flex;
flex-direction: row;
width: 50%;
height: 30px;
margin-left: auto;
margin-right: auto;
margin-top: 1vh;
display: flex;
flex-direction: row;
width: 50%;
height: 30px;
margin-left: auto;
margin-right: auto;
margin-top: 1vh;
}
.horizontal-sound-meter div {
margin-left: 5px;
flex-grow: 1;
margin-left: 5px;
flex-grow: 1;
}
</style>

View File

@ -1,9 +1,9 @@
<script lang="typescript">
import { fly } from 'svelte/transition';
import {helpCameraSettingsVisibleStore} from "../../Stores/HelpCameraSettingsStore";
import { fly } from "svelte/transition";
import { helpCameraSettingsVisibleStore } from "../../Stores/HelpCameraSettingsStore";
import firefoxImg from "./images/help-setting-camera-permission-firefox.png";
import chromeImg from "./images/help-setting-camera-permission-chrome.png";
import {getNavigatorType, isAndroid as isAndroidFct, NavigatorType} from "../../WebRtc/DeviceUtils";
import { getNavigatorType, isAndroid as isAndroidFct, NavigatorType } from "../../WebRtc/DeviceUtils";
let isAndroid = isAndroidFct();
let isFirefox = getNavigatorType() === NavigatorType.firefox;
@ -16,30 +16,37 @@
function close() {
helpCameraSettingsVisibleStore.set(false);
}
</script>
<form class="helpCameraSettings nes-container" on:submit|preventDefault={close} transition:fly="{{ y: -900, duration: 500 }}">
<form
class="helpCameraSettings nes-container"
on:submit|preventDefault={close}
transition:fly={{ y: -900, duration: 500 }}
>
<section>
<h2>Camera / Microphone access needed</h2>
<p class="err">Permission denied</p>
<p>You must allow camera and microphone access in your browser.</p>
<p>
{#if isFirefox }
<p class="err">Please click the "Remember this decision" checkbox, if you don't want Firefox to keep asking you the authorization.</p>
{#if isFirefox}
<p class="err">
Please click the "Remember this decision" checkbox, if you don't want Firefox to keep asking you the
authorization.
</p>
<img src={firefoxImg} alt="" />
{:else if isChrome && !isAndroid }
{:else if isChrome && !isAndroid}
<img src={chromeImg} alt="" />
{/if}
</p>
</section>
<section>
<button class="helpCameraSettingsFormRefresh nes-btn" on:click|preventDefault={refresh}>Refresh</button>
<button type="submit" class="helpCameraSettingsFormContinue nes-btn is-primary" on:click|preventDefault={close}>Continue without webcam</button>
<button type="submit" class="helpCameraSettingsFormContinue nes-btn is-primary" on:click|preventDefault={close}
>Continue without webcam</button
>
</section>
</form>
<style lang="scss">
.helpCameraSettings {
pointer-events: auto;
@ -53,13 +60,13 @@
text-align: center;
h2 {
font-family: 'Press Start 2P';
font-family: "Press Start 2P";
}
section {
p {
margin: 15px;
font-family: 'Press Start 2P';
font-family: "Press Start 2P";
& .err {
color: #ff0000;

View File

@ -11,7 +11,6 @@
}
</script>
<div class="layout-manager-list">
{#each $layoutManagerActionStore as action}
<div class="nes-container is-dark {action.type}" on:click={() => onClick(action.callback)}>
@ -20,39 +19,48 @@
{/each}
</div>
<style lang="scss">
div.layout-manager-list {
pointer-events: auto;
position: absolute;
left: 0;
right: 0;
bottom: 40px;
margin: 0 auto;
padding: 0;
width: clamp(200px, 20vw, 20vw);
div.layout-manager-list {
pointer-events: auto;
position: absolute;
left: 0;
right: 0;
bottom: 40px;
margin: 0 auto;
padding: 0;
width: clamp(200px, 20vw, 20vw);
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
animation: moveMessage .5s;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
}
div.nes-container {
padding: 8px 4px;
text-align: center;
&.warning {
background-color: #ff9800eb;
color: #000;
animation: moveMessage 0.5s;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
}
}
@keyframes moveMessage {
0% {bottom: 40px;}
50% {bottom: 30px;}
100% {bottom: 40px;}
}
div.nes-container {
padding: 8px 4px;
text-align: center;
font-family: Lato;
color: whitesmoke;
background-color: rgb(0, 0, 0, 0.5);
&.warning {
background-color: #ff9800eb;
color: #000;
}
}
@keyframes moveMessage {
0% {
bottom: 40px;
}
50% {
bottom: 30px;
}
100% {
bottom: 40px;
}
}
</style>

View File

@ -1,22 +1,22 @@
<script lang="typescript">
import type {Game} from "../../Phaser/Game/Game";
import {LoginScene, LoginSceneName} from "../../Phaser/Login/LoginScene";
import {DISPLAY_TERMS_OF_USE, MAX_USERNAME_LENGTH} from "../../Enum/EnvironmentVariable";
import type { Game } from "../../Phaser/Game/Game";
import { LoginScene, LoginSceneName } from "../../Phaser/Login/LoginScene";
import { DISPLAY_TERMS_OF_USE, MAX_USERNAME_LENGTH } from "../../Enum/EnvironmentVariable";
import logoImg from "../images/logo.png";
import {gameManager} from "../../Phaser/Game/GameManager";
import { gameManager } from "../../Phaser/Game/GameManager";
export let game: Game;
const loginScene = game.scene.getScene(LoginSceneName) as LoginScene;
let name = gameManager.getPlayerName() || '';
let name = gameManager.getPlayerName() || "";
let startValidating = false;
function submit() {
startValidating = true;
let finalName = name.trim();
if (finalName !== '') {
if (finalName !== "") {
loginScene.login(finalName);
}
}
@ -29,17 +29,34 @@
<section class="text-center">
<h2>Enter your name</h2>
</section>
<input type="text" name="loginSceneName" class="nes-input is-dark" autofocus maxlength={MAX_USERNAME_LENGTH} bind:value={name} on:keypress={() => {startValidating = true}} class:is-error={name.trim() === '' && startValidating} />
<!-- svelte-ignore a11y-autofocus -->
<input
type="text"
name="loginSceneName"
class="nes-input is-dark"
autofocus
maxlength={MAX_USERNAME_LENGTH}
bind:value={name}
on:keypress={() => {
startValidating = true;
}}
class:is-error={name.trim() === "" && startValidating}
/>
<section class="error-section">
{#if name.trim() === '' && startValidating }
<p class="err">The name is empty</p>
{/if}
{#if name.trim() === "" && startValidating}
<p class="err">The name is empty</p>
{/if}
</section>
{#if DISPLAY_TERMS_OF_USE}
<section class="terms-and-conditions">
<p>By continuing, you are agreeing our <a href="https://workadventu.re/terms-of-use" target="_blank">terms of use</a>, <a href="https://workadventu.re/privacy-policy" target="_blank">privacy policy</a> and <a href="https://workadventu.re/cookie-policy" target="_blank">cookie policy</a>.</p>
</section>
<section class="terms-and-conditions">
<p>
By continuing, you are agreeing our <a href="https://workadventu.re/terms-of-use" target="_blank"
>terms of use</a
>, <a href="https://workadventu.re/privacy-policy" target="_blank">privacy policy</a> and
<a href="https://workadventu.re/cookie-policy" target="_blank">cookie policy</a>.
</p>
</section>
{/if}
<section class="action">
<button type="submit" class="nes-btn is-primary loginSceneFormSubmit">Continue</button>
@ -47,76 +64,75 @@
</form>
<style lang="scss">
.loginScene {
pointer-events: auto;
margin: 20px auto 0;
width: 90%;
color: #ebeeee;
display: flex;
flex-flow: column wrap;
align-items: center;
input {
text-align: center;
font-family: "Press Start 2P";
max-width: 400px;
}
.terms-and-conditions {
max-width: 400px;
}
p.err {
color: #ce372b;
text-align: center;
}
section {
margin: 10px;
&.error-section {
min-height: 2rem;
margin: 0;
p {
margin: 0;
}
}
&.action {
text-align: center;
margin-top: 20px;
}
h2 {
font-family: "Press Start 2P";
margin: 1px;
}
&.text-center {
text-align: center;
}
a {
text-decoration: underline;
.loginScene {
pointer-events: auto;
margin: 20px auto 0;
width: 90%;
color: #ebeeee;
}
a:hover {
font-weight: 700;
}
display: flex;
flex-flow: column wrap;
align-items: center;
p {
text-align: left;
margin: 10px 10px;
}
input {
text-align: center;
font-family: "Press Start 2P";
max-width: 400px;
}
img {
width: 100%;
margin: 20px 0;
}
.terms-and-conditions {
max-width: 400px;
}
p.err {
color: #ce372b;
text-align: center;
}
section {
margin: 10px;
&.error-section {
min-height: 2rem;
margin: 0;
p {
margin: 0;
}
}
&.action {
text-align: center;
margin-top: 20px;
}
h2 {
font-family: "Press Start 2P";
margin: 1px;
}
&.text-center {
text-align: center;
}
a {
text-decoration: underline;
color: #ebeeee;
}
a:hover {
font-weight: 700;
}
p {
text-align: left;
margin: 10px 10px;
}
img {
width: 100%;
margin: 20px 0;
}
}
}
}
</style>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { gameManager } from "../../Phaser/Game/GameManager";
import {onMount} from "svelte";
import { onMount } from "svelte";
let gameScene = gameManager.getCurrentGameScene();
@ -14,29 +14,33 @@
onMount(() => {
if (gameScene.mapFile.properties !== undefined) {
const propertyName = gameScene.mapFile.properties.find((property) => property.name === 'mapName')
if ( propertyName !== undefined && typeof propertyName.value === 'string') {
const propertyName = gameScene.mapFile.properties.find((property) => property.name === "mapName");
if (propertyName !== undefined && typeof propertyName.value === "string") {
mapName = propertyName.value;
}
const propertyDescription = gameScene.mapFile.properties.find((property) => property.name === 'mapDescription')
if (propertyDescription !== undefined && typeof propertyDescription.value === 'string') {
const propertyDescription = gameScene.mapFile.properties.find(
(property) => property.name === "mapDescription"
);
if (propertyDescription !== undefined && typeof propertyDescription.value === "string") {
mapDescription = propertyDescription.value;
}
const propertyCopyright = gameScene.mapFile.properties.find((property) => property.name === 'mapCopyright')
if (propertyCopyright !== undefined && typeof propertyCopyright.value === 'string') {
const propertyCopyright = gameScene.mapFile.properties.find((property) => property.name === "mapCopyright");
if (propertyCopyright !== undefined && typeof propertyCopyright.value === "string") {
mapCopyright = propertyCopyright.value;
}
}
for (const tileset of gameScene.mapFile.tilesets) {
if (tileset.properties !== undefined) {
const propertyTilesetCopyright = tileset.properties.find((property) => property.name === 'tilesetCopyright')
if (propertyTilesetCopyright !== undefined && typeof propertyTilesetCopyright.value === 'string') {
const propertyTilesetCopyright = tileset.properties.find(
(property) => property.name === "tilesetCopyright"
);
if (propertyTilesetCopyright !== undefined && typeof propertyTilesetCopyright.value === "string") {
tilesetCopyright = [...tilesetCopyright, propertyTilesetCopyright.value]; //Assignment needed to trigger Svelte's reactivity
}
}
}
})
});
</script>
<div class="about-room-main">
@ -44,51 +48,58 @@
<section class="container-overflow">
<h3>{mapName}</h3>
<p class="string-HTML">{mapDescription}</p>
<h3 class="nes-pointer hoverable" on:click={() => expandedMapCopyright = !expandedMapCopyright}>Copyrights of the map</h3>
<p class="string-HTML" hidden="{!expandedMapCopyright}">{mapCopyright}</p>
<h3 class="nes-pointer hoverable" on:click={() => expandedTilesetCopyright = !expandedTilesetCopyright}>Copyrights of the tilesets</h3>
<section hidden="{!expandedTilesetCopyright}">
<h3 class="nes-pointer hoverable" on:click={() => (expandedMapCopyright = !expandedMapCopyright)}>
Copyrights of the map
</h3>
<p class="string-HTML" hidden={!expandedMapCopyright}>{mapCopyright}</p>
<h3 class="nes-pointer hoverable" on:click={() => (expandedTilesetCopyright = !expandedTilesetCopyright)}>
Copyrights of the tilesets
</h3>
<section hidden={!expandedTilesetCopyright}>
{#each tilesetCopyright as copyright}
<p class="string-HTML">{copyright}</p>
{:else}
<p>The map creator did not declare a copyright for the tilesets. Warning, This doesn't mean that those tilesets have no license.</p>
<p>
The map creator did not declare a copyright for the tilesets. Warning, This doesn't mean that those
tilesets have no license.
</p>
{/each}
</section>
</section>
</div>
<style lang="scss">
.string-HTML{
white-space: pre-line;
}
div.about-room-main {
height: calc(100% - 56px);
h2, h3 {
width: 100%;
text-align: center;
.string-HTML {
white-space: pre-line;
}
h3.hoverable:hover {
background-color: #3c3e40;
border-radius: 32px;
}
section.container-overflow {
height: calc(100% - 220px);
margin: 0;
padding: 0;
overflow-y: auto;
}
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
div.about-room-main {
section.container-overflow {
height: calc(100% - 120px);
}
height: calc(100% - 56px);
h2,
h3 {
width: 100%;
text-align: center;
}
h3.hoverable:hover {
background-color: #3c3e40;
border-radius: 32px;
}
section.container-overflow {
height: calc(100% - 220px);
margin: 0;
padding: 0;
overflow-y: auto;
}
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
div.about-room-main {
section.container-overflow {
height: calc(100% - 120px);
}
}
}
}
</style>

View File

@ -26,31 +26,31 @@
const selectedFile = inputAudio.files ? inputAudio.files[0] : null;
if (!selectedFile) {
errorFile = true;
throw 'no file selected';
throw "no file selected";
}
const fd = new FormData();
fd.append('file', selectedFile);
fd.append("file", selectedFile);
const res = await gameScene.connection?.uploadAudio(fd);
const audioGlobalMessage: PlayGlobalMessageInterface = {
content: (res as { path: string }).path,
type: AUDIO_TYPE,
broadcastToWorld: broadcast
}
inputAudio.value = '';
broadcastToWorld: broadcast,
};
inputAudio.value = "";
gameScene.connection?.emitGlobalMessage(audioGlobalMessage);
}
}
},
};
function inputAudioFile(event: Event) {
const eventTarget : EventTargetFiles = (event.target as EventTargetFiles);
if(!eventTarget || !eventTarget.files || eventTarget.files.length === 0){
const eventTarget: EventTargetFiles = event.target as EventTargetFiles;
if (!eventTarget || !eventTarget.files || eventTarget.files.length === 0) {
return;
}
const file = eventTarget.files[0];
if(!file) {
if (!file) {
return;
}
@ -61,52 +61,65 @@
function getFileSize(number: number) {
if (number < 1024) {
return number + 'bytes';
return number + "bytes";
} else if (number >= 1024 && number < 1048576) {
return (number / 1024).toFixed(1) + 'KB';
return (number / 1024).toFixed(1) + "KB";
} else if (number >= 1048576) {
return (number / 1048576).toFixed(1) + 'MB';
return (number / 1048576).toFixed(1) + "MB";
} else {
return '';
return "";
}
}
</script>
<section class="section-input-send-audio">
<img class="nes-pointer" src="{uploadFile}" alt="Upload a file" on:click|preventDefault={ () => {fileInput.click();}}>
<img
class="nes-pointer"
src={uploadFile}
alt="Upload a file"
on:click|preventDefault={() => {
fileInput.click();
}}
/>
{#if fileName !== undefined}
<p>{fileName} : {fileSize}</p>
{/if}
{#if errorFile}
<p class="err">No file selected. You need to upload a file before sending it.</p>
{/if}
<input type="file" id="input-send-audio" bind:this={fileInput} on:change={(e) => {inputAudioFile(e)}}>
<input
type="file"
id="input-send-audio"
bind:this={fileInput}
on:change={(e) => {
inputAudioFile(e);
}}
/>
</section>
<style lang="scss">
section.section-input-send-audio {
display: flex;
flex-direction: column;
section.section-input-send-audio {
display: flex;
flex-direction: column;
height: 100%;
text-align: center;
height: 100%;
text-align: center;
img {
flex: 1 1 auto;
max-height: 80%;
margin-bottom: 20px;
img {
flex: 1 1 auto;
max-height: 80%;
margin-bottom: 20px;
}
p {
margin-bottom: 5px;
color: whitesmoke;
font-size: 1rem;
&.err {
color: #ce372b;
}
}
input {
display: none;
}
}
p {
margin-bottom: 5px;
color: whitesmoke;
font-size: 1rem;
&.err {
color: #ce372b;
}
}
input {
display: none;
}
}
</style>

View File

@ -1,5 +1,4 @@
<script lang="ts">
function goToGettingStarted() {
const sparkHost = "https://workadventu.re/getting-started";
window.open(sparkHost, "_blank");
@ -10,7 +9,7 @@
window.open(sparkHost, "_blank");
}
import {contactPageStore} from "../../Stores/MenuStore";
import { contactPageStore } from "../../Stores/MenuStore";
</script>
<div class="create-map-main">
@ -18,8 +17,8 @@
<section>
<h3>Getting started</h3>
<p>
WorkAdventure allows you to create an online space to communicate spontaneously with others.
And it all starts with creating your own space. Choose from a large selection of prefabricated maps by our team.
WorkAdventure allows you to create an online space to communicate spontaneously with others. And it all
starts with creating your own space. Choose from a large selection of prefabricated maps by our team.
</p>
<button type="button" class="nes-btn is-primary" on:click={goToGettingStarted}>Getting started</button>
</section>
@ -30,35 +29,37 @@
<button type="button" class="nes-btn" on:click={goToBuildingMap}>Create your map</button>
</section>
<iframe title="contact"
src="{$contactPageStore}"
allow="clipboard-read; clipboard-write self {$contactPageStore}"
allowfullscreen></iframe>
<iframe
title="contact"
src={$contactPageStore}
allow="clipboard-read; clipboard-write self {$contactPageStore}"
allowfullscreen
/>
</section>
</div>
<style lang="scss">
div.create-map-main {
height: calc(100% - 56px);
height: calc(100% - 56px);
text-align: center;
text-align: center;
section {
margin-bottom: 50px;
}
section {
margin-bottom: 50px;
}
section.container-overflow {
height: 100%;
margin: 0;
padding: 0;
overflow: auto;
}
section.container-overflow {
height: 100%;
margin: 0;
padding: 0;
overflow: auto;
}
}
iframe {
border: none;
height: calc(100% - 56px);
width: 100%;
margin: 0;
border: none;
height: calc(100% - 56px);
width: 100%;
margin: 0;
}
</style>

View File

@ -1,33 +1,32 @@
<script lang="ts">
import {onDestroy, onMount} from "svelte";
import {iframeListener} from "../../Api/IframeListener";
import { onDestroy, onMount } from "svelte";
import { iframeListener } from "../../Api/IframeListener";
export let url: string;
export let allowApi: boolean;
let HTMLIframe: HTMLIFrameElement;
onMount( () => {
onMount(() => {
if (allowApi) {
iframeListener.registerIframe(HTMLIframe);
}
})
});
onDestroy( () => {
onDestroy(() => {
if (allowApi) {
iframeListener.unregisterIframe(HTMLIframe);
}
})
});
</script>
<iframe title="customSubMenu" src="{url}" bind:this={HTMLIframe}></iframe>
<iframe title="customSubMenu" src={url} bind:this={HTMLIframe} />
<style lang="scss">
iframe {
border: none;
height: calc(100% - 56px);
width: 100%;
margin: 0;
}
iframe {
border: none;
height: calc(100% - 56px);
width: 100%;
margin: 0;
}
</style>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import TextGlobalMessage from './TextGlobalMessage.svelte';
import AudioGlobalMessage from './AudioGlobalMessage.svelte';
import TextGlobalMessage from "./TextGlobalMessage.svelte";
import AudioGlobalMessage from "./AudioGlobalMessage.svelte";
let handleSendText: { sendTextMessage(broadcast: boolean): void };
let handleSendAudio: { sendAudioMessage(broadcast: boolean): Promise<void> };
@ -32,23 +32,31 @@
<div class="global-message-main">
<div class="global-message-subOptions">
<section>
<button type="button" class="nes-btn {inputSendTextActive ? 'is-disabled' : ''}" on:click|preventDefault={activateInputText}>Text</button>
<button
type="button"
class="nes-btn {inputSendTextActive ? 'is-disabled' : ''}"
on:click|preventDefault={activateInputText}>Text</button
>
</section>
<section>
<button type="button" class="nes-btn {uploadAudioActive ? 'is-disabled' : ''}" on:click|preventDefault={activateUploadAudio}>Audio</button>
<button
type="button"
class="nes-btn {uploadAudioActive ? 'is-disabled' : ''}"
on:click|preventDefault={activateUploadAudio}>Audio</button
>
</section>
</div>
<div class="global-message-content">
{#if inputSendTextActive}
<TextGlobalMessage bind:handleSending={handleSendText}/>
<TextGlobalMessage bind:handleSending={handleSendText} />
{/if}
{#if uploadAudioActive}
<AudioGlobalMessage bind:handleSending={handleSendAudio}/>
<AudioGlobalMessage bind:handleSending={handleSendAudio} />
{/if}
</div>
<div class="global-message-footer">
<label>
<input type="checkbox" class="nes-checkbox is-dark nes-pointer" bind:checked={broadcastToWorld}>
<input type="checkbox" class="nes-checkbox is-dark nes-pointer" bind:checked={broadcastToWorld} />
<span>Broadcast to all rooms of the world</span>
</label>
<section>
@ -57,62 +65,59 @@
</div>
</div>
<style lang="scss">
div.global-message-main {
height: calc(100% - 50px);
display: grid;
grid-template-rows: 15% 65% 20%;
div.global-message-subOptions {
height: calc(100% - 50px);
display: grid;
grid-template-columns: 50% 50%;
margin-bottom: 20px;
grid-template-rows: 15% 65% 20%;
section {
display: flex;
justify-content: center;
align-items: center;
}
}
div.global-message-subOptions {
display: grid;
grid-template-columns: 50% 50%;
margin-bottom: 20px;
div.global-message-footer {
margin-bottom: 10px;
display: grid;
grid-template-rows: 50% 50%;
section {
display: flex;
justify-content: center;
align-items: center;
section {
display: flex;
justify-content: center;
align-items: center;
}
}
label {
margin: 10px;
display: flex;
justify-content: center;
align-items: center;
div.global-message-footer {
margin-bottom: 10px;
span {
font-family: "Press Start 2P";
}
display: grid;
grid-template-rows: 50% 50%;
section {
display: flex;
justify-content: center;
align-items: center;
}
label {
margin: 10px;
display: flex;
justify-content: center;
align-items: center;
span {
font-family: "Press Start 2P";
}
}
}
}
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
.global-message-content {
height: calc(100% - 5px);
}
.global-message-footer {
margin-bottom: 0;
label {
width: calc(100% - 10px);
.global-message-content {
height: calc(100% - 5px);
}
.global-message-footer {
margin-bottom: 0;
label {
width: calc(100% - 10px);
}
}
}
}
</style>

View File

@ -1,18 +1,18 @@
<script lang="ts">
function copyLink() {
const input: HTMLInputElement = document.getElementById('input-share-link') as HTMLInputElement;
const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement;
input.focus();
input.select();
document.execCommand('copy');
document.execCommand("copy");
}
async function shareLink() {
const shareData = {url: location.toString()};
const shareData = { url: location.toString() };
try {
await navigator.share(shareData);
} catch (err) {
console.error('Error: ' + err);
console.error("Error: " + err);
copyLink();
}
}
@ -22,12 +22,12 @@
<section class="container-overflow">
<section class="share-url not-mobile">
<h3>Share the link of the room !</h3>
<input type="text" readonly id="input-share-link" value={location.toString()}>
<input type="text" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={copyLink}>Copy</button>
</section>
<section class="is-mobile">
<h3>Share the link of the room !</h3>
<input type="hidden" readonly id="input-share-link" value={location.toString()}>
<input type="hidden" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={shareLink}>Share</button>
</section>
</section>
@ -35,41 +35,41 @@
<style lang="scss">
div.guest-main {
height: calc(100% - 56px);
height: calc(100% - 56px);
text-align: center;
text-align: center;
section {
margin-bottom: 50px;
}
section.container-overflow {
height: 100%;
margin: 0;
padding: 0;
overflow: auto;
}
section.is-mobile {
display: none;
}
}
@media only screen and (max-width: 900px), only screen and (max-height: 600px) {
div.guest-main {
section.share-url.not-mobile {
display: none;
}
section.is-mobile {
display: block;
text-align: center;
margin-bottom: 20px;
section {
margin-bottom: 50px;
}
section.container-overflow {
height: calc(100% - 120px);
height: 100%;
margin: 0;
padding: 0;
overflow: auto;
}
section.is-mobile {
display: none;
}
}
@media only screen and (max-width: 900px), only screen and (max-height: 600px) {
div.guest-main {
section.share-url.not-mobile {
display: none;
}
section.is-mobile {
display: block;
text-align: center;
margin-bottom: 20px;
}
section.container-overflow {
height: calc(100% - 120px);
}
}
}
}
</style>

View File

@ -1,47 +1,47 @@
<script lang="typescript">
import {fly} from "svelte/transition";
import { fly } from "svelte/transition";
import SettingsSubMenu from "./SettingsSubMenu.svelte";
import ProfileSubMenu from "./ProfileSubMenu.svelte";
import WorldsSubMenu from "./WorldsSubMenu.svelte";
import AboutRoomSubMenu from "./AboutRoomSubMenu.svelte";
import GlobalMessageSubMenu from "./GlobalMessagesSubMenu.svelte";
import ContactSubMenu from "./ContactSubMenu.svelte";
import CustomSubMenu from "./CustomSubMenu.svelte"
import CustomSubMenu from "./CustomSubMenu.svelte";
import GuestSubMenu from "./GuestSubMenu.svelte";
import {
checkSubMenuToShow,
customMenuIframe,
menuVisiblilityStore,
SubMenusInterface,
subMenusStore
subMenusStore,
} from "../../Stores/MenuStore";
import {onDestroy, onMount} from "svelte";
import {get} from "svelte/store";
import type {Unsubscriber} from "svelte/store";
import {sendMenuClickedEvent} from "../../Api/iframe/Ui/MenuItem";
import { onDestroy, onMount } from "svelte";
import { get } from "svelte/store";
import type { Unsubscriber } from "svelte/store";
import { sendMenuClickedEvent } from "../../Api/iframe/Ui/MenuItem";
let activeSubMenu: string = SubMenusInterface.profile;
let activeComponent: typeof ProfileSubMenu | typeof CustomSubMenu = ProfileSubMenu;
let props: { url: string, allowApi: boolean };
let props: { url: string; allowApi: boolean };
let unsubscriberSubMenuStore: Unsubscriber;
onMount(() => {
unsubscriberSubMenuStore = subMenusStore.subscribe(() => {
if(!get(subMenusStore).includes(activeSubMenu)) {
if (!get(subMenusStore).includes(activeSubMenu)) {
switchMenu(SubMenusInterface.profile);
}
})
});
checkSubMenuToShow();
switchMenu(SubMenusInterface.profile);
})
});
onDestroy(() => {
if(unsubscriberSubMenuStore) {
if (unsubscriberSubMenuStore) {
unsubscriberSubMenuStore();
}
})
});
function switchMenu(menu: string) {
if (get(subMenusStore).find((subMenu) => subMenu === menu)) {
@ -68,7 +68,7 @@
case SubMenusInterface.contact:
activeComponent = ContactSubMenu;
break;
default:
default: {
const customMenu = customMenuIframe.get(menu);
if (customMenu !== undefined) {
props = { url: customMenu.url, allowApi: customMenu.allowApi };
@ -78,109 +78,113 @@
menuVisiblilityStore.set(false);
}
break;
}
}
} else throw ("There is no menu called " + menu);
} else throw "There is no menu called " + menu;
}
function closeMenu() {
menuVisiblilityStore.set(false);
}
function onKeyDown(e:KeyboardEvent) {
if (e.key === 'Escape') {
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") {
closeMenu();
}
}
</script>
<svelte:window on:keydown={onKeyDown}/>
<svelte:window on:keydown={onKeyDown} />
<div class="menu-container-main">
<div class="menu-nav-sidebar nes-container is-rounded" transition:fly="{{ x: -1000, duration: 500 }}">
<div class="menu-nav-sidebar nes-container is-rounded" transition:fly={{ x: -1000, duration: 500 }}>
<h2>Menu</h2>
<nav>
{#each $subMenusStore as submenu}
<button type="button" class="nes-btn {activeSubMenu === submenu ? 'is-disabled' : ''}" on:click|preventDefault={() => switchMenu(submenu)}>
<button
type="button"
class="nes-btn {activeSubMenu === submenu ? 'is-disabled' : ''}"
on:click|preventDefault={() => switchMenu(submenu)}
>
{submenu}
</button>
{/each}
</nav>
</div>
<div class="menu-submenu-container nes-container is-rounded" transition:fly="{{ y: -1000, duration: 500 }}">
<div class="menu-submenu-container nes-container is-rounded" transition:fly={{ y: -1000, duration: 500 }}>
<button type="button" class="nes-btn is-error close" on:click={closeMenu}>&times</button>
<h2>{activeSubMenu}</h2>
<svelte:component this={activeComponent} {...props}/>
<svelte:component this={activeComponent} {...props} />
</div>
</div>
<style lang="scss">
.nes-container {
padding: 5px;
}
div.menu-container-main {
--size-first-columns-grid: 200px;
font-family: "Press Start 2P";
pointer-events: auto;
height: 80%;
width: 75%;
top: 10%;
position: relative;
z-index: 80;
margin: auto;
display: grid;
grid-template-columns: var(--size-first-columns-grid) calc(100% - var(--size-first-columns-grid));
grid-template-rows: 100%;
h2 {
text-align: center;
margin-bottom: 20px;
.nes-container {
padding: 5px;
}
div.menu-nav-sidebar {
background-color: #333333;
color: whitesmoke;
nav button {
width: calc(100% - 10px);
margin-bottom: 10px;
}
}
div.menu-submenu-container {
background-color: #333333;
color: whitesmoke;
.nes-btn.is-error.close {
position: absolute;
top: -20px;
right: -20px;
}
}
}
@media only screen and (max-width: 800px) {
div.menu-container-main {
--size-first-columns-grid: 120px;
height: 70%;
top: 55px;
width: 100%;
font-size: 0.5em;
--size-first-columns-grid: 200px;
div.menu-nav-sidebar {
overflow-y: auto;
}
font-family: "Press Start 2P";
pointer-events: auto;
height: 80%;
width: 75%;
top: 10%;
div.menu-submenu-container {
.nes-btn.is-error.close {
position: absolute;
top: -35px;
right: 0;
position: relative;
z-index: 80;
margin: auto;
display: grid;
grid-template-columns: var(--size-first-columns-grid) calc(100% - var(--size-first-columns-grid));
grid-template-rows: 100%;
h2 {
text-align: center;
margin-bottom: 20px;
}
div.menu-nav-sidebar {
background-color: #333333;
color: whitesmoke;
nav button {
width: calc(100% - 10px);
margin-bottom: 10px;
}
}
div.menu-submenu-container {
background-color: #333333;
color: whitesmoke;
.nes-btn.is-error.close {
position: absolute;
top: -20px;
right: -20px;
}
}
}
@media only screen and (max-width: 800px) {
div.menu-container-main {
--size-first-columns-grid: 120px;
height: 70%;
top: 55px;
width: 100%;
font-size: 0.5em;
div.menu-nav-sidebar {
overflow-y: auto;
}
div.menu-submenu-container {
.nes-btn.is-error.close {
position: absolute;
top: -35px;
right: 0;
}
}
}
}
}
}
</style>

View File

@ -1,26 +1,26 @@
<script lang="typescript">
import logoWA from "../images/menu.svg"
import logoTalk from "../images/chat.svg"
import {menuVisiblilityStore} from "../../Stores/MenuStore";
import {chatVisibilityStore} from "../../Stores/ChatStore";
import {get} from "svelte/store";
import { menuVisiblilityStore } from "../../Stores/MenuStore";
import { chatVisibilityStore } from "../../Stores/ChatStore";
import { get } from "svelte/store";
function showMenu(){
menuVisiblilityStore.set(!get(menuVisiblilityStore))
function showMenu() {
menuVisiblilityStore.set(!get(menuVisiblilityStore));
}
function showChat(){
function showChat() {
chatVisibilityStore.set(true);
}
</script>
<svelte:window/>
<svelte:window />
<main class="menuIcon">
<span class=" nes-btn is-dark">
<img src={logoWA} alt="open menu" on:click|preventDefault={showMenu}>
<span class="nes-btn is-dark">
<img src={logoWA} alt="open menu" on:click|preventDefault={showMenu} />
</span>
<span class=" nes-btn is-dark">
<img src={logoTalk} alt="open menu" on:click|preventDefault={showChat}>
<span class="nes-btn is-dark">
<img src={logoTalk} alt="open menu" on:click|preventDefault={showChat} />
</span>
</main>
@ -37,6 +37,7 @@
margin: 3px
}
}
.menuIcon img:hover{
transform: scale(1.2);
}

View File

@ -1,60 +1,59 @@
<script lang="typescript">
import {gameManager} from "../../Phaser/Game/GameManager";
import {SelectCompanionScene, SelectCompanionSceneName} from "../../Phaser/Login/SelectCompanionScene";
import {menuIconVisiblilityStore, menuVisiblilityStore, userIsConnected} from "../../Stores/MenuStore";
import {selectCompanionSceneVisibleStore} from "../../Stores/SelectCompanionStore";
import {LoginScene, LoginSceneName} from "../../Phaser/Login/LoginScene";
import {loginSceneVisibleStore} from "../../Stores/LoginSceneStore";
import {selectCharacterSceneVisibleStore} from "../../Stores/SelectCharacterStore";
import {SelectCharacterScene, SelectCharacterSceneName} from "../../Phaser/Login/SelectCharacterScene";
import {connectionManager} from "../../Connexion/ConnectionManager";
import {PROFILE_URL} from "../../Enum/EnvironmentVariable";
import {localUserStore} from "../../Connexion/LocalUserStore";
import {EnableCameraScene, EnableCameraSceneName} from "../../Phaser/Login/EnableCameraScene";
import {enableCameraSceneVisibilityStore} from "../../Stores/MediaStore";
import { gameManager } from "../../Phaser/Game/GameManager";
import { SelectCompanionScene, SelectCompanionSceneName } from "../../Phaser/Login/SelectCompanionScene";
import { menuIconVisiblilityStore, menuVisiblilityStore, userIsConnected } from "../../Stores/MenuStore";
import { selectCompanionSceneVisibleStore } from "../../Stores/SelectCompanionStore";
import { LoginScene, LoginSceneName } from "../../Phaser/Login/LoginScene";
import { loginSceneVisibleStore } from "../../Stores/LoginSceneStore";
import { selectCharacterSceneVisibleStore } from "../../Stores/SelectCharacterStore";
import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene";
import { connectionManager } from "../../Connexion/ConnectionManager";
import { PROFILE_URL } from "../../Enum/EnvironmentVariable";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { EnableCameraScene, EnableCameraSceneName } from "../../Phaser/Login/EnableCameraScene";
import { enableCameraSceneVisibilityStore } from "../../Stores/MediaStore";
import btnProfileSubMenuCamera from "../images/btn-menu-profile-camera.svg";
import btnProfileSubMenuIdentity from "../images/btn-menu-profile-identity.svg";
import btnProfileSubMenuCompanion from "../images/btn-menu-profile-companion.svg";
import btnProfileSubMenuWoka from "../images/btn-menu-profile-woka.svg";
function disableMenuStores(){
function disableMenuStores() {
menuVisiblilityStore.set(false);
menuIconVisiblilityStore.set(false);
}
function openEditCompanionScene(){
function openEditCompanionScene() {
disableMenuStores();
selectCompanionSceneVisibleStore.set(true);
gameManager.leaveGame(SelectCompanionSceneName,new SelectCompanionScene());
gameManager.leaveGame(SelectCompanionSceneName, new SelectCompanionScene());
}
function openEditNameScene(){
function openEditNameScene() {
disableMenuStores();
loginSceneVisibleStore.set(true);
gameManager.leaveGame(LoginSceneName,new LoginScene());
gameManager.leaveGame(LoginSceneName, new LoginScene());
}
function openEditSkinScene(){
function openEditSkinScene() {
disableMenuStores();
selectCharacterSceneVisibleStore.set(true);
gameManager.leaveGame(SelectCharacterSceneName,new SelectCharacterScene());
gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene());
}
function logOut(){
function logOut() {
disableMenuStores();
loginSceneVisibleStore.set(true);
connectionManager.logout();
}
function getProfileUrl(){
function getProfileUrl() {
return PROFILE_URL + `?token=${localUserStore.getAuthToken()}`;
}
function openEnableCameraScene(){
function openEnableCameraScene() {
disableMenuStores();
enableCameraSceneVisibilityStore.showEnableCameraScene();
gameManager.leaveGame(EnableCameraSceneName,new EnableCameraScene());
gameManager.leaveGame(EnableCameraSceneName, new EnableCameraScene());
}
</script>
@ -63,20 +62,20 @@
<section>
<!--
<button type="button" class="nes-btn" on:click|preventDefault={openEditNameScene}>
<img src={btnProfileSubMenuIdentity} alt="Edit your name">
<img src={btnProfileSubMenuIdentity} alt="Edit your name" />
<span class="btn-hover">Edit your name</span>
</button>
-->
<button type="button" class="nes-btn" on:click|preventDefault={openEditSkinScene}>
<img src={btnProfileSubMenuWoka} alt="Edit your Avatar">
<img src={btnProfileSubMenuWoka} alt="Edit your WOKA" />
<span class="btn-hover">Edit your Avatar</span>
</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}>
<img src={btnProfileSubMenuCompanion} alt="Change your companion">
<img src={btnProfileSubMenuCompanion} alt="Edit your companion" />
<span class="btn-hover">Change your companion</span>
</button>
<button type="button" class="nes-btn" on:click|preventDefault={openEnableCameraScene}>
<img src={btnProfileSubMenuCamera} alt="Edit your camera">
<img src={btnProfileSubMenuCamera} alt="Edit your camera" />
<span class="btn-hover">Edit your camera</span>
</button>
</section>
@ -86,7 +85,7 @@
{#if $userIsConnected}
<section>
{#if PROFILE_URL != undefined}
<iframe title="profile" src="{getProfileUrl()}"></iframe>
<iframe title="profile" src={getProfileUrl()} />
{/if}
</section>
<section>
@ -101,68 +100,68 @@
</div>
<style lang="scss">
div.customize-main{
width: 100%;
display: inline-flex;
div.submenu{
height: 100%;
width: 50px;
button {
transition: all .5s ease;
text-align: left;
white-space: nowrap;
margin-bottom: 10px;
max-height: 44px;
img {
height: 26px;
width: 26px;
cursor: pointer;
}
span.btn-hover{
display: none;
font-family: "Press Start 2P";
}
&:hover{
width: auto;
span.btn-hover {
display: initial;
}
}
}
}
div.content {
div.customize-main {
width: 100%;
section {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
margin-bottom: 20px;
display: inline-flex;
iframe {
width: 100%;
height: 50vh;
border: none;
}
div.submenu {
height: 100%;
width: 50px;
button {
height: 50px;
width: 250px;
}
button {
transition: all 0.5s ease;
text-align: left;
white-space: nowrap;
margin-bottom: 10px;
max-height: 44px;
img {
height: 26px;
width: 26px;
cursor: pointer;
}
span.btn-hover {
display: none;
font-family: "Press Start 2P";
}
&:hover {
width: auto;
span.btn-hover {
display: initial;
}
}
}
}
div.content {
width: 100%;
section {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
margin-bottom: 20px;
iframe {
width: 100%;
height: 50vh;
border: none;
}
button {
height: 50px;
width: 250px;
}
}
}
}
}
@media only screen and (max-width: 800px) {
div.customize-main.content section button {
width: 130px;
}
div.customize-main.content section button {
width: 130px;
}
}
</style>

View File

@ -1,62 +1,62 @@
<script lang="typescript">
import {localUserStore} from "../../Connexion/LocalUserStore";
import {videoConstraintStore} from "../../Stores/MediaStore";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import {isMobile} from "../../Enum/EnvironmentVariable";
import {menuVisiblilityStore} from "../../Stores/MenuStore";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { videoConstraintStore } from "../../Stores/MediaStore";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { isMobile } from "../../Enum/EnvironmentVariable";
import { menuVisiblilityStore } from "../../Stores/MenuStore";
let fullscreen : boolean = localUserStore.getFullscreen();
let notification : boolean = localUserStore.getNotification() === 'granted';
let valueGame : number = localUserStore.getGameQualityValue();
let valueVideo : number = localUserStore.getVideoQualityValue();
let previewValueGame = valueGame;
let previewValueVideo = valueVideo;
let fullscreen: boolean = localUserStore.getFullscreen();
let notification: boolean = localUserStore.getNotification() === "granted";
let valueGame: number = localUserStore.getGameQualityValue();
let valueVideo: number = localUserStore.getVideoQualityValue();
let previewValueGame = valueGame;
let previewValueVideo = valueVideo;
function saveSetting(){
if (valueGame !== previewValueGame) {
previewValueGame = valueGame;
localUserStore.setGameQualityValue(valueGame);
window.location.reload();
}
if (valueVideo !== previewValueVideo) {
previewValueVideo = valueVideo;
videoConstraintStore.setFrameRate(valueVideo);
}
closeMenu();
}
function changeFullscreen() {
const body = HtmlUtils.querySelectorOrFail('body');
if (body) {
if (document.fullscreenElement !== null && !fullscreen) {
document.exitFullscreen()
} else {
body.requestFullscreen();
function saveSetting() {
if (valueGame !== previewValueGame) {
previewValueGame = valueGame;
localUserStore.setGameQualityValue(valueGame);
window.location.reload();
}
localUserStore.setFullscreen(fullscreen);
}
}
function changeNotification() {
if (Notification.permission === 'granted') {
localUserStore.setNotification(notification ? 'granted' : 'denied');
} else {
Notification.requestPermission().then((response) => {
if (response === 'granted') {
localUserStore.setNotification(notification ? 'granted' : 'denied');
if (valueVideo !== previewValueVideo) {
previewValueVideo = valueVideo;
videoConstraintStore.setFrameRate(valueVideo);
}
closeMenu();
}
function changeFullscreen() {
const body = HtmlUtils.querySelectorOrFail("body");
if (body) {
if (document.fullscreenElement !== null && !fullscreen) {
document.exitFullscreen();
} else {
localUserStore.setNotification('denied');
notification = false;
body.requestFullscreen();
}
})
localUserStore.setFullscreen(fullscreen);
}
}
}
function closeMenu() {
menuVisiblilityStore.set(false);
}
function changeNotification() {
if (Notification.permission === "granted") {
localUserStore.setNotification(notification ? "granted" : "denied");
} else {
Notification.requestPermission().then((response) => {
if (response === "granted") {
localUserStore.setNotification(notification ? "granted" : "denied");
} else {
localUserStore.setNotification("denied");
notification = false;
}
});
}
}
function closeMenu() {
menuVisiblilityStore.set(false);
}
</script>
<div class="settings-main" on:submit|preventDefault={saveSetting}>
@ -64,10 +64,12 @@ function closeMenu() {
<h3>Game quality</h3>
<div class="nes-select is-dark">
<select bind:value={valueGame}>
<option value="{120}">{isMobile() ? 'High (120 fps)' : 'High video quality (120 fps)'}</option>
<option value="{60}">{isMobile() ? 'Medium (60 fps)' : 'Medium video quality (60 fps, recommended)'}</option>
<option value="{40}">{isMobile() ? 'Minimum (40 fps)' : 'Minimum video quality (40 fps)'}</option>
<option value="{20}">{isMobile() ? 'Small (20 fps)' : 'Small video quality (20 fps)'}</option>
<option value={120}>{isMobile() ? "High (120 fps)" : "High video quality (120 fps)"}</option>
<option value={60}
>{isMobile() ? "Medium (60 fps)" : "Medium video quality (60 fps, recommended)"}</option
>
<option value={40}>{isMobile() ? "Minimum (40 fps)" : "Minimum video quality (40 fps)"}</option>
<option value={20}>{isMobile() ? "Small (20 fps)" : "Small video quality (20 fps)"}</option>
</select>
</div>
</section>
@ -75,10 +77,12 @@ function closeMenu() {
<h3>Video quality</h3>
<div class="nes-select is-dark">
<select bind:value={valueVideo}>
<option value="{30}">{isMobile() ? 'High (30 fps)' : 'High video quality (30 fps)'}</option>
<option value="{20}">{isMobile() ? 'Medium (20 fps)' : 'Medium video quality (20 fps, recommended)'}</option>
<option value="{10}">{isMobile() ? 'Minimum (10 fps)' : 'Minimum video quality (10 fps)'}</option>
<option value="{5}">{isMobile() ? 'Small (5 fps)' : 'Small video quality (5 fps)'}</option>
<option value={30}>{isMobile() ? "High (30 fps)" : "High video quality (30 fps)"}</option>
<option value={20}
>{isMobile() ? "Medium (20 fps)" : "Medium video quality (20 fps, recommended)"}</option
>
<option value={10}>{isMobile() ? "Minimum (10 fps)" : "Minimum video quality (10 fps)"}</option>
<option value={5}>{isMobile() ? "Small (5 fps)" : "Small video quality (5 fps)"}</option>
</select>
</div>
</section>
@ -88,55 +92,65 @@ function closeMenu() {
</section>
<section class="settings-section-noSaveOption">
<label>
<input type="checkbox" class="nes-checkbox is-dark" bind:checked={fullscreen} on:change={changeFullscreen}/>
<input
type="checkbox"
class="nes-checkbox is-dark"
bind:checked={fullscreen}
on:change={changeFullscreen}
/>
<span>Fullscreen</span>
</label>
<label>
<input type="checkbox" class="nes-checkbox is-dark" bind:checked={notification} on:change={changeNotification}>
<input
type="checkbox"
class="nes-checkbox is-dark"
bind:checked={notification}
on:change={changeNotification}
/>
<span>Notifications</span>
</label>
</section>
</div>
<style lang="scss">
div.settings-main {
height: calc(100% - 40px);
overflow-y: auto;
section {
width: 100%;
padding: 20px 20px 0;
margin-bottom: 20px;
text-align: center;
div.nes-select select:focus {
outline: none;
}
}
section.settings-section-save {
text-align: center;
p {
margin: 16px 0;
}
}
section.settings-section-noSaveOption {
display: flex;
align-items: center;
flex-wrap: wrap;
label {
flex: 1 1 auto;
text-align: center;
margin: 0 0 15px;
}
}
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
div.settings-main {
section {
padding: 0;
}
height: calc(100% - 40px);
overflow-y: auto;
section {
width: 100%;
padding: 20px 20px 0;
margin-bottom: 20px;
text-align: center;
div.nes-select select:focus {
outline: none;
}
}
section.settings-section-save {
text-align: center;
p {
margin: 16px 0;
}
}
section.settings-section-noSaveOption {
display: flex;
align-items: center;
flex-wrap: wrap;
label {
flex: 1 1 auto;
text-align: center;
margin: 0 0 15px;
}
}
}
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
div.settings-main {
section {
padding: 0;
}
}
}
}
</style>

View File

@ -8,25 +8,25 @@
//toolbar
const toolbarOptions = [
['bold', 'italic', 'underline', 'strike'], // toggled buttons
['blockquote', 'code-block'],
["bold", "italic", "underline", "strike"], // toggled buttons
["blockquote", "code-block"],
[{'header': 1}, {'header': 2}], // custom button values
[{'list': 'ordered'}, {'list': 'bullet'}],
[{'script': 'sub'}, {'script': 'super'}], // superscript/subscript
[{'indent': '-1'}, {'indent': '+1'}], // outdent/indent
[{'direction': 'rtl'}], // text direction
[{ header: 1 }, { header: 2 }], // custom button values
[{ list: "ordered" }, { list: "bullet" }],
[{ script: "sub" }, { script: "super" }], // superscript/subscript
[{ indent: "-1" }, { indent: "+1" }], // outdent/indent
[{ direction: "rtl" }], // text direction
[{'size': ['small', false, 'large', 'huge']}], // custom dropdown
[{'header': [1, 2, 3, 4, 5, 6, false]}],
[{ size: ["small", false, "large", "huge"] }], // custom dropdown
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{'color': []}, {'background': []}], // dropdown with defaults from theme
[{'font': []}],
[{'align': []}],
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
[{ font: [] }],
[{ align: [] }],
['clean'], // remove formatting button
["clean"], // remove formatting button
['link', 'image', 'video']
["link", "image", "video"],
];
const gameScene = gameManager.getCurrentGameScene();
@ -44,24 +44,24 @@
const textGlobalMessage: PlayGlobalMessageInterface = {
type: MESSAGE_TYPE,
content: text,
broadcastToWorld: broadcastToWorld
broadcastToWorld: broadcastToWorld,
};
quill.deleteText(0, quill.getLength());
gameScene.connection?.emitGlobalMessage(textGlobalMessage);
}
}
},
};
//Quill
onMount(async () => {
// Import quill
const {default: Quill} = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any
const { default: Quill } = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any
quill = new Quill(QUILL_EDITOR, {
placeholder: 'Enter your message here...',
theme: 'snow',
placeholder: "Enter your message here...",
theme: "snow",
modules: {
toolbar: toolbarOptions
toolbar: toolbarOptions,
},
});
menuInputFocusStore.set(true);
@ -69,9 +69,9 @@
onDestroy(() => {
menuInputFocusStore.set(false);
})
});
</script>
<section class="section-input-send-text">
<div class="input-send-text" bind:this={QUILL_EDITOR}></div>
<div class="input-send-text" bind:this={QUILL_EDITOR} />
</section>

View File

@ -1,14 +1,14 @@
<script lang="typescript">
import {obtainedMediaConstraintStore} from "../Stores/MediaStore";
import {localStreamStore, isSilentStore} from "../Stores/MediaStore";
import { obtainedMediaConstraintStore } from "../Stores/MediaStore";
import { localStreamStore, isSilentStore } from "../Stores/MediaStore";
import SoundMeterWidget from "./SoundMeterWidget.svelte";
import {onDestroy} from "svelte";
import {srcObject} from "./Video/utils";
import { onDestroy } from "svelte";
import { srcObject } from "./Video/utils";
let stream : MediaStream|null;
let stream: MediaStream | null;
const unsubscribe = localStreamStore.subscribe(value => {
if (value.type === 'success') {
const unsubscribe = localStreamStore.subscribe((value) => {
if (value.type === "success") {
stream = value.stream;
} else {
stream = null;
@ -17,26 +17,21 @@
onDestroy(unsubscribe);
let isSilent: boolean;
const unsubscribeIsSilent = isSilentStore.subscribe(value => {
const unsubscribeIsSilent = isSilentStore.subscribe((value) => {
isSilent = value;
});
onDestroy(unsubscribeIsSilent);
</script>
<div>
<div class="video-container nes-container is-rounded is-dark div-myCamVideo"
class:hide={!$obtainedMediaConstraintStore.video || isSilent}>
{#if $localStreamStore.type === "success" && $localStreamStore.stream}
<video class="myCamVideo" use:srcObject={stream} autoplay muted playsinline></video>
<SoundMeterWidget stream={stream}></SoundMeterWidget>
<video class="myCamVideo" use:srcObject={stream} autoplay muted playsinline />
<SoundMeterWidget {stream} />
{/if}
</div>
<div class="is-silent" class:hide={isSilent}>
Silent zone
</div>
<div class="is-silent" class:hide={isSilent}>Silent zone</div>
</div>

View File

@ -1,7 +1,7 @@
<script lang="ts">
import {blackListManager} from "../../WebRtc/BlackListManager";
import {showReportScreenStore, userReportEmpty} from "../../Stores/ShowReportScreenStore";
import {onMount} from "svelte";
import { blackListManager } from "../../WebRtc/BlackListManager";
import { showReportScreenStore, userReportEmpty } from "../../Stores/ShowReportScreenStore";
import { onMount } from "svelte";
export let userUUID: string | undefined;
export let userName: string;
@ -14,7 +14,7 @@
} else {
userIsBlocked = blackListManager.isBlackListed(userUUID);
}
})
});
function blockUser(): void {
if (userUUID === undefined) {
@ -32,13 +32,12 @@
<h3>Block</h3>
<p>Block any communication from and to {userName}. This can be reverted.</p>
<button type="button" class="nes-btn is-error" on:click|preventDefault={blockUser}>
{userIsBlocked ? 'Unblock this user' : 'Block this user'}
{userIsBlocked ? "Unblock this user" : "Block this user"}
</button>
</div>
<style lang="scss">
div.block-container {
text-align: center;
text-align: center;
}
</style>

View File

@ -1,20 +1,20 @@
<script lang="ts">
import {showReportScreenStore, userReportEmpty} from "../../Stores/ShowReportScreenStore";
import { showReportScreenStore, userReportEmpty } from "../../Stores/ShowReportScreenStore";
import BlockSubMenu from "./BlockSubMenu.svelte";
import ReportSubMenu from "./ReportSubMenu.svelte";
import {onDestroy, onMount} from "svelte";
import type {Unsubscriber} from "svelte/store";
import {playersStore} from "../../Stores/PlayersStore";
import {connectionManager} from "../../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../../Url/UrlManager";
import {get} from "svelte/store";
import { onDestroy, onMount } from "svelte";
import type { Unsubscriber } from "svelte/store";
import { playersStore } from "../../Stores/PlayersStore";
import { connectionManager } from "../../Connexion/ConnectionManager";
import { GameConnexionTypes } from "../../Url/UrlManager";
import { get } from "svelte/store";
let blockActive = true;
let blockActive = true;
let reportActive = !blockActive;
let anonymous: boolean = false;
let userUUID: string | undefined = playersStore.getPlayerById(get(showReportScreenStore).userId)?.userUuid;
let userName = "No name";
let unsubscriber: Unsubscriber
let unsubscriber: Unsubscriber;
onMount(() => {
unsubscriber = showReportScreenStore.subscribe((reportScreenStore) => {
@ -25,15 +25,15 @@
console.error("Could not find UUID for user with ID " + reportScreenStore.userId);
}
}
})
});
anonymous = connectionManager.getConnexionType === GameConnexionTypes.anonymous;
})
});
onDestroy(() => {
if (unsubscriber) {
unsubscriber();
}
})
});
function close() {
showReportScreenStore.set(userReportEmpty);
@ -49,14 +49,14 @@
reportActive = true;
}
function onKeyDown(e:KeyboardEvent) {
if (e.key === 'Escape') {
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") {
close();
}
}
</script>
<svelte:window on:keydown={onKeyDown}/>
<svelte:window on:keydown={onKeyDown} />
<div class="report-menu-main nes-container is-rounded">
<section class="report-menu-title">
@ -67,75 +67,83 @@
</section>
<section class="report-menu-action {anonymous ? 'hidden' : ''}">
<section class="justify-center">
<button type="button" class="nes-btn {blockActive ? 'is-disabled' : ''}" on:click|preventDefault={activateBlock}>Block</button>
<button
type="button"
class="nes-btn {blockActive ? 'is-disabled' : ''}"
on:click|preventDefault={activateBlock}>Block</button
>
</section>
<section class="justify-center">
<button type="button" class="nes-btn {reportActive ? 'is-disabled' : ''}" on:click|preventDefault={activateReport}>Report</button>
<button
type="button"
class="nes-btn {reportActive ? 'is-disabled' : ''}"
on:click|preventDefault={activateReport}>Report</button
>
</section>
</section>
<section class="report-menu-content">
{#if blockActive}
<BlockSubMenu userUUID="{userUUID}" userName="{userName}"/>
<BlockSubMenu {userUUID} {userName} />
{:else if reportActive}
<ReportSubMenu userUUID="{userUUID}"/>
{:else }
<ReportSubMenu {userUUID} />
{:else}
<p>ERROR : There is no action selected.</p>
{/if}
</section>
</div>
<style lang="scss">
.nes-container {
padding: 5px;
}
.nes-container {
padding: 5px;
}
section.justify-center {
display: flex;
justify-content: center;
align-items: center;
}
div.report-menu-main {
font-family: "Press Start 2P";
pointer-events: auto;
background-color: #333333;
color: whitesmoke;
position: relative;
height: 70vh;
width: 50vw;
top: 10vh;
margin: auto;
section.report-menu-title {
display: grid;
grid-template-columns: calc(100% - 45px) 40px;
margin-bottom: 20px;
h2 {
section.justify-center {
display: flex;
justify-content: center;
align-items: center;
}
}
section.report-menu-action {
display: grid;
grid-template-columns: 50% 50%;
margin-bottom: 20px;
}
section.report-menu-action.hidden {
display: none;
}
}
@media only screen and (max-width: 800px) {
div.report-menu-main {
top: 21vh;
height: 60vh;
width: 100vw;
font-size: 0.5em;
font-family: "Press Start 2P";
pointer-events: auto;
background-color: #333333;
color: whitesmoke;
position: relative;
height: 70vh;
width: 50vw;
top: 10vh;
margin: auto;
section.report-menu-title {
display: grid;
grid-template-columns: calc(100% - 45px) 40px;
margin-bottom: 20px;
h2 {
display: flex;
justify-content: center;
align-items: center;
}
}
section.report-menu-action {
display: grid;
grid-template-columns: 50% 50%;
margin-bottom: 20px;
}
section.report-menu-action.hidden {
display: none;
}
}
@media only screen and (max-width: 800px) {
div.report-menu-main {
top: 21vh;
height: 60vh;
width: 100vw;
font-size: 0.5em;
}
}
}
</style>

View File

@ -1,22 +1,22 @@
<script lang="ts">
import {showReportScreenStore, userReportEmpty} from "../../Stores/ShowReportScreenStore";
import {gameManager} from "../../Phaser/Game/GameManager";
import { showReportScreenStore, userReportEmpty } from "../../Stores/ShowReportScreenStore";
import { gameManager } from "../../Phaser/Game/GameManager";
export let userUUID: string | undefined;
let reportMessage: string;
let hiddenError = true;
function submitReport() {
if (reportMessage === '') {
if (reportMessage === "") {
hiddenError = true;
} else {
hiddenError = false;
if( userUUID === undefined) {
console.error('User UUID is not valid.');
if (userUUID === undefined) {
console.error("User UUID is not valid.");
return;
}
gameManager.getCurrentGameScene().connection?.emitReportPlayerMessage(userUUID, reportMessage);
showReportScreenStore.set(userReportEmpty)
showReportScreenStore.set(userReportEmpty);
}
}
</script>
@ -28,9 +28,9 @@
<section>
<label>
<span>Your message: </span>
<textarea type="text" class="nes-textarea" bind:value={reportMessage}></textarea>
<textarea type="text" class="nes-textarea" bind:value={reportMessage} />
</label>
<p hidden="{hiddenError}">Report message cannot to be empty.</p>
<p hidden={hiddenError}>Report message cannot to be empty.</p>
</section>
<section>
<button type="submit" class="nes-btn is-error" on:click={submitReport}>Report this user</button>
@ -40,16 +40,16 @@
<style lang="scss">
div.report-container-main {
text-align: center;
text-align: center;
textarea {
height: clamp(100px, 15vh, 300px);
}
textarea {
height: clamp(100px, 15vh, 300px);
}
}
@media only screen and (max-height: 630px) {
div.report-container-main textarea {
height: 50px;
}
div.report-container-main textarea {
height: 50px;
}
}
</style>

View File

@ -1,6 +1,6 @@
<script lang="typescript">
import type {Game} from "../../Phaser/Game/Game";
import {SelectCompanionScene, SelectCompanionSceneName} from "../../Phaser/Login/SelectCompanionScene";
import type { Game } from "../../Phaser/Game/Game";
import { SelectCompanionScene, SelectCompanionSceneName } from "../../Phaser/Login/SelectCompanionScene";
export let game: Game;
@ -26,62 +26,70 @@
<form class="selectCompanionScene">
<section class="text-center">
<h2>Select your companion</h2>
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}> &lt; </button>
<button class="selectCharacterButton selectCharacterButtonRight nes-btn" on:click|preventDefault={selectRight}> &gt; </button>
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}>
&lt;
</button>
<button class="selectCharacterButton selectCharacterButtonRight nes-btn" on:click|preventDefault={selectRight}>
&gt;
</button>
</section>
<section class="action">
<button href="/" class="selectCompanionSceneFormBack nes-btn" on:click|preventDefault={noCompanion}>No companion</button>
<button type="submit" class="selectCompanionSceneFormSubmit nes-btn is-primary" on:click|preventDefault={selectCompanion}>Continue</button>
<button href="/" class="selectCompanionSceneFormBack nes-btn" on:click|preventDefault={noCompanion}
>No companion</button
>
<button
type="submit"
class="selectCompanionSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={selectCompanion}>Continue</button
>
</section>
</form>
<style lang="scss">
form.selectCompanionScene {
font-family: "Press Start 2P";
pointer-events: auto;
color: #ebeeee;
section {
margin: 10px;
&.action {
text-align: center;
margin-top: 55vh;
}
h2 {
form.selectCompanionScene {
font-family: "Press Start 2P";
margin: 1px;
}
pointer-events: auto;
color: #ebeeee;
&.text-center {
text-align: center;
}
section {
margin: 10px;
button.selectCharacterButton {
position: absolute;
top: 33vh;
margin: 0;
}
&.action {
text-align: center;
margin-top: 55vh;
}
h2 {
font-family: "Press Start 2P";
margin: 1px;
}
&.text-center {
text-align: center;
}
button.selectCharacterButton {
position: absolute;
top: 33vh;
margin: 0;
}
}
button.selectCharacterButtonLeft {
left: 33vw;
}
button.selectCharacterButtonRight {
right: 33vw;
}
}
button.selectCharacterButtonLeft {
left: 33vw;
@media only screen and (max-width: 800px) {
form.selectCompanionScene button.selectCharacterButtonLeft {
left: 5vw;
}
form.selectCompanionScene button.selectCharacterButtonRight {
right: 5vw;
}
}
button.selectCharacterButtonRight {
right: 33vw;
}
}
@media only screen and (max-width: 800px) {
form.selectCompanionScene button.selectCharacterButtonLeft{
left: 5vw;
}
form.selectCompanionScene button.selectCharacterButtonRight{
right: 5vw;
}
}
</style>

View File

@ -1,14 +1,15 @@
<script lang="typescript">
import { AudioContext } from 'standardized-audio-context';
import {SoundMeter} from "../Phaser/Components/SoundMeter";
import {onDestroy} from "svelte";
import { AudioContext } from "standardized-audio-context";
import { SoundMeter } from "../Phaser/Components/SoundMeter";
import { onDestroy } from "svelte";
export let stream: MediaStream|null;
export let stream: MediaStream | null;
let volume = 0;
let timeout: ReturnType<typeof setTimeout>;
const soundMeter = new SoundMeter();
let display = false;
let error = false;
$: {
if (stream && stream.getAudioTracks().length > 0) {
@ -17,17 +18,19 @@
if (timeout) {
clearInterval(timeout);
error = false;
}
timeout = setInterval(() => {
try{
try {
volume = soundMeter.getVolume();
//console.log(volume);
}catch(err){
} catch (err) {
if (!error) {
console.error(err);
error = true;
}
}
}, 100);
} else {
display = false;
}
@ -38,14 +41,13 @@
if (timeout) {
clearInterval(timeout);
}
})
});
</script>
<div class="sound-progress" class:active={display}>
<span class:active={volume > 5}></span>
<span class:active={volume > 10}></span>
<span class:active={volume > 15}></span>
<span class:active={volume > 40}></span>
<span class:active={volume > 70}></span>
<span class:active={volume > 5} />
<span class:active={volume > 10} />
<span class:active={volume > 15} />
<span class:active={volume > 40} />
<span class:active={volume > 70} />
</div>

View File

@ -1,22 +1,22 @@
<script lang="ts">
import { fly } from "svelte/transition";
import {banMessageVisibleStore, banMessageContentStore} from "../../Stores/TypeMessageStore/BanMessageStore";
import {onMount} from "svelte";
import { banMessageVisibleStore, banMessageContentStore } from "../../Stores/TypeMessageStore/BanMessageStore";
import { onMount } from "svelte";
const text = $banMessageContentStore;
const NAME_BUTTON = 'Ok';
const NAME_BUTTON = "Ok";
let nbSeconds = 10;
let nameButton = '';
let nameButton = "";
onMount(() => {
timeToRead()
})
timeToRead();
});
function timeToRead() {
nbSeconds -= 1;
nameButton = nbSeconds.toString();
if ( nbSeconds > 0 ) {
setTimeout( () => {
if (nbSeconds > 0) {
setTimeout(() => {
timeToRead();
}, 1000);
} else {
@ -29,68 +29,76 @@
}
</script>
<div class="main-ban-message nes-container is-rounded" transition:fly="{{ y: -1000, duration: 500 }}">
<h2 class="title-ban-message"><img src="resources/logos/report.svg" alt="***"/> Important message <img src="resources/logos/report.svg" alt="***"/></h2>
<div class="main-ban-message nes-container is-rounded" transition:fly={{ y: -1000, duration: 500 }}>
<h2 class="title-ban-message">
<img src="resources/logos/report.svg" alt="***" /> Important message
<img src="resources/logos/report.svg" alt="***" />
</h2>
<div class="content-ban-message">
<p>{text}</p>
</div>
<div class="footer-ban-message">
<button type="button" class="nes-btn {nameButton === NAME_BUTTON ? 'is-primary' : 'is-error'}" disabled="{!(nameButton === NAME_BUTTON)}" on:click|preventDefault={closeBanMessage}>{nameButton}</button>
<button
type="button"
class="nes-btn {nameButton === NAME_BUTTON ? 'is-primary' : 'is-error'}"
disabled={!(nameButton === NAME_BUTTON)}
on:click|preventDefault={closeBanMessage}>{nameButton}</button
>
</div>
<!-- svelte-ignore a11y-media-has-caption -->
<audio id="report-message" autoplay>
<source src="/resources/objects/report-message.mp3" type="audio/mp3">
<source src="/resources/objects/report-message.mp3" type="audio/mp3" />
</audio>
</div>
<style lang="scss">
div.main-ban-message {
display: flex;
flex-direction: column;
position: relative;
top: 15vh;
div.main-ban-message {
display: flex;
flex-direction: column;
position: relative;
top: 15vh;
height: 70vh;
width: 60vw;
margin-left: auto;
margin-right: auto;
padding-bottom: 0;
height: 70vh;
width: 60vw;
margin-left: auto;
margin-right: auto;
padding-bottom: 0;
pointer-events: auto;
background-color: #333333;
color: whitesmoke;
pointer-events: auto;
background-color: #333333;
color: whitesmoke;
h2.title-ban-message {
flex: 1 1 auto;
max-height: 50px;
margin-bottom: 20px;
h2.title-ban-message {
flex: 1 1 auto;
max-height: 50px;
margin-bottom: 20px;
text-align: center;
text-align: center;
img {
height: 50px;
}
img {
height: 50px;
}
}
div.content-ban-message {
flex: 1 1 auto;
max-height: calc(100% - 50px);
overflow: auto;
p {
white-space: pre-wrap;
}
}
div.footer-ban-message {
height: 50px;
margin-top: 10px;
text-align: center;
button {
width: 88px;
height: 44px;
}
}
}
div.content-ban-message {
flex: 1 1 auto;
max-height: calc(100% - 50px);
overflow: auto;
p {
white-space: pre-wrap;
}
}
div.footer-ban-message {
height: 50px;
margin-top: 10px;
text-align: center;
button {
width: 88px;
height: 44px;
}
}
}
</style>

View File

@ -1,59 +1,61 @@
<script lang="ts">
import { fly } from "svelte/transition";
import {textMessageContentStore, textMessageVisibleStore} from "../../Stores/TypeMessageStore/TextMessageStore";
import { textMessageContentStore, textMessageVisibleStore } from "../../Stores/TypeMessageStore/TextMessageStore";
import { QuillDeltaToHtmlConverter } from "quill-delta-to-html";
const content = JSON.parse($textMessageContentStore);
const converter = new QuillDeltaToHtmlConverter(content.ops, {inlineStyles: true});
const NAME_BUTTON = 'Ok';
const converter = new QuillDeltaToHtmlConverter(content.ops, { inlineStyles: true });
const NAME_BUTTON = "Ok";
function closeTextMessage() {
textMessageVisibleStore.set(false);
}
function onKeyDown(e:KeyboardEvent) {
if (e.key === 'Escape') {
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") {
closeTextMessage();
}
}
</script>
<svelte:window on:keydown={onKeyDown}/>
<svelte:window on:keydown={onKeyDown} />
<div class="main-text-message nes-container is-rounded" transition:fly="{{ x: -1000, duration: 500 }}">
<div class="main-text-message nes-container is-rounded" transition:fly={{ x: -1000, duration: 500 }}>
<div class="content-text-message">
{@html converter.convert()}
</div>
<div class="footer-text-message">
<button type="button" class="nes-btn is-primary" on:click|preventDefault={closeTextMessage}>{NAME_BUTTON}</button>
<button type="button" class="nes-btn is-primary" on:click|preventDefault={closeTextMessage}
>{NAME_BUTTON}</button
>
</div>
</div>
<style lang="scss">
div.main-text-message {
display: flex;
flex-direction: column;
div.main-text-message {
display: flex;
flex-direction: column;
max-height: 25vh;
width: 80vw;
margin-right: auto;
margin-left: auto;
padding-bottom: 0;
max-height: 25vh;
width: 80vw;
margin-right: auto;
margin-left: auto;
padding-bottom: 0;
pointer-events: auto;
background-color: #333333;
pointer-events: auto;
background-color: #333333;
div.content-text-message {
flex: 1 1 auto;
max-height: calc(100% - 50px);
color: whitesmoke;
div.content-text-message {
flex: 1 1 auto;
max-height: calc(100% - 50px);
color: whitesmoke;
overflow: auto;
}
overflow: auto;
}
div.footer-text-message {
height: 50px;
text-align: center;
}
}
div.footer-text-message {
height: 50px;
text-align: center;
}
}
</style>

View File

@ -1,8 +1,8 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { fly } from "svelte/transition";
import megaphoneImg from "./images/megaphone.svg";
import {soundPlayingStore} from "../../Stores/SoundPlayingStore";
import {afterUpdate} from "svelte";
import { soundPlayingStore } from "../../Stores/SoundPlayingStore";
import { afterUpdate } from "svelte";
export let url: string;
let audio: HTMLAudioElement;
@ -16,37 +16,37 @@
});
</script>
<div class="audio-playing" transition:fly="{{ x: 210, duration: 500 }}">
<div class="audio-playing" transition:fly={{ x: 210, duration: 500 }}>
<img src={megaphoneImg} alt="Audio playing" />
<p>Audio message</p>
<audio bind:this={audio} src={url} on:ended={soundEnded} >
<track kind="captions">
<audio bind:this={audio} src={url} on:ended={soundEnded}>
<track kind="captions" />
</audio>
</div>
<style lang="scss">
/*audio html when audio message playing*/
.audio-playing {
position: absolute;
width: 200px;
height: 54px;
right: 0;
top: 40px;
transition: all 0.1s ease-out;
background-color: black;
border-radius: 30px 0 0 30px;
display: inline-flex;
/*audio html when audio message playing*/
.audio-playing {
position: absolute;
width: 200px;
height: 54px;
right: 0;
top: 40px;
transition: all 0.1s ease-out;
background-color: black;
border-radius: 30px 0 0 30px;
display: inline-flex;
img {
border-radius: 50%;
background-color: #ffda01;
padding: 10px;
}
img {
border-radius: 50%;
background-color: #ffda01;
padding: 10px;
}
p {
color: white;
margin-left: 10px;
margin-top: 14px;
p {
color: white;
margin-left: 10px;
margin-top: 14px;
}
}
}
</style>

View File

@ -1,48 +1,49 @@
<script lang="ts">
import {errorStore} from "../../Stores/ErrorStore";
function close(): boolean {
errorStore.clearMessages();
return false;
}
import { errorStore, hasClosableMessagesInErrorStore } from "../../Stores/ErrorStore";
function close(): boolean {
errorStore.clearClosableMessages();
return false;
}
</script>
<div class="error-div nes-container is-dark is-rounded" open>
<p class="nes-text is-error title">Error</p>
<div class="body">
{#each $errorStore as error}
<p>{error}</p>
{/each}
</div>
<div class="button-bar">
<button class="nes-btn is-error" on:click={close}>Close</button>
{#each $errorStore as error}
<p>{error.message}</p>
{/each}
</div>
{#if $hasClosableMessagesInErrorStore}
<div class="button-bar">
<button class="nes-btn is-error" on:click={close}>Close</button>
</div>
{/if}
</div>
<style lang="scss">
div.error-div {
pointer-events: auto;
margin-top: 10vh;
margin-right: auto;
margin-left: auto;
width: max-content;
max-width: 80vw;
pointer-events: auto;
margin-top: 10vh;
margin-right: auto;
margin-left: auto;
width: max-content;
max-width: 80vw;
.button-bar {
text-align: center;
}
.body {
max-height: 50vh;
}
p {
font-family: "Press Start 2P";
&.title {
text-align: center;
.button-bar {
text-align: center;
}
.body {
max-height: 50vh;
}
p {
font-family: "Press Start 2P";
&.title {
text-align: center;
}
}
}
}
</style>

View File

@ -1,21 +1,21 @@
<script lang="ts">
import {streamableCollectionStore} from "../../Stores/StreamableCollectionStore";
import {afterUpdate, onDestroy} from "svelte";
import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore";
import { streamableCollectionStore } from "../../Stores/StreamableCollectionStore";
import { afterUpdate, onDestroy } from "svelte";
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
import MediaBox from "./MediaBox.svelte";
let cssClass = 'one-col';
let cssClass = "one-col";
const unsubscribe = streamableCollectionStore.subscribe((displayableMedias) => {
const nbUsers = displayableMedias.size;
if (nbUsers <= 1) {
cssClass = 'one-col';
cssClass = "one-col";
} else if (nbUsers <= 4) {
cssClass = 'two-col';
cssClass = "two-col";
} else if (nbUsers <= 9) {
cssClass = 'three-col';
cssClass = "three-col";
} else {
cssClass = 'four-col';
cssClass = "four-col";
}
});
@ -25,11 +25,11 @@
afterUpdate(() => {
biggestAvailableAreaStore.recompute();
})
});
</script>
<div class="chat-mode {cssClass}">
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
<MediaBox streamable={peer}></MediaBox>
<MediaBox streamable={peer} />
{/each}
</div>

View File

@ -1,16 +1,15 @@
<script lang="typescript">
import type {ScreenSharingLocalMedia} from "../../Stores/ScreenSharingStore";
import {videoFocusStore} from "../../Stores/VideoFocusStore";
import {srcObject} from "./utils";
import type { ScreenSharingLocalMedia } from "../../Stores/ScreenSharingStore";
import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { srcObject } from "./utils";
export let peer : ScreenSharingLocalMedia;
export let peer: ScreenSharingLocalMedia;
let stream = peer.stream;
export let cssClass : string|undefined;
export let cssClass: string | undefined;
</script>
<div class="video-container {cssClass ? cssClass : ''}" class:hide={!stream}>
{#if stream}
<video use:srcObject={stream} autoplay muted playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
<video use:srcObject={stream} autoplay muted playsinline on:click={() => videoFocusStore.toggleFocus(peer)} />
{/if}
</div>

View File

@ -1,20 +1,20 @@
<script lang="ts">
import {VideoPeer} from "../../WebRtc/VideoPeer";
import { VideoPeer } from "../../WebRtc/VideoPeer";
import VideoMediaBox from "./VideoMediaBox.svelte";
import ScreenSharingMediaBox from "./ScreenSharingMediaBox.svelte";
import {ScreenSharingPeer} from "../../WebRtc/ScreenSharingPeer";
import { ScreenSharingPeer } from "../../WebRtc/ScreenSharingPeer";
import LocalStreamMediaBox from "./LocalStreamMediaBox.svelte";
import type {Streamable} from "../../Stores/StreamableCollectionStore";
import type { Streamable } from "../../Stores/StreamableCollectionStore";
export let streamable: Streamable;
</script>
<div class="media-container">
{#if streamable instanceof VideoPeer}
<VideoMediaBox peer={streamable}/>
<VideoMediaBox peer={streamable} />
{:else if streamable instanceof ScreenSharingPeer}
<ScreenSharingMediaBox peer={streamable}/>
<ScreenSharingMediaBox peer={streamable} />
{:else}
<LocalStreamMediaBox peer={streamable} cssClass=""/>
<LocalStreamMediaBox peer={streamable} cssClass="" />
{/if}
</div>

View File

@ -1,26 +1,26 @@
<script lang="ts">
import {streamableCollectionStore} from "../../Stores/StreamableCollectionStore";
import {videoFocusStore} from "../../Stores/VideoFocusStore";
import {afterUpdate} from "svelte";
import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore";
import { streamableCollectionStore } from "../../Stores/StreamableCollectionStore";
import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { afterUpdate } from "svelte";
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
import MediaBox from "./MediaBox.svelte";
afterUpdate(() => {
biggestAvailableAreaStore.recompute();
})
});
</script>
<div class="main-section">
{#if $videoFocusStore }
{#if $videoFocusStore}
{#key $videoFocusStore.uniqueId}
<MediaBox streamable={$videoFocusStore}></MediaBox>
<MediaBox streamable={$videoFocusStore} />
{/key}
{/if}
</div>
<aside class="sidebar">
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
{#if peer !== $videoFocusStore }
<MediaBox streamable={peer}></MediaBox>
{#if peer !== $videoFocusStore}
<MediaBox streamable={peer} />
{/if}
{/each}
</aside>

View File

@ -1,33 +1,33 @@
<script lang="ts">
import type {ScreenSharingPeer} from "../../WebRtc/ScreenSharingPeer";
import {videoFocusStore} from "../../Stores/VideoFocusStore";
import {getColorByString, srcObject} from "./utils";
import type { ScreenSharingPeer } from "../../WebRtc/ScreenSharingPeer";
import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { getColorByString, srcObject } from "./utils";
export let peer: ScreenSharingPeer;
let streamStore = peer.streamStore;
let name = peer.userName;
let statusStore = peer.statusStore;
</script>
<div class="video-container">
{#if $statusStore === 'connecting'}
<div class="connecting-spinner"></div>
{#if $statusStore === "connecting"}
<div class="connecting-spinner" />
{/if}
{#if $statusStore === 'error'}
<div class="rtc-error"></div>
{#if $statusStore === "error"}
<div class="rtc-error" />
{/if}
{#if $streamStore === null}
<i style="background-color: {getColorByString(name)};">{name}</i>
{:else}
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
<!-- svelte-ignore a11y-media-has-caption -->
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)} />
{/if}
</div>
<style lang="scss">
.video-container {
video {
width: 100%;
.video-container {
video {
width: 100%;
}
}
}
</style>

View File

@ -1,12 +1,12 @@
<script lang="ts">
import type {VideoPeer} from "../../WebRtc/VideoPeer";
import type { VideoPeer } from "../../WebRtc/VideoPeer";
import SoundMeterWidget from "../SoundMeterWidget.svelte";
import microphoneCloseImg from "../images/microphone-close.svg";
import reportImg from "./images/report.svg";
import blockSignImg from "./images/blockSign.svg";
import {videoFocusStore} from "../../Stores/VideoFocusStore";
import {showReportScreenStore} from "../../Stores/ShowReportScreenStore";
import {getColorByString, srcObject} from "./utils";
import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { showReportScreenStore } from "../../Stores/ShowReportScreenStore";
import { getColorByString, srcObject } from "./utils";
export let peer: VideoPeer;
let streamStore = peer.streamStore;
@ -15,32 +15,31 @@
let constraintStore = peer.constraintsStore;
function openReport(peer: VideoPeer): void {
showReportScreenStore.set({ userId:peer.userId, userName: peer.userName });
showReportScreenStore.set({ userId: peer.userId, userName: peer.userName });
}
</script>
<div class="video-container nes-container is-rounded is-dark">
{#if $statusStore === 'connecting'}
<div class="connecting-spinner"></div>
{#if $statusStore === "connecting"}
<div class="connecting-spinner" />
{/if}
{#if $statusStore === 'error'}
<div class="rtc-error"></div>
{#if $statusStore === "error"}
<div class="rtc-error" />
{/if}
{#if !$constraintStore || $constraintStore.video === false}
<i style="background-color: {getColorByString(name)};">{name}</i>
{/if}
{#if $constraintStore && $constraintStore.audio === false}
<img src={microphoneCloseImg} class="active" alt="Muted">
<img src={microphoneCloseImg} class="active" alt="Muted" />
{/if}
<button class="report nes-button is-dark" on:click={() => openReport(peer)}>
<img alt="Report this user" src={reportImg}>
<img alt="Report this user" src={reportImg} />
<span>Report/Block</span>
</button>
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
<!-- svelte-ignore a11y-media-has-caption -->
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)} />
<img src={blockSignImg} class="block-logo" alt="Block" />
{#if $constraintStore && $constraintStore.audio !== false}
<SoundMeterWidget stream={$streamStore}></SoundMeterWidget>
<SoundMeterWidget stream={$streamStore} />
{/if}
</div>

View File

@ -1,23 +1,22 @@
<script lang="ts">
import {LayoutMode} from "../../WebRtc/LayoutManager";
import {layoutModeStore} from "../../Stores/StreamableCollectionStore";
import { LayoutMode } from "../../WebRtc/LayoutManager";
import { layoutModeStore } from "../../Stores/StreamableCollectionStore";
import PresentationLayout from "./PresentationLayout.svelte";
import ChatLayout from "./ChatLayout.svelte";
</script>
<div class="video-overlay">
{#if $layoutModeStore === LayoutMode.Presentation }
{#if $layoutModeStore === LayoutMode.Presentation}
<PresentationLayout />
{:else }
{:else}
<ChatLayout />
{/if}
</div>
<style lang="scss">
.video-overlay {
display: flex;
width: 100%;
height: 100%;
display: flex;
width: 100%;
height: 100%;
}
</style>

View File

@ -1,11 +1,11 @@
<script lang="typescript">
import { fly } from 'svelte/transition';
import {requestVisitCardsStore} from "../../Stores/GameStore";
import {onMount} from "svelte";
import { fly } from "svelte/transition";
import { requestVisitCardsStore } from "../../Stores/GameStore";
import { onMount } from "svelte";
export let visitCardUrl: string;
let w = '500px';
let h = '250px';
let w = "500px";
let h = "250px";
let hidden = true;
let cvIframe: HTMLIFrameElement;
@ -13,73 +13,81 @@
requestVisitCardsStore.set(null);
}
function handleIframeMessage(message:any) {
if (message.data.type === 'cvIframeSize') {
w = (message.data.data.w) + 'px';
h = (message.data.data.h) + 'px';
function handleIframeMessage(message: MessageEvent) {
if (message.data.type === "cvIframeSize") {
w = message.data.data.w + "px";
h = message.data.data.h + "px";
}
}
onMount(() => {
cvIframe.onload = () => hidden = false
cvIframe.onerror = () => hidden = false
})
cvIframe.onload = () => (hidden = false);
cvIframe.onerror = () => (hidden = false);
});
</script>
<style lang="scss">
.loader {
border: 16px solid #f3f3f3; /* Light grey */
border-top: 16px solid #3498db; /* Blue */
border-radius: 50%;
width: 120px;
height: 120px;
margin:auto;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.visitCard {
pointer-events: all;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
margin-top: 200px;
max-width: 80vw;
iframe {
border: 0;
max-width: 80vw;
overflow: hidden;
&.hidden {
visibility: hidden;
position: absolute;
}
}
button {
float: right;
}
}
</style>
<section class="visitCard" transition:fly="{{ y: -200, duration: 1000 }}" style="width: {w}">
<section class="visitCard" transition:fly={{ y: -200, duration: 1000 }} style="width: {w}">
{#if hidden}
<div class="loader"></div>
<div class="loader" />
{/if}
<iframe title="visitCard" src={visitCardUrl} allow="clipboard-read; clipboard-write self {visitCardUrl}" style="width: {w}; height: {h}" class:hidden={hidden} bind:this={cvIframe}></iframe>
<iframe
title="visitCard"
src={visitCardUrl}
allow="clipboard-read; clipboard-write self {visitCardUrl}"
style="width: {w}; height: {h}"
class:hidden
bind:this={cvIframe}
/>
{#if !hidden}
<div class="buttonContainer">
<button class="nes-btn is-popUpElement" on:click={closeCard}>Close</button>
</div>
{/if}
</section>
<svelte:window on:message={handleIframeMessage}/>
<svelte:window on:message={handleIframeMessage} />
<style lang="scss">
.loader {
border: 16px solid #f3f3f3; /* Light grey */
border-top: 16px solid #3498db; /* Blue */
border-radius: 50%;
width: 120px;
height: 120px;
margin: auto;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.visitCard {
pointer-events: all;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
margin-top: 200px;
max-width: 80vw;
iframe {
border: 0;
max-width: 80vw;
overflow: hidden;
&.hidden {
visibility: hidden;
position: absolute;
}
}
button {
float: right;
}
}
</style>

View File

@ -1,37 +1,39 @@
<script lang="typescript">
import { fly } from 'svelte/transition';
import {userIsAdminStore} from "../../Stores/GameStore";
import {ADMIN_URL} from "../../Enum/EnvironmentVariable";
const upgradeLink = ADMIN_URL+'/pricing';
import { fly } from "svelte/transition";
import { userIsAdminStore } from "../../Stores/GameStore";
import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
const upgradeLink = ADMIN_URL + "/pricing";
</script>
<main class="warningMain" transition:fly="{{ y: -200, duration: 500 }}">
<main class="warningMain" transition:fly={{ y: -200, duration: 500 }}>
<h2>Warning!</h2>
{#if $userIsAdminStore}
<p>This world is close to its limit!. You can upgrade its capacity <a href="{upgradeLink}" target="_blank">here</a></p>
<p>
This world is close to its limit!. You can upgrade its capacity <a href={upgradeLink} target="_blank"
>here</a
>
</p>
{:else}
<p>This world is close to its limit!</p>
{/if}
</main>
<style lang="scss">
main.warningMain {
pointer-events: auto;
width: 100vw;
background-color: red;
text-align: center;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
font-family: Lato;
min-width: 300px;
opacity: 0.9;
z-index: 2;
main.warningMain {
pointer-events: auto;
width: 100vw;
background-color: red;
text-align: center;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
font-family: Lato;
min-width: 300px;
opacity: 0.9;
z-index: 2;
h2 {
padding: 5px;
padding: 5px;
}
}
</style>

View File

@ -1,6 +1,6 @@
<script lang="typescript">
import type { Game } from "../../Phaser/Game/Game";
import {SelectCharacterScene, SelectCharacterSceneName} from "../../Phaser/Login/SelectCharacterScene";
import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene";
export let game: Game;
@ -21,72 +21,81 @@
function customizeScene() {
selectCharacterScene.nextSceneToCustomizeScene();
}
</script>
<form class="selectCharacterScene">
<section class="text-center">
<h2>Select your Avatar</h2>
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={ selectLeft }> &lt; </button>
<button class="selectCharacterButton selectCharacterButtonRight nes-btn" on:click|preventDefault={ selectRight }> &gt; </button>
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}>
&lt;
</button>
<button class="selectCharacterButton selectCharacterButtonRight nes-btn" on:click|preventDefault={selectRight}>
&gt;
</button>
</section>
<section class="action">
<button type="submit" class="selectCharacterSceneFormSubmit nes-btn is-primary" on:click|preventDefault={ cameraScene }>Continue</button>
<button type="submit" class="selectCharacterSceneFormCustomYourOwnSubmit nes-btn" on:click|preventDefault={ customizeScene }>Customize your Avatar</button>
<button
type="submit"
class="selectCharacterSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={cameraScene}>Continue</button
>
<button
type="submit"
class="selectCharacterSceneFormCustomYourOwnSubmit nes-btn"
on:click|preventDefault={customizeScene}>Customize your Avatar</button
>
</section>
</form>
<style lang="scss">
form.selectCharacterScene {
font-family: "Press Start 2P";
pointer-events: auto;
color: #ebeeee;
section {
margin: 10px;
&.action {
text-align: center;
margin-top: 55vh;
}
h2 {
form.selectCharacterScene {
font-family: "Press Start 2P";
margin: 1px;
}
pointer-events: auto;
color: #ebeeee;
&.text-center {
text-align: center;
}
section {
margin: 10px;
button.selectCharacterButton {
position: absolute;
top: 33vh;
margin: 0;
}
&.action {
text-align: center;
margin-top: 55vh;
}
h2 {
font-family: "Press Start 2P";
margin: 1px;
}
&.text-center {
text-align: center;
}
button.selectCharacterButton {
position: absolute;
top: 33vh;
margin: 0;
}
}
button {
font-family: "Press Start 2P";
&.selectCharacterButtonLeft {
left: 33vw;
}
&.selectCharacterButtonRight {
right: 33vw;
}
}
}
button {
font-family: "Press Start 2P";
&.selectCharacterButtonLeft {
left: 33vw;
}
&.selectCharacterButtonRight {
right: 33vw;
}
@media only screen and (max-width: 800px) {
form.selectCharacterScene button.selectCharacterButtonLeft {
left: 5vw;
}
form.selectCharacterScene button.selectCharacterButtonRight {
right: 5vw;
}
}
}
@media only screen and (max-width: 800px) {
form.selectCharacterScene button.selectCharacterButtonLeft{
left: 5vw;
}
form.selectCharacterScene button.selectCharacterButtonRight{
right: 5vw;
}
}
</style>

View File

@ -0,0 +1,37 @@
import axios from "axios";
import * as rax from "retry-axios";
import { errorStore } from "../Stores/ErrorStore";
/**
* This instance of Axios will retry in case of an issue and display an error message as a HTML overlay.
*/
export const axiosWithRetry = axios.create();
axiosWithRetry.defaults.raxConfig = {
instance: axiosWithRetry,
retry: Infinity,
noResponseRetries: Infinity,
maxRetryAfter: 60_000,
// You can detect when a retry is happening, and figure out how many
// retry attempts have been made
onRetryAttempt: (err) => {
const cfg = rax.getConfig(err);
console.log(err);
console.log(cfg);
console.log(`Retry attempt #${cfg?.currentRetryAttempt} on URL '${err.config.url}'`);
errorStore.addErrorMessage("Unable to connect to WorkAdventure. Are you connected to internet?", {
closable: false,
id: "axios_retry",
});
},
};
axiosWithRetry.interceptors.response.use((res) => {
if (res.status < 400) {
errorStore.clearMessageById("axios_retry");
}
return res;
});
const interceptorId = rax.attach(axiosWithRetry);

View File

@ -11,6 +11,7 @@ import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore";
import { userIsConnected } from "../Stores/MenuStore";
import { analyticsClient } from "../Administration/AnalyticsClient";
import { gameManager } from "../Phaser/Game/GameManager";
import { axiosWithRetry } from "./AxiosUtils";
class ConnectionManager {
private localUser!: LocalUser;
@ -233,7 +234,7 @@ class ConnectionManager {
}
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
const data = await axiosWithRetry.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
this.localUser = new LocalUser(data.userUuid, [], data.email);
this.authToken = data.authToken;
if (!isBenchmark) {

View File

@ -1,7 +1,10 @@
import * as rax from "retry-axios";
import Axios from "axios";
import { CONTACT_URL, PUSHER_URL, DISABLE_ANONYMOUS, OPID_LOGIN_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable";
import type { CharacterTexture } from "./LocalUser";
import { localUserStore } from "./LocalUserStore";
import axios from "axios";
import { axiosWithRetry } from "./AxiosUtils";
export class MapDetail {
constructor(public readonly mapUrl: string, public readonly textures: CharacterTexture[] | undefined) {}
@ -90,7 +93,7 @@ export class Room {
private async getMapDetail(): Promise<MapDetail | RoomRedirect> {
try {
const result = await Axios.get(`${PUSHER_URL}/map`, {
const result = await axiosWithRetry.get(`${PUSHER_URL}/map`, {
params: {
playUri: this.roomUrl.toString(),
authToken: localUserStore.getAuthToken(),

View File

@ -255,6 +255,9 @@ export class RoomConnection implements RoomConnection {
warningContainerStore.activateWarningContainer();
} else if (message.hasRefreshroommessage()) {
//todo: implement a way to notify the user the room was refreshed.
} else if (message.hasErrormessage()) {
const errorMessage = message.getErrormessage() as ErrorMessage;
console.error("An error occurred server side: " + errorMessage.getMessage());
} else {
throw new Error("Unknown message received");
}

View File

@ -271,8 +271,10 @@ export class GameScene extends DirtyScene {
// 127.0.0.1, localhost and *.localhost are considered secure, even on HTTP.
// So if we are in https, we can still try to load a HTTP local resource (can be useful for testing purposes)
// See https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure
const url = new URL(file.src);
const host = url.host.split(":")[ 0 ];
const base = new URL(window.location.href);
base.pathname = "";
const url = new URL(file.src, base.toString());
const host = url.host.split(":")[0];
if (
window.location.protocol === "https:" &&
file.src === this.MapUrlFile &&
@ -324,12 +326,11 @@ export class GameScene extends DirtyScene {
this.onMapLoad(data);
}
this.load.bitmapFont("main_font", "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
//eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.load as any).rexWebFont({
custom: {
families: [ "Press Start 2P" ],
urls: [ "/resources/fonts/fonts.css" ],
families: ["Press Start 2P"],
urls: ["/resources/fonts/fonts.css"],
testString: "abcdefg",
},
});
@ -375,7 +376,7 @@ export class GameScene extends DirtyScene {
}
}
for (const [ itemType, objectsOfType ] of this.objectsByType) {
for (const [itemType, objectsOfType] of this.objectsByType) {
// FIXME: we would ideally need for the loader to WAIT for the import to be performed, which means writing our own loader plugin.
let itemFactory: ItemFactoryInterface;
@ -406,7 +407,7 @@ export class GameScene extends DirtyScene {
// TODO: we should pass here a factory to create sprites (maybe?)
// Do we have a state for this object?
const state = roomJoinedAnswer.items[ object.id ];
const state = roomJoinedAnswer.items[object.id];
const actionableItem = itemFactory.factory(this, object, state);
this.actionableItems.set(actionableItem.getId(), actionableItem);
@ -636,7 +637,7 @@ export class GameScene extends DirtyScene {
}
});
Promise.all([ this.connectionAnswerPromise as Promise<unknown>, ...scriptPromises ]).then(() => {
Promise.all([this.connectionAnswerPromise as Promise<unknown>, ...scriptPromises]).then(() => {
this.scene.wake();
});
}
@ -724,8 +725,8 @@ export class GameScene extends DirtyScene {
if (item === undefined) {
console.warn(
'Received an event about object "' +
message.itemId +
'" but cannot find this item on the map.'
message.itemId +
'" but cannot find this item on the map.'
);
return;
}
@ -933,8 +934,8 @@ export class GameScene extends DirtyScene {
} else {
console.error(
"Error while opening a popup. Cannot find an object on the map with name '" +
openPopupEvent.targetObject +
"'. The first parameter of WA.openPopup() must be the name of a rectangle object in your map."
openPopupEvent.targetObject +
"'. The first parameter of WA.openPopup() must be the name of a rectangle object in your map."
);
return;
}
@ -1198,7 +1199,7 @@ export class GameScene extends DirtyScene {
const jsonTilesetDir = eventTileset.url.substr(0, eventTileset.url.lastIndexOf("/"));
//Initialise the firstgid to 1 because if there is no tileset in the tilemap, the firstgid will be 1
let newFirstgid = 1;
const lastTileset = this.mapFile.tilesets[ this.mapFile.tilesets.length - 1 ];
const lastTileset = this.mapFile.tilesets[this.mapFile.tilesets.length - 1];
if (lastTileset) {
//If there is at least one tileset in the tilemap then calculate the firstgid of the new tileset
newFirstgid = lastTileset.firstgid + lastTileset.tilecount;
@ -1298,14 +1299,14 @@ export class GameScene extends DirtyScene {
if (phaserLayers === []) {
console.warn(
'Could not find layer with name that contains "' +
layerName +
'" when calling WA.hideLayer / WA.showLayer'
layerName +
'" when calling WA.hideLayer / WA.showLayer'
);
return;
}
for (let i = 0; i < phaserLayers.length; i++) {
phaserLayers[ i ].setVisible(visible);
phaserLayers[ i ].setCollisionByProperty({ collides: true }, visible);
phaserLayers[i].setVisible(visible);
phaserLayers[i].setCollisionByProperty({ collides: true }, visible);
}
}
this.markDirty();

View File

@ -3,6 +3,7 @@ import { Scene } from "phaser";
import { ErrorScene, ErrorSceneName } from "../Reconnecting/ErrorScene";
import { WAError } from "../Reconnecting/WAError";
import { waScaleManager } from "../Services/WaScaleManager";
import { ReconnectingTextures } from "../Reconnecting/ReconnectingScene";
export const EntrySceneName = "EntryScene";
@ -17,6 +18,14 @@ export class EntryScene extends Scene {
});
}
// From the very start, let's preload images used in the ReconnectingScene.
preload() {
this.load.image(ReconnectingTextures.icon, "static/images/favicons/favicon-32x32.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(ReconnectingTextures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 });
}
create() {
gameManager
.init(this.scene)

View File

@ -4,6 +4,7 @@ import Sprite = Phaser.GameObjects.Sprite;
import Text = Phaser.GameObjects.Text;
import ScenePlugin = Phaser.Scenes.ScenePlugin;
import { WAError } from "./WAError";
import Axios from "axios";
export const ErrorSceneName = "ErrorScene";
enum Textures {
@ -36,7 +37,11 @@ export class ErrorScene extends Phaser.Scene {
preload() {
this.load.image(Textures.icon, "static/images/favicons/favicon-32x32.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(Textures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
if (!this.cache.bitmapFont.has("main_font")) {
// We put this inside a "if" because despite the cache, Phaser will make a query to the XML file. And if there is no connection (which
// is not unlikely given the fact we are in an error scene), this will cause an error.
this.load.bitmapFont(Textures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
}
this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 });
}
@ -71,9 +76,9 @@ export class ErrorScene extends Phaser.Scene {
/**
* Displays the error page, with an error message matching the "error" parameters passed in.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public static showError(error: any, scene: ScenePlugin): void {
public static showError(error: unknown, scene: ScenePlugin): void {
console.error(error);
console.trace();
if (typeof error === "string" || error instanceof String) {
scene.start(ErrorSceneName, {
@ -86,9 +91,10 @@ export class ErrorScene extends Phaser.Scene {
subTitle: error.subTitle,
message: error.details,
});
} else if (error.response) {
} else if (Axios.isAxiosError(error) && error.response) {
// Axios HTTP error
// client received an error response (5xx, 4xx)
console.error("Axios error. Request:", error.request, " - Response: ", error.response);
scene.start(ErrorSceneName, {
title:
"HTTP " +
@ -98,9 +104,10 @@ export class ErrorScene extends Phaser.Scene {
subTitle: "An error occurred while accessing URL:",
message: error.response.config.url,
});
} else if (error.request) {
} else if (Axios.isAxiosError(error)) {
// Axios HTTP error
// client never received a response, or request never left
console.error("Axios error. No full HTTP response received. Request to URL:", error.config.url);
scene.start(ErrorSceneName, {
title: "Network error",
subTitle: error.message,

View File

@ -3,7 +3,7 @@ import Image = Phaser.GameObjects.Image;
import Sprite = Phaser.GameObjects.Sprite;
export const ReconnectingSceneName = "ReconnectingScene";
enum ReconnectingTextures {
export enum ReconnectingTextures {
icon = "icon",
mainFont = "main_font",
}
@ -41,7 +41,7 @@ export class ReconnectingScene extends Phaser.Scene {
"Connection lost. Reconnecting..."
);
const cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, "cat");
const cat = this.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, "cat");
this.anims.create({
key: "right",
frames: this.anims.generateFrameNumbers("cat", { start: 6, end: 8 }),

View File

@ -1,15 +1,27 @@
import { writable } from "svelte/store";
import { derived, writable } from "svelte/store";
interface ErrorMessage {
id: string | undefined;
closable: boolean; // Whether it can be closed by a user action or not
message: string | number | boolean | undefined;
}
/**
* A store that contains a list of error messages to be displayed.
*/
function createErrorStore() {
const { subscribe, set, update } = writable<string[]>([]);
const { subscribe, set, update } = writable<ErrorMessage[]>([]);
return {
subscribe,
addErrorMessage: (e: string | Error): void => {
update((messages: string[]) => {
addErrorMessage: (
e: string | Error,
options?: {
closable?: boolean;
id?: string;
}
): void => {
update((messages: ErrorMessage[]) => {
let message: string;
if (e instanceof Error) {
message = e.message;
@ -17,17 +29,35 @@ function createErrorStore() {
message = e;
}
if (!messages.includes(message)) {
messages.push(message);
if (!messages.find((errorMessage) => errorMessage.message === message)) {
messages.push({
message,
closable: options?.closable ?? true,
id: options?.id,
});
}
return messages;
});
},
clearMessages: (): void => {
set([]);
clearMessageById: (id: string): void => {
update((messages: ErrorMessage[]) => {
messages = messages.filter((message) => message.id !== id);
return messages;
});
},
clearClosableMessages: (): void => {
update((messages: ErrorMessage[]) => {
messages = messages.filter((message) => message.closable);
return messages;
});
},
};
}
export const errorStore = createErrorStore();
export const hasClosableMessagesInErrorStore = derived(errorStore, ($errorStore) => {
const closableMessage = $errorStore.find((errorMessage) => errorMessage.closable);
return !!closableMessage;
});

View File

@ -32,6 +32,7 @@ module.exports = {
rewrites: [{ from: /^_\/.*$/, to: "/index.html" }],
disableDotRule: true,
},
liveReload: process.env.LIVE_RELOAD != "0" && process.env.LIVE_RELOAD != "false",
},
module: {
rules: [

View File

@ -2070,6 +2070,11 @@ escape-string-regexp@^4.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
eslint-plugin-svelte3@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-svelte3/-/eslint-plugin-svelte3-3.2.1.tgz#f0f24150ecea3061c38c69e282bea26dc3e660c6"
integrity sha512-YoBR9mLoKCjGghJ/gvpnFZKaMEu/VRcuxpSRS8KuozuEo7CdBH7bmBHa6FmMm0i4kJnOyx+PVsaptz96K6H/4Q==
eslint-scope@^5.0.0, eslint-scope@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
@ -2395,14 +2400,6 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"
file-loader@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d"
integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==
dependencies:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
file-uri-to-path@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
@ -4580,6 +4577,11 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier-plugin-svelte@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-2.5.0.tgz#7922534729f7febe59b4c56c3f5360539f0d8ab1"
integrity sha512-+iHY2uGChOngrgKielJUnqo74gIL/EO5oeWm8MftFWjEi213lq9QYTOwm1pv4lI1nA61tdgf80CF2i5zMcu1kw==
prettier@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6"
@ -4934,6 +4936,11 @@ ret@~0.1.10:
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
retry-axios@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/retry-axios/-/retry-axios-2.6.0.tgz#d4dc5c8a8e73982e26a705e46a33df99a28723e0"
integrity sha512-pOLi+Gdll3JekwuFjXO3fTq+L9lzMQGcSq7M5gIjExcl3Gu1hd4XXuf5o3+LuSBsaULQH7DiNbsqPd1chVpQGQ==
retry@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"

View File

@ -0,0 +1,33 @@
WA.onInit().then(() => {
console.log('Trying to read variable "doorOpened" whose default property is true. This should display "true".');
console.log('doorOpened', WA.state.loadVariable('doorOpened'));
console.log('Trying to set variable "not_exists". This should display an error in the console, followed by a log saying the error was caught.')
WA.state.saveVariable('not_exists', 'foo').catch((e) => {
console.log('Successfully caught error: ', e);
});
console.log('Trying to set variable "myvar". This should work.');
WA.state.saveVariable('myvar', {'foo': 'bar'});
console.log('Trying to read variable "myvar". This should display a {"foo": "bar"} object.');
console.log(WA.state.loadVariable('myvar'));
console.log('Trying to set variable "myvar" using proxy. This should work.');
WA.state.myvar = {'baz': 42};
console.log('Trying to read variable "myvar" using proxy. This should display a {"baz": 42} object.');
console.log(WA.state.myvar);
console.log('Trying to set variable "config". This should not work because we are not logged as admin.');
WA.state.saveVariable('config', {'foo': 'bar'}).catch(e => {
console.log('Successfully caught error because variable "config" is not writable: ', e);
});
console.log('Trying to read variable "readableByAdmin" that can only be read by "admin". We are not admin so we should not get the default value.');
if (WA.state.readableByAdmin === true) {
console.error('Failed test: readableByAdmin can be read.');
} else {
console.log('Success test: readableByAdmin was not read.');
}
});

View File

@ -0,0 +1,47 @@
<!doctype html>
<html lang="en">
<head>
<script>
var script = document.createElement('script');
// Don't do this at home kids! The "document.referrer" part is actually inserting a XSS security.
// We are OK in this precise case because the HTML page is hosted on the "maps" domain that contains only static files.
document.head.appendChild(script);
window.addEventListener('load', () => {
console.log('On load');
WA.onInit().then(() => {
console.log('After WA init');
const textField = document.getElementById('textField');
textField.value = WA.state.textField;
textField.addEventListener('change', function (evt) {
console.log('saving variable')
WA.state.textField = this.value;
});
WA.state.onVariableChange('textField').subscribe((value) => {
console.log('variable changed received')
textField.value = value;
});
document.getElementById('btn').addEventListener('click', () => {
console.log(WA.state.loadVariable('textField'));
document.getElementById('placeholder').innerText = WA.state.loadVariable('textField');
});
document.getElementById('setUndefined').addEventListener('click', () => {
WA.state.textField = undefined;
document.getElementById('textField').value = '';
});
});
})
</script>
</head>
<body>
<input type="text" id="textField" />
<button id="setUndefined">Delete variable</button>
<button id="btn">Display textField variable value</button>
<div id="placeholder"></div>
</body>
</html>

View File

@ -0,0 +1,131 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"layers":[
{
"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],
"height":10,
"id":1,
"name":"floor",
"opacity":1,
"properties":[
{
"name":"openWebsite",
"type":"string",
"value":"shared_variables.html"
},
{
"name":"openWebsiteAllowApi",
"type":"bool",
"value":true
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"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, 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],
"height":10,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[
{
"height":67,
"id":3,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":11,
"text":"Test:\nChange the form\nConnect with another user\n\nResult:\nThe form should open in the same state for the other user\nAlso, a change on one user is directly propagated to the other user",
"wrap":true
},
"type":"",
"visible":true,
"width":252.4375,
"x":2.78125,
"y":2.5
},
{
"height":0,
"id":5,
"name":"textField",
"point":true,
"properties":[
{
"name":"default",
"type":"string",
"value":"default value"
},
{
"name":"jsonSchema",
"type":"string",
"value":"{}"
},
{
"name":"persist",
"type":"bool",
"value":true
},
{
"name":"readableBy",
"type":"string",
"value":""
},
{
"name":"writableBy",
"type":"string",
"value":""
}],
"rotation":0,
"type":"variable",
"visible":true,
"width":0,
"x":57.5,
"y":111
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":8,
"nextobjectid":10,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"2021.03.23",
"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.5,
"width":10
}

View File

@ -274,12 +274,12 @@ message ServerToClientMessage {
SendJitsiJwtMessage sendJitsiJwtMessage = 11;
SendUserMessage sendUserMessage = 12;
BanUserMessage banUserMessage = 13;
AdminRoomMessage adminRoomMessage = 14;
//AdminRoomMessage adminRoomMessage = 14;
WorldFullWarningMessage worldFullWarningMessage = 15;
WorldFullMessage worldFullMessage = 16;
RefreshRoomMessage refreshRoomMessage = 17;
WorldConnexionMessage worldConnexionMessage = 18;
EmoteEventMessage emoteEventMessage = 19;
//EmoteEventMessage emoteEventMessage = 19;
TokenExpiredMessage tokenExpiredMessage = 20;
}
}

View File

@ -26,7 +26,7 @@ import { jwtTokenManager, tokenInvalidException } from "../Services/JWTTokenMana
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
import { SocketManager, socketManager } from "../Services/SocketManager";
import { emitInBatch } from "../Services/IoSocketHelpers";
import { ADMIN_SOCKETS_TOKEN, ADMIN_API_URL, DISABLE_ANONYMOUS, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable";
import { ADMIN_API_URL, DISABLE_ANONYMOUS, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable";
import { Zone } from "_Model/Zone";
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
import { CharacterTexture } from "../Services/AdminApi/CharacterTexture";

View File

@ -1,8 +1,5 @@
import { ADMIN_API_URL, ADMIN_SOCKETS_TOKEN, ALLOW_ARTILLERY, SECRET_KEY } from "../Enum/EnvironmentVariable";
import { uuid } from "uuidv4";
import Jwt, { verify } from "jsonwebtoken";
import { TokenInterface } from "../Controller/AuthenticateController";
import { adminApi, AdminBannedData } from "../Services/AdminApi";
import { ADMIN_SOCKETS_TOKEN, SECRET_KEY } from "../Enum/EnvironmentVariable";
import Jwt from "jsonwebtoken";
export interface AuthTokenData {
identifier: string; //will be a sub (id) if logged in or an uuid if anonymous

View File

@ -3,11 +3,28 @@ const BROWSER = process.env.BROWSER || "chrome --use-fake-ui-for-media-stream --
module.exports = {
"browsers": BROWSER,
"hostname": "localhost",
//"skipJsErrors": true,
"src": "tests/",
"screenshots": {
"path": "screenshots/",
"takeOnFails": true,
"thumbnails": false,
},
"assertionTimeout": 20000,
"selectorTimeout": 60000,
"videoPath": "screenshots/videos",
"videoOptions": {
"failedOnly": true,
}
/*"skipJsErrors": true,
"clientScripts": [ { "content": `
window.addEventListener('error', function (e) {
if (e instanceof Error && e.message.includes('_jp')) {
console.log('Ignoring sockjs related error');
return;
}
throw e;
});
` } ]*/
}

5
tests/Dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM testcafe/testcafe:1.17.1
USER root
RUN apk add docker-compose
USER user

734
tests/package-lock.json generated
View File

@ -4,7 +4,13 @@
"requires": true,
"packages": {
"": {
"dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"@types/dockerode": "^3.3.0",
"axios": "^0.24.0"
},
"devDependencies": {
"dockerode": "^3.3.1",
"testcafe": "^1.17.1"
}
},
@ -1804,6 +1810,123 @@
"node": ">=6.9.0"
}
},
"node_modules/@ffmpeg-installer/darwin-arm64": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-arm64/-/darwin-arm64-4.1.5.tgz",
"integrity": "sha512-hYqTiP63mXz7wSQfuqfFwfLOfwwFChUedeCVKkBtl/cliaTM7/ePI9bVzfZ2c+dWu3TqCwLDRWNSJ5pqZl8otA==",
"cpu": [
"arm64"
],
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@ffmpeg-installer/darwin-x64": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-x64/-/darwin-x64-4.1.0.tgz",
"integrity": "sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==",
"cpu": [
"x64"
],
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@ffmpeg-installer/ffmpeg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/ffmpeg/-/ffmpeg-1.1.0.tgz",
"integrity": "sha512-Uq4rmwkdGxIa9A6Bd/VqqYbT7zqh1GrT5/rFwCwKM70b42W5gIjWeVETq6SdcL0zXqDtY081Ws/iJWhr1+xvQg==",
"optionalDependencies": {
"@ffmpeg-installer/darwin-arm64": "4.1.5",
"@ffmpeg-installer/darwin-x64": "4.1.0",
"@ffmpeg-installer/linux-arm": "4.1.3",
"@ffmpeg-installer/linux-arm64": "4.1.4",
"@ffmpeg-installer/linux-ia32": "4.1.0",
"@ffmpeg-installer/linux-x64": "4.1.0",
"@ffmpeg-installer/win32-ia32": "4.1.0",
"@ffmpeg-installer/win32-x64": "4.1.0"
}
},
"node_modules/@ffmpeg-installer/linux-arm": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm/-/linux-arm-4.1.3.tgz",
"integrity": "sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==",
"cpu": [
"arm"
],
"hasInstallScript": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@ffmpeg-installer/linux-arm64": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm64/-/linux-arm64-4.1.4.tgz",
"integrity": "sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==",
"cpu": [
"arm64"
],
"hasInstallScript": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@ffmpeg-installer/linux-ia32": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-ia32/-/linux-ia32-4.1.0.tgz",
"integrity": "sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==",
"cpu": [
"ia32"
],
"hasInstallScript": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@ffmpeg-installer/linux-x64": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-x64/-/linux-x64-4.1.0.tgz",
"integrity": "sha512-Y5BWhGLU/WpQjOArNIgXD3z5mxxdV8c41C+U15nsE5yF8tVcdCGet5zPs5Zy3Ta6bU7haGpIzryutqCGQA/W8A==",
"cpu": [
"x64"
],
"hasInstallScript": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@ffmpeg-installer/win32-ia32": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-ia32/-/win32-ia32-4.1.0.tgz",
"integrity": "sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
]
},
"node_modules/@ffmpeg-installer/win32-x64": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-x64/-/win32-x64-4.1.0.tgz",
"integrity": "sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
]
},
"node_modules/@miherlosev/esm": {
"version": "3.2.26",
"resolved": "https://registry.npmjs.org/@miherlosev/esm/-/esm-3.2.26.tgz",
@ -1848,6 +1971,24 @@
"node": ">= 8"
}
},
"node_modules/@types/docker-modem": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.2.tgz",
"integrity": "sha512-qC7prjoEYR2QEe6SmCVfB1x3rfcQtUr1n4x89+3e0wSTMQ/KYCyf+/RAA9n2tllkkNc6//JMUZePdFRiGIWfaQ==",
"dependencies": {
"@types/node": "*",
"@types/ssh2": "*"
}
},
"node_modules/@types/dockerode": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.0.tgz",
"integrity": "sha512-3Mc0b2gnypJB8Gwmr+8UVPkwjpf4kg1gVxw8lAI4Y/EzpK50LixU1wBSPN9D+xqiw2Ubb02JO8oM0xpwzvi2mg==",
"dependencies": {
"@types/docker-modem": "*",
"@types/node": "*"
}
},
"node_modules/@types/error-stack-parser": {
"version": "1.3.18",
"resolved": "https://registry.npmjs.org/@types/error-stack-parser/-/error-stack-parser-1.3.18.tgz",
@ -1885,8 +2026,24 @@
"node_modules/@types/node": {
"version": "12.20.37",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.37.tgz",
"integrity": "sha512-i1KGxqcvJaLQali+WuypQnXwcplhtNtjs66eNsZpp2P2FL/trJJxx/VWsM0YCL2iMoIJrbXje48lvIQAQ4p2ZA==",
"dev": true
"integrity": "sha512-i1KGxqcvJaLQali+WuypQnXwcplhtNtjs66eNsZpp2P2FL/trJJxx/VWsM0YCL2iMoIJrbXje48lvIQAQ4p2ZA=="
},
"node_modules/@types/ssh2": {
"version": "0.5.49",
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.49.tgz",
"integrity": "sha512-ffxhQhJqgTzrw8NxHTgkaDtAmAj2qxCyoves7ztpRgqvzbHcZTpTcm+ATWuuCbPQzxnnF4F3SGGTLGEWTZpwqA==",
"dependencies": {
"@types/node": "*",
"@types/ssh2-streams": "*"
}
},
"node_modules/@types/ssh2-streams": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.9.tgz",
"integrity": "sha512-I2J9jKqfmvXLR5GomDiCoHrEJ58hAOmFrekfFqmCFd+A6gaEStvWnPykoWUwld1PNg4G5ag1LwdA+Lz1doRJqg==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/acorn-hammerhead": {
"version": "0.5.0",
@ -2012,6 +2169,15 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"dev": true,
"dependencies": {
"safer-buffer": "~2.1.0"
}
},
"node_modules/assertion-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
@ -2048,6 +2214,14 @@
"node": ">= 4.5.0"
}
},
"node_modules/axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"dependencies": {
"follow-redirects": "^1.14.4"
}
},
"node_modules/babel-plugin-dynamic-import-node": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz",
@ -2153,12 +2327,46 @@
}
]
},
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
"dev": true,
"dependencies": {
"tweetnacl": "^0.14.3"
}
},
"node_modules/bin-v8-flags-filter": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/bin-v8-flags-filter/-/bin-v8-flags-filter-1.2.0.tgz",
"integrity": "sha512-g8aeYkY7GhyyKRvQMBsJQZjhm2iCX3dKYvfrMpwVR8IxmUGrkpCBFoKbB9Rh0o3sTLCjU/1tFpZ4C7j3f+D+3g==",
"dev": true
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dev": true,
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/bl/node_modules/readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@ -2225,6 +2433,30 @@
"url": "https://opencollective.com/browserslist"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -2319,6 +2551,12 @@
"node": "*"
}
},
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"dev": true
},
"node_modules/chrome-remote-interface": {
"version": "0.30.1",
"resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.30.1.tgz",
@ -2449,6 +2687,20 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true
},
"node_modules/cpu-features": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.2.tgz",
"integrity": "sha512-/2yieBqvMcRj8McNzkycjW2v3OIUOibBfd2dLEJ0nWts8NobAxwiyw9phVNS6oDL8x8tz9F7uNVFEVpJncQpeA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"dependencies": {
"nan": "^2.14.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -2649,6 +2901,60 @@
"node": ">=8"
}
},
"node_modules/docker-modem": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.3.tgz",
"integrity": "sha512-Tgkn2a+yiNP9FoZgMa/D9Wk+D2Db///0KOyKSYZRJa8w4+DzKyzQMkczKSdR/adQ0x46BOpeNkoyEOKjPhCzjw==",
"dev": true,
"dependencies": {
"debug": "^4.1.1",
"readable-stream": "^3.5.0",
"split-ca": "^1.0.1",
"ssh2": "^1.4.0"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/docker-modem/node_modules/readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/dockerode": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.3.1.tgz",
"integrity": "sha512-AS2mr8Lp122aa5n6d99HkuTNdRV1wkkhHwBdcnY6V0+28D3DSYwhxAk85/mM9XwD3RMliTxyr63iuvn5ZblFYQ==",
"dev": true,
"dependencies": {
"docker-modem": "^3.0.0",
"tar-fs": "~2.0.1"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/dockerode/node_modules/tar-fs": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz",
"integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==",
"dev": true,
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.0.0"
}
},
"node_modules/electron-to-chromium": {
"version": "1.3.906",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.906.tgz",
@ -2852,6 +3158,31 @@
"node": ">=6"
}
},
"node_modules/follow-redirects": {
"version": "1.14.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
"integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -3100,6 +3431,26 @@
"node": ">=0.10.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/ignore": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz",
@ -3549,6 +3900,12 @@
"mkdirp": "bin/cmd.js"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"dev": true
},
"node_modules/moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
@ -3582,6 +3939,13 @@
"npm": ">=1.4.0"
}
},
"node_modules/nan": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
"dev": true,
"optional": true
},
"node_modules/nanoid": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-1.3.4.tgz",
@ -4281,6 +4645,30 @@
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
"dev": true
},
"node_modules/split-ca": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
"integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=",
"dev": true
},
"node_modules/ssh2": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.5.0.tgz",
"integrity": "sha512-iUmRkhH9KGeszQwDW7YyyqjsMTf4z+0o48Cp4xOwlY5LjtbIAvyd3fwnsoUZW/hXmTCRA3yt7S/Jb9uVjErVlA==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"asn1": "^0.2.4",
"bcrypt-pbkdf": "^1.0.2"
},
"engines": {
"node": ">=10.16.0"
},
"optionalDependencies": {
"cpu-features": "0.0.2",
"nan": "^2.15.0"
}
},
"node_modules/stackframe": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-0.3.1.tgz",
@ -4355,6 +4743,36 @@
"node": ">=4"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true,
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tar-stream/node_modules/readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/testcafe": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/testcafe/-/testcafe-1.17.1.tgz",
@ -4889,6 +5307,12 @@
"node": "*"
}
},
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
"dev": true
},
"node_modules/type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
@ -6330,6 +6754,69 @@
"to-fast-properties": "^2.0.0"
}
},
"@ffmpeg-installer/darwin-arm64": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-arm64/-/darwin-arm64-4.1.5.tgz",
"integrity": "sha512-hYqTiP63mXz7wSQfuqfFwfLOfwwFChUedeCVKkBtl/cliaTM7/ePI9bVzfZ2c+dWu3TqCwLDRWNSJ5pqZl8otA==",
"optional": true
},
"@ffmpeg-installer/darwin-x64": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-x64/-/darwin-x64-4.1.0.tgz",
"integrity": "sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==",
"optional": true
},
"@ffmpeg-installer/ffmpeg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/ffmpeg/-/ffmpeg-1.1.0.tgz",
"integrity": "sha512-Uq4rmwkdGxIa9A6Bd/VqqYbT7zqh1GrT5/rFwCwKM70b42W5gIjWeVETq6SdcL0zXqDtY081Ws/iJWhr1+xvQg==",
"requires": {
"@ffmpeg-installer/darwin-arm64": "4.1.5",
"@ffmpeg-installer/darwin-x64": "4.1.0",
"@ffmpeg-installer/linux-arm": "4.1.3",
"@ffmpeg-installer/linux-arm64": "4.1.4",
"@ffmpeg-installer/linux-ia32": "4.1.0",
"@ffmpeg-installer/linux-x64": "4.1.0",
"@ffmpeg-installer/win32-ia32": "4.1.0",
"@ffmpeg-installer/win32-x64": "4.1.0"
}
},
"@ffmpeg-installer/linux-arm": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm/-/linux-arm-4.1.3.tgz",
"integrity": "sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==",
"optional": true
},
"@ffmpeg-installer/linux-arm64": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm64/-/linux-arm64-4.1.4.tgz",
"integrity": "sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==",
"optional": true
},
"@ffmpeg-installer/linux-ia32": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-ia32/-/linux-ia32-4.1.0.tgz",
"integrity": "sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==",
"optional": true
},
"@ffmpeg-installer/linux-x64": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-x64/-/linux-x64-4.1.0.tgz",
"integrity": "sha512-Y5BWhGLU/WpQjOArNIgXD3z5mxxdV8c41C+U15nsE5yF8tVcdCGet5zPs5Zy3Ta6bU7haGpIzryutqCGQA/W8A==",
"optional": true
},
"@ffmpeg-installer/win32-ia32": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-ia32/-/win32-ia32-4.1.0.tgz",
"integrity": "sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==",
"optional": true
},
"@ffmpeg-installer/win32-x64": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-x64/-/win32-x64-4.1.0.tgz",
"integrity": "sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==",
"optional": true
},
"@miherlosev/esm": {
"version": "3.2.26",
"resolved": "https://registry.npmjs.org/@miherlosev/esm/-/esm-3.2.26.tgz",
@ -6362,6 +6849,24 @@
"fastq": "^1.6.0"
}
},
"@types/docker-modem": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.2.tgz",
"integrity": "sha512-qC7prjoEYR2QEe6SmCVfB1x3rfcQtUr1n4x89+3e0wSTMQ/KYCyf+/RAA9n2tllkkNc6//JMUZePdFRiGIWfaQ==",
"requires": {
"@types/node": "*",
"@types/ssh2": "*"
}
},
"@types/dockerode": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.0.tgz",
"integrity": "sha512-3Mc0b2gnypJB8Gwmr+8UVPkwjpf4kg1gVxw8lAI4Y/EzpK50LixU1wBSPN9D+xqiw2Ubb02JO8oM0xpwzvi2mg==",
"requires": {
"@types/docker-modem": "*",
"@types/node": "*"
}
},
"@types/error-stack-parser": {
"version": "1.3.18",
"resolved": "https://registry.npmjs.org/@types/error-stack-parser/-/error-stack-parser-1.3.18.tgz",
@ -6399,8 +6904,24 @@
"@types/node": {
"version": "12.20.37",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.37.tgz",
"integrity": "sha512-i1KGxqcvJaLQali+WuypQnXwcplhtNtjs66eNsZpp2P2FL/trJJxx/VWsM0YCL2iMoIJrbXje48lvIQAQ4p2ZA==",
"dev": true
"integrity": "sha512-i1KGxqcvJaLQali+WuypQnXwcplhtNtjs66eNsZpp2P2FL/trJJxx/VWsM0YCL2iMoIJrbXje48lvIQAQ4p2ZA=="
},
"@types/ssh2": {
"version": "0.5.49",
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.49.tgz",
"integrity": "sha512-ffxhQhJqgTzrw8NxHTgkaDtAmAj2qxCyoves7ztpRgqvzbHcZTpTcm+ATWuuCbPQzxnnF4F3SGGTLGEWTZpwqA==",
"requires": {
"@types/node": "*",
"@types/ssh2-streams": "*"
}
},
"@types/ssh2-streams": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.9.tgz",
"integrity": "sha512-I2J9jKqfmvXLR5GomDiCoHrEJ58hAOmFrekfFqmCFd+A6gaEStvWnPykoWUwld1PNg4G5ag1LwdA+Lz1doRJqg==",
"requires": {
"@types/node": "*"
}
},
"acorn-hammerhead": {
"version": "0.5.0",
@ -6498,6 +7019,15 @@
}
}
},
"asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"dev": true,
"requires": {
"safer-buffer": "~2.1.0"
}
},
"assertion-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
@ -6522,6 +7052,14 @@
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"dev": true
},
"axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"requires": {
"follow-redirects": "^1.14.4"
}
},
"babel-plugin-dynamic-import-node": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz",
@ -6600,12 +7138,45 @@
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true
},
"bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
"dev": true,
"requires": {
"tweetnacl": "^0.14.3"
}
},
"bin-v8-flags-filter": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/bin-v8-flags-filter/-/bin-v8-flags-filter-1.2.0.tgz",
"integrity": "sha512-g8aeYkY7GhyyKRvQMBsJQZjhm2iCX3dKYvfrMpwVR8IxmUGrkpCBFoKbB9Rh0o3sTLCjU/1tFpZ4C7j3f+D+3g==",
"dev": true
},
"bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dev": true,
"requires": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
},
"dependencies": {
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
}
}
},
"bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@ -6659,6 +7230,16 @@
"picocolors": "^1.0.0"
}
},
"buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -6734,6 +7315,12 @@
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
"dev": true
},
"chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"dev": true
},
"chrome-remote-interface": {
"version": "0.30.1",
"resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.30.1.tgz",
@ -6842,6 +7429,16 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true
},
"cpu-features": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.2.tgz",
"integrity": "sha512-/2yieBqvMcRj8McNzkycjW2v3OIUOibBfd2dLEJ0nWts8NobAxwiyw9phVNS6oDL8x8tz9F7uNVFEVpJncQpeA==",
"dev": true,
"optional": true,
"requires": {
"nan": "^2.14.1"
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -7000,6 +7597,55 @@
"path-type": "^4.0.0"
}
},
"docker-modem": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.3.tgz",
"integrity": "sha512-Tgkn2a+yiNP9FoZgMa/D9Wk+D2Db///0KOyKSYZRJa8w4+DzKyzQMkczKSdR/adQ0x46BOpeNkoyEOKjPhCzjw==",
"dev": true,
"requires": {
"debug": "^4.1.1",
"readable-stream": "^3.5.0",
"split-ca": "^1.0.1",
"ssh2": "^1.4.0"
},
"dependencies": {
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
}
}
},
"dockerode": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.3.1.tgz",
"integrity": "sha512-AS2mr8Lp122aa5n6d99HkuTNdRV1wkkhHwBdcnY6V0+28D3DSYwhxAk85/mM9XwD3RMliTxyr63iuvn5ZblFYQ==",
"dev": true,
"requires": {
"docker-modem": "^3.0.0",
"tar-fs": "~2.0.1"
},
"dependencies": {
"tar-fs": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz",
"integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==",
"dev": true,
"requires": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.0.0"
}
}
}
},
"electron-to-chromium": {
"version": "1.3.906",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.906.tgz",
@ -7165,6 +7811,17 @@
"locate-path": "^3.0.0"
}
},
"follow-redirects": {
"version": "1.14.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
"integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA=="
},
"fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -7357,6 +8014,12 @@
"safer-buffer": ">= 2.1.2 < 3"
}
},
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true
},
"ignore": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz",
@ -7699,6 +8362,12 @@
"minimist": "^1.2.5"
}
},
"mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"dev": true
},
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
@ -7723,6 +8392,13 @@
"integrity": "sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ==",
"dev": true
},
"nan": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
"dev": true,
"optional": true
},
"nanoid": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-1.3.4.tgz",
@ -8257,6 +8933,24 @@
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
"dev": true
},
"split-ca": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
"integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=",
"dev": true
},
"ssh2": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.5.0.tgz",
"integrity": "sha512-iUmRkhH9KGeszQwDW7YyyqjsMTf4z+0o48Cp4xOwlY5LjtbIAvyd3fwnsoUZW/hXmTCRA3yt7S/Jb9uVjErVlA==",
"dev": true,
"requires": {
"asn1": "^0.2.4",
"bcrypt-pbkdf": "^1.0.2",
"cpu-features": "0.0.2",
"nan": "^2.15.0"
}
},
"stackframe": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-0.3.1.tgz",
@ -8316,6 +9010,32 @@
"has-flag": "^3.0.0"
}
},
"tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true,
"requires": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"dependencies": {
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
}
}
},
"testcafe": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/testcafe/-/testcafe-1.17.1.tgz",
@ -8772,6 +9492,12 @@
"safe-buffer": "^5.0.1"
}
},
"tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
"dev": true
},
"type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",

View File

@ -1,8 +1,14 @@
{
"devDependencies": {
"dockerode": "^3.3.1",
"testcafe": "^1.17.1"
},
"scripts": {
"test": "testcafe"
},
"dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"@types/dockerode": "^3.3.0",
"axios": "^0.24.0"
}
}

View File

@ -1,13 +1,38 @@
import {assertLogMessage} from "./utils/log";
const fs = require('fs')
const fs = require('fs');
const Docker = require('dockerode');
import { Selector } from 'testcafe';
import {userAlice} from "./utils/roles";
import {login} from "./utils/roles";
import {findContainer, rebootBack, rebootPusher, resetRedis, startContainer, stopContainer} from "./utils/containers";
// Note: we are also testing that we can connect if the URL contains a random query string
fixture `Variables`
.page `http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/Cache/variables_tmp.json?somerandomparam=1`;
fixture `Reconnection`
.page `http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/mousewheel.json`;
test("Test that connection can succeed even if WorkAdventure starts while pusher is down", async (t: TestController) => {
// Let's stop the pusher
const container = await findContainer('pusher');
await stopContainer(container);
const errorMessage = Selector('.error-div');
await t
.navigateTo('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/mousewheel.json')
.expect(errorMessage.innerText).contains('Unable to connect to WorkAdventure')
await startContainer(container);
await login(t, 'http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/mousewheel.json');
t.ctx.passed = true;
}).after(async t => {
if (!t.ctx.passed) {
console.log("Test failed. Browser logs:")
console.log(await t.getBrowserConsoleMessages());
}
});
/*
test("Test that variables cache in the back don't prevent setting a variable in case the map changes", async (t: TestController) => {
// Let's start by visiting a map that DOES not have the variable.
fs.copyFileSync('../maps/tests/Variables/Cache/variables_cache_1.json', '../maps/tests/Variables/Cache/variables_tmp.json');
@ -18,11 +43,9 @@ test("Test that variables cache in the back don't prevent setting a variable in
// Let's REPLACE the map by a map that has a new variable
// At this point, the back server contains a cache of the old map (with no variables)
fs.copyFileSync('../maps/tests/Variables/Cache/variables_cache_2.json', '../maps/tests/Variables/Cache/variables_tmp.json');
await t.openWindow('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/Cache/variables_tmp.json');
await t.resizeWindow(960, 800);
await t.useRole(userAlice);
await t.openWindow('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/Cache/variables_tmp.json')
.resizeWindow(960, 800)
.useRole(userAlice);
//.takeScreenshot('after_switch.png');
// Let's check we successfully manage to save the variable value.
@ -35,3 +58,4 @@ test("Test that variables cache in the back don't prevent setting a variable in
console.log(await t.getBrowserConsoleMessages());
}
});
*/

View File

@ -0,0 +1,85 @@
//import Docker from "dockerode";
//import * as Dockerode from "dockerode";
import Dockerode = require( 'dockerode')
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const { execSync } = require('child_process');
const path = require("path");
const fs = require('fs');
/**
* Execute Docker compose, passing the correct host directory (in case this is run from the TestCafe container)
*/
export function dockerCompose(command: string): void {
let param = '';
const projectDir = process.env.PROJECT_DIR;
if (!projectDir && fs.existsSync('/project')) {
// We are probably in the docker-image AND we did not pass PROJECT_DIR env variable
throw new Error('Incorrect docker-compose command used to fire testcafe tests. You need to add a PROJECT_DIR environment variable. Please refer to the CONTRIBUTING.md guide');
}
if (projectDir) {
const dirName = path.basename(projectDir);
param = '--project-name '+dirName+' --project-directory '+projectDir;
}
let stdout = execSync('docker-compose '+param+' '+command, {
cwd: __dirname + '/../../../'
});
}
/**
* Returns a container ID based on the container name.
*/
export async function findContainer(name: string): Promise<Dockerode.ContainerInfo> {
const docker = new Dockerode();
const containers = await docker.listContainers();
const foundContainer = containers.find((container) => container.State === 'running' && container.Names.find((containerName) => containerName.includes(name)));
if (foundContainer === undefined) {
throw new Error('Could not find a running container whose name contains "'+name+'"');
}
return foundContainer;
}
export async function stopContainer(container: Dockerode.ContainerInfo): Promise<void> {
const docker = new Dockerode();
await docker.getContainer(container.Id).stop();
}
export async function startContainer(container: Dockerode.ContainerInfo): Promise<void> {
const docker = new Dockerode();
await docker.getContainer(container.Id).start();
}
export async function rebootBack(): Promise<void> {
dockerCompose('up --force-recreate -d back');
}
export function rebootTraefik(): void {
dockerCompose('up --force-recreate -d reverse-proxy');
}
export async function rebootPusher(): Promise<void> {
dockerCompose('up --force-recreate -d pusher');
}
export async function resetRedis(): Promise<void> {
dockerCompose('stop redis');
dockerCompose('rm -f redis');
dockerCompose('up --force-recreate -d redis');
}
export function stopRedis(): void {
dockerCompose('stop redis');
}
export function startRedis(): void {
dockerCompose('start redis');
}

View File

@ -0,0 +1,31 @@
import axios from "axios";
const fs = require('fs');
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN;
export async function getPusherDump(): Promise<any> {
let url = 'http://pusher.workadventure.localhost/dump?token='+ADMIN_API_TOKEN;
if (fs.existsSync('/project')) {
// We are inside a container. Let's use a direct route
url = 'http://pusher:8080/dump?token='+ADMIN_API_TOKEN;
}
return (await axios({
url,
method: 'get',
})).data;
}
export async function getBackDump(): Promise<any> {
let url = 'http://api.workadventure.localhost/dump?token='+ADMIN_API_TOKEN;
if (fs.existsSync('/project')) {
// We are inside a container. Let's use a direct route
url = 'http://back:8080/dump?token='+ADMIN_API_TOKEN;
}
return (await axios({
url,
method: 'get',
})).data;
}

View File

@ -1,20 +1,15 @@
import { Role } from 'testcafe';
export const userAlice = Role('http://play.workadventure.localhost/', async t => {
await t
.typeText('input[name="loginSceneName"]', 'Alice')
.click('button.loginSceneFormSubmit')
.click('button.selectCharacterButtonRight')
.click('button.selectCharacterButtonRight')
.click('button.selectCharacterSceneFormSubmit')
.click('button.letsgo');
});
export function login(t: TestController, url: string, userName: string = "Alice", characterNumber: number = 2) {
t = t
.navigateTo(url)
.typeText('input[name="loginSceneName"]', userName)
.click('button.loginSceneFormSubmit');
export const userBob = Role('http://play.workadventure.localhost/', async t => {
await t
.typeText('input[name="loginSceneName"]', 'Bob')
.click('button.loginSceneFormSubmit')
.click('button.selectCharacterButtonRight')
.click('button.selectCharacterSceneFormSubmit')
for (let i = 0; i < characterNumber; i++) {
t = t.click('button.selectCharacterButtonRight');
}
return t.click('button.selectCharacterSceneFormSubmit')
.click('button.letsgo');
});
}

202
tests/tests/variables.ts Normal file
View File

@ -0,0 +1,202 @@
import {assertLogMessage} from "./utils/log";
const fs = require('fs');
const Docker = require('dockerode');
import { Selector } from 'testcafe';
import {login} from "./utils/roles";
import {
findContainer,
rebootBack,
rebootPusher,
resetRedis,
rebootTraefik,
startContainer,
stopContainer, stopRedis, startRedis
} from "./utils/containers";
import {getBackDump, getPusherDump} from "./utils/debug";
// Note: we are also testing that passing a random query parameter does not cause any issue.
fixture `Variables`
.page `http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json?somerandomparam=1`;
test("Test that variables storage works", async (t: TestController) => {
const variableInput = Selector('#textField');
await resetRedis();
await Promise.all([
rebootBack(),
rebootPusher(),
]);
//const mainWindow = await t.getCurrentWindow();
await login(t, 'http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json?somerandomparam=1');
await t //.useRole(userAliceOnPage('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json?somerandomparam=1'))
.switchToIframe("#cowebsite-buffer iframe")
.expect(variableInput.value).eql('default value')
.selectText(variableInput)
.pressKey('delete')
.typeText(variableInput, 'new value')
.pressKey('tab')
.switchToMainWindow()
//.switchToWindow(mainWindow)
.wait(500)
// reload
.navigateTo('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json')
.switchToIframe("#cowebsite-buffer iframe")
.expect(variableInput.value).eql('new value')
//.debug()
.switchToMainWindow()
//.wait(5000)
//.switchToWindow(mainWindow)
/*
.wait(5000)
//.debug()
.switchToIframe("#cowebsite-buffer iframe")
.expect(variableInput.value).eql('new value')
.switchToMainWindow();*/
// Now, let's kill the reverse proxy to cut the connexion
//console.log('Rebooting traefik');
rebootTraefik();
//console.log('Rebooting done');
// Maybe we should:
// 1: stop Traefik
// 2: detect reconnecting screen
// 3: start Traefik again
await t
.switchToIframe("#cowebsite-buffer iframe")
.expect(variableInput.value).eql('new value')
stopRedis();
// Redis is stopped, let's try to modify a variable.
await t.selectText(variableInput)
.pressKey('delete')
.typeText(variableInput, 'value set while Redis stopped')
.pressKey('tab')
.switchToMainWindow()
startRedis();
// Navigate to some other map so that the pusher connection is freed.
await t.navigateTo('http://maps.workadventure.localhost/tests/')
.wait(3000);
const backDump = await getBackDump();
//console.log('backDump', backDump);
for (const room of backDump) {
if (room.roomUrl === 'http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json') {
throw new Error('Room still found in back');
}
}
const pusherDump = await getPusherDump();
//console.log('pusherDump', pusherDump);
await t.expect(pusherDump['http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json']).eql(undefined);
await t.navigateTo('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json')
.switchToIframe("#cowebsite-buffer iframe")
// Redis will reconnect automatically and will store the variable on reconnect!
// So we should see the new value.
.expect(variableInput.value).eql('value set while Redis stopped')
.switchToMainWindow()
// Now, let's try to kill / reboot the back
await rebootBack();
await t.navigateTo('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json')
.switchToIframe("#cowebsite-buffer iframe")
.expect(variableInput.value).eql('value set while Redis stopped')
.selectText(variableInput)
.pressKey('delete')
.typeText(variableInput, 'value set after back restart')
.pressKey('tab')
.switchToMainWindow()
.navigateTo('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json')
.switchToIframe("#cowebsite-buffer iframe")
// Redis will reconnect automatically and will store the variable on reconnect!
// So we should see the new value.
.expect(variableInput.value).eql('value set after back restart')
.switchToMainWindow()
// Now, let's try to kill / reboot the back
await rebootPusher();
await t.navigateTo('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json')
.switchToIframe("#cowebsite-buffer iframe")
.expect(variableInput.value).eql('value set after back restart')
.selectText(variableInput)
.pressKey('delete')
.typeText(variableInput, 'value set after pusher restart')
.pressKey('tab')
.switchToMainWindow()
.navigateTo('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json')
.switchToIframe("#cowebsite-buffer iframe")
// Redis will reconnect automatically and will store the variable on reconnect!
// So we should see the new value.
.expect(variableInput.value).eql('value set after pusher restart')
.switchToMainWindow()
t.ctx.passed = true;
}).after(async t => {
if (!t.ctx.passed) {
console.log("Test 'Test that variables storage works' failed. Browser logs:")
try {
console.log(await t.getBrowserConsoleMessages());
} catch (e) {
console.error('Error while fetching browser logs (maybe linked to a closed iframe?)', e);
try {
console.log('Logs from main window:');
console.log(await t.switchToMainWindow().getBrowserConsoleMessages());
} catch (e) {
console.error('Unable to retrieve logs', e);
}
}
}
});
test("Test that variables cache in the back don't prevent setting a variable in case the map changes", async (t: TestController) => {
// Let's start by visiting a map that DOES not have the variable.
fs.copyFileSync('../maps/tests/Variables/Cache/variables_cache_1.json', '../maps/tests/Variables/Cache/variables_tmp.json');
//const aliceOnPageTmp = userAliceOnPage('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/Cache/variables_tmp.json');
await login(t, 'http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/Cache/variables_tmp.json', 'Alice', 2);
//.takeScreenshot('before_switch.png');
// Let's REPLACE the map by a map that has a new variable
// At this point, the back server contains a cache of the old map (with no variables)
fs.copyFileSync('../maps/tests/Variables/Cache/variables_cache_2.json', '../maps/tests/Variables/Cache/variables_tmp.json');
await t.openWindow('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/Cache/variables_tmp.json')
.resizeWindow(960, 800)
await login(t, 'http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/Cache/variables_tmp.json', 'Bob', 3);
//.takeScreenshot('after_switch.png');
// Let's check we successfully manage to save the variable value.
await assertLogMessage(t, 'SUCCESS!');
t.ctx.passed = true;
}).after(async t => {
if (!t.ctx.passed) {
console.log("Test 'Test that variables cache in the back don't prevent setting a variable in case the map changes' failed. Browser logs:")
console.log(await t.getBrowserConsoleMessages());
}
});