Merge branch 'develop' of https://github.com/thecodingmachine/workadventure into new-favicon

This commit is contained in:
Valdo Romao 2021-06-28 15:56:28 +01:00
commit 4cf5a6f7a0
489 changed files with 34967 additions and 8127 deletions

View file

@ -25,6 +25,15 @@
],
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "error"
"@typescript-eslint/no-explicit-any": "error",
// TODO: remove those ignored rules and write a stronger code!
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/restrict-plus-operands": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/restrict-template-expressions": "off"
}
}

6
front/.gitignore vendored
View file

@ -1,5 +1,9 @@
/node_modules/
/dist/bundle.js
/dist/*.js
/dist/*.js.map
/dist/*.js.LICENSE.txt
/dist/main.*.css
/dist/main.*.css.map
/dist/tests/
/yarn-error.log
/dist/webpack.config.js

1
front/.prettierignore Normal file
View file

@ -0,0 +1 @@
src/Messages/generated

4
front/.prettierrc.json Normal file
View file

@ -0,0 +1,4 @@
{
"printWidth": 120,
"tabWidth": 4
}

View file

@ -3,11 +3,16 @@ WORKDIR /var/www/messages
COPY --chown=docker:docker messages .
RUN yarn install && yarn proto
# we are rebuilding on each deploy to cope with the API_URL environment URL
# we are rebuilding on each deploy to cope with the PUSHER_URL environment URL
FROM thecodingmachine/nodejs:14-apache
COPY --chown=docker:docker front .
COPY --from=builder --chown=docker:docker /var/www/messages/generated /var/www/html/src/Messages/generated
# Removing the iframe.html file from the final image as this adds a XSS attack.
# iframe.html is only in dev mode to circumvent a limitation
RUN rm dist/iframe.html
RUN yarn install
ENV NODE_ENV=production

View file

@ -1,3 +1,4 @@
index.html
index.tmpl.html.tmp
style.*.css
/js/
style.*.css

17
front/dist/iframe.html vendored Normal file
View file

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<script src="/iframe_api.js" ></script>
<script>
// Note: this is a huge XSS flow as we allow anyone to load a Javascript file in our domain.
// This file must ABSOLUTELY be removed from the Docker images/deployments and is only here
// for development purpose (because dynamically generated iframes are not working with
// webpack hot reload due to an issue with rights)
const urlParams = new URLSearchParams(window.location.search);
const scriptUrl = urlParams.get('script');
const script = document.createElement('script');
script.src = scriptUrl;
document.head.append(script);
</script>
</head>
</html>

View file

@ -29,12 +29,16 @@
<base href="/">
<link href="https://unpkg.com/nes.css@2.3.0/css/nes.min.css" rel="stylesheet" />
<title>WorkAdventure</title>
</head>
<body id="body" style="margin: 0; background-color: #000">
<div class="main-container" id="main-container">
<!-- Create the editor container -->
<div id="game" class="game">
<div id="svelte-overlay">
</div>
<div id="game-overlay" class="game-overlay">
<div id="main-section" class="main-section">
</div>
@ -42,26 +46,6 @@
</aside>
<div id="chat-mode" class="chat-mode three-col" style="display: none;">
</div>
<div id="activeCam" class="activeCam">
<div id="div-myCamVideo" class="video-container">
<video id="myCamVideo" autoplay muted></video>
</div>
</div>
<div class="btn-cam-action">
<div id="btn-micro" class="btn-micro">
<img id="microphone" src="resources/logos/microphone.svg">
<img id="microphone-close" src="resources/logos/microphone-close.svg">
</div>
<div id="btn-video" class="btn-video">
<img id="cinema" src="resources/logos/cinema.svg">
<img id="cinema-close" src="resources/logos/cinema-close.svg">
</div>
<div id="btn-monitor" class="btn-monitor">
<img id="monitor" src="resources/logos/monitor.svg">
<img id="monitor-close" src="resources/logos/monitor-close.svg">
</div>
</div>
</div>
</div>
<div id="cowebsite" class="cowebsite hidden">
@ -70,11 +54,11 @@
</aside>
<main id="cowebsite-main">
</main>
<button class="top-right-btn" id="cowebsite-fullscreen">
<img id="cowebsite-fullscreen-open" src="resources/logos/monitor.svg"/>
<img id="cowebsite-fullscreen-close" style="display: none;" src="resources/logos/monitor-close.svg"/>
<button class="top-right-btn" id="cowebsite-fullscreen" alt="fullscreen mode">
<img id="cowebsite-fullscreen-open" src="resources/logos/fullscreen.svg"/>
<img id="cowebsite-fullscreen-close" style="display: none;" src="resources/logos/fullscreen-exit.svg"/>
</button>
<button class="top-right-btn" id="cowebsite-close">
<button class="top-right-btn" id="cowebsite-close" alt="close the iframe">
<img src="resources/logos/close.svg"/>
</button>
</div>
@ -96,30 +80,17 @@
</div>
</div>
<div class="audioplayer">
<label id="label-audioplayer_decrease_while_talking" for="audiooplayer_decrease_while_talking" title="decrease background volume by 50% when entering conversations">
<label id="label-audioplayer_decrease_while_talking" for="audioplayer_decrease_while_talking" title="decrease background volume by 50% when entering conversations">
reduce in conversations
<input type="checkbox" id="audioplayer_decrease_while_talking" checked />
</label>
<div id="audioplayer" style="visibility: hidden"></div>
</div>
</div>
<div class="audio-playing">
<img src="/resources/logos/megaphone.svg" />
</div>
</div>
<div id="activeScreenSharing" class="active-screen-sharing active">
</div>
<div id="webRtcSetup" class="webrtcsetup">
<img id="webRtcSetupNoVideo" class="background-img" src="resources/logos/cinema-close.svg">
<video id="myCamVideoSetup" autoplay muted></video>
</div>
<audio id="audio-webrtc-in">
<source src="/resources/objects/webrtc-in.mp3" type="audio/mp3">
</audio>
<audio id="audio-webrtc-out">
<source src="/resources/objects/webrtc-out.mp3" type="audio/mp3">
</audio>
<audio id="report-message">
<source src="/resources/objects/report-message.mp3" type="audio/mp3">
</audio>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

5
front/dist/resources/fonts/fonts.css vendored Normal file
View file

@ -0,0 +1,5 @@
/*This file is a workaround to allow phaser to load directly this font */
@font-face {
font-family: "Press Start 2P";
src: url("/fonts/press-start-2p-latin-400-normal.woff2") format('woff2');
}

View file

@ -1,4 +1,7 @@
<style>
#gameMenu main{
margin-top: 15px;
}
#gameMenu button {
background-color: black;
color: white;
@ -16,6 +19,21 @@
width: 32px;
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
}
@media only screen and (max-height: 700px) {
#gameMenu main {
display: flex;
flex-direction: row;
align-items: flex-end;
flex-wrap: wrap;
margin-top: 0;
}
#gameMenu section{
margin: 2px;
}
section#socialLinks{
position: relative;
}
}
</style>
<div id="gameMenu" hidden>
@ -30,9 +48,15 @@
<section>
<button id="changeSkinButton">Edit skin</button>
</section>
<section>
<button id="changeCompanionButton">Edit companion</button>
</section>
<section>
<button id="editGameSettingsButton">Settings</button>
</section>
<section>
<button id="toggleFullscreen">Toggle fullscreen</button>
</section>
<section>
<button id="sparkButton">Create map</button>
</section>
@ -40,7 +64,7 @@
<button id="adminConsoleButton">Admin console</button>
</section>
<section id="socialLinks" hidden>
<a class="not-button" href="https://www.facebook.com/workadventurebytcm" target="_blank"><img class="not-button" src="/resources/objects/facebook-icon.png"/></a>
<a class="not-button" href="https://www.facebook.com/workadventure.WA" target="_blank"><img class="not-button" src="/resources/objects/facebook-icon.png"/></a>
<a class="not-button" href="https://twitter.com/Workadventure_" target="_blank"><img class="not-button" src="/resources/objects/twitter-icon.png"/></a>
</section>
</main>

View file

@ -3,17 +3,21 @@
background-color: black;
color: white;
border-radius: 7px;
height: 28px;
width: 34px;
padding: 2px 8px;
}
#menuIcon button img{
width: 14px;
padding-top: 3px;
padding-top: 0;
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
}
#menuIcon section {
margin: 10px;
}
@media only screen and (max-height: 700px) {
#menuIcon section {
margin: 2px;
}
}
</style>
<main id="menuIcon" hidden>
<section>

View file

@ -3,9 +3,9 @@
background: #eceeee;
border: 1px solid #42464b;
border-radius: 6px;
height: 257px;
margin: 20px auto 0;
width: 298px;
width: 50vw;
max-width: 300px;
}
#gameQuality .cautiousText {
font-size: 50%;
@ -33,7 +33,7 @@
color: #696969;
height: 30px;
transition: box-shadow 0.3s;
width: 240px;
width: 100%;
}
#gameQuality section {
margin: 10px;
@ -42,12 +42,11 @@
text-align: center;
}
#gameQuality button {
margin-top: 10px;
margin: 10px;
background-color: black;
color: white;
border-radius: 7px;
padding-bottom: 4px;
width: 60px;
}
#gameQuality button#gameQualityFormCancel {
background-color: #c7c7c700;
@ -57,7 +56,7 @@
<form id="gameQuality" hidden>
<section>
<h3>Game quality</h3>
<h5>Game quality</h3>
<p class="cautiousText">(Editing these settings will restart the game)</p>
<select id="select-game-quality">
<option value="120">High video quality (120 fps)</option>
@ -67,7 +66,7 @@
</select>
</section>
<section>
<h3>Video quality</h3>
<h5>Video quality</h3>
<select id="select-video-quality">
<option value="30">High video quality (30 fps)</option>
<option value="20">Medium video quality (20 fps, recommended)</option>

View file

@ -4,8 +4,8 @@
border: 1px solid #42464b;
border-radius: 6px;
margin: 20px auto 0;
width: 298px;
height: 150px;
width: 50vw;
max-width: 400px;
}
#gameShare h1 {
background-image: linear-gradient(top, #f1f3f3, #d4dae0);
@ -40,7 +40,7 @@
margin: 0;
}
#gameShare button {
margin-top: 10px;
margin: 10px;
background-color: black;
color: white;
border-radius: 7px;
@ -66,7 +66,7 @@
}
#gameShare section p{
font-size: 8px;
margin: 0px 70px;
margin: 0;
}
#gameShare section p.err{
color: red;

View file

@ -1,103 +0,0 @@
<style>
#helpCameraSettings {
background: #eceeee;
border: 1px solid #42464b;
border-radius: 6px;
margin: 10px auto 0;
width: 400px;
height: 370px;
}
#helpCameraSettings h1 {
background-image: linear-gradient(top, #f1f3f3, #d4dae0);
border-bottom: 1px solid #a6abaf;
border-radius: 6px 6px 0 0;
box-sizing: border-box;
color: #727678;
display: block;
height: 43px;
padding-top: 10px;
margin: 0;
text-align: center;
text-shadow: 0 -1px 0 rgba(0,0,0,0.2), 0 1px 0 #fff;
}
#helpCameraSettings input {
font-size: 70%;
background: linear-gradient(top, #d6d7d7, #dee0e0);
border: 1px solid #a1a3a3;
border-radius: 4px;
box-shadow: 0 1px #fff;
box-sizing: border-box;
color: #696969;
height: 30px;
transition: box-shadow 0.3s;
width: 100%;
}
#helpCameraSettings section {
margin: 10px;
}
#helpCameraSettings section.action{
text-align: center;
margin: 0;
}
#helpCameraSettings button {
margin-top: 10px;
background-color: black;
color: white;
border-radius: 7px;
padding-bottom: 4px;
}
#helpCameraSettings button#helpCameraSettingsFormCancel {
background-color: #c7c7c700;
color: #292929;
}
#helpCameraSettings section a{
text-align: center;
font-size: 12px;
margin: 0 6px;
color: black;
}
#helpCameraSettings section h6,
#helpCameraSettings section h5{
margin: 1px;
}
#helpCameraSettings section.text-center{
text-align: center;
}
#helpCameraSettings section p{
font-size: 8px;
margin: 0px 20px;
}
#helpCameraSettings section p.err{
color: #ff0000;
}
#helpCameraSettings section ul{
margin: 6px;
}
#helpCameraSettings section li{
text-align: left;
font-size: 8px;
}
#helpCameraSettings section img {
width: 200px;
margin-top: 10px;
}
</style>
<form id="helpCameraSettings" hidden>
<section class="text-center">
<h5>Camera/Microphone access needed</h5>
<p class="err" id="permissionError">Permission denied</p>
<p class="info">You must allow camera and microphone access in your browser.</p>
<ul>
<li>Please click on the lock or camera symbol on the side of the URL in the address bar. Here you can grant "always allow" access to your input devices.</li>
<li>Please ensure that you have a camera AND microphone plugged into your computer.</li>
</ul>
<p class="info">Once you've followed these steps, please refresh this page.</p>
<p>If you prefer to continue without allowing camera and microphone access, click on Continue</p>
<p id='browserHelpSetting'></p>
</section>
<section class="action">
<button type="submit" id="helpCameraSettingsFormRefresh">Refresh</button>
<button type="submit" id="helpCameraSettingsFormContinue">Continue</button>
</section>
</form>

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg id="i-fullscreen-exit" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="#FFF" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M4 12 L12 12 12 4 M20 4 L20 12 28 12 M4 20 L12 20 12 28 M28 20 L20 20 20 28" />
</svg>

After

Width:  |  Height:  |  Size: 329 B

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg id="i-fullscreen" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M4 12 L4 4 12 4 M20 4 L28 4 28 12 M4 20 L4 28 12 28 M28 20 L28 28 20 28" />
</svg>

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -1,44 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 480" style="enable-background:new 0 0 512 480;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g>
<path class="st0" d="M411.1,384.2c-12.2,0-24.3,0-36.5,0C259.6,257.6,144.5,130.9,29.5,4.3c11.4-0.8,22.9-1.6,34.3-2.4
C179.6,129.3,295.3,256.8,411.1,384.2z"/>
<g>
<path class="st0" d="M352,152.5c-8.8-8.7-34.2-31.6-74.5-38.1c-32.3-5.2-58.1,2.7-70,7.3C170.9,81.5,134.3,41.3,97.8,1
C220.4,1,343,1,465.6,1C427.7,51.5,389.8,102,352,152.5z"/>
<path class="st0" d="M511.5,338.3c0,4.7-0.8,12.2-4.7,20.2c-1.2,2.4-3.4,6.3-7.1,10.5c-4,4.4-7.9,7.1-10.2,8.5
c-5.6,3.5-10.7,4.9-13.5,5.6c-3.8,0.9-6.7,1-10.2,1.2c-3.6,0.2-5.3,0-13.1,0c-3,0-5.4,0-7,0C414.5,307,383.2,229.8,352,152.5
C402.9,62.7,448.7,1,465.6,1c7.5,0,14.3,2.3,14.3,2.3c14.5,4.8,22.3,15.8,23.6,17.8c7.4,10.8,8,21.7,8,25.9
C511.5,144.1,511.5,241.2,511.5,338.3z"/>
<path class="st0" d="M312.5,192c-5.2-5.2-15.6-14.1-31.4-19.4c-12.8-4.2-24-4.3-30.9-3.8c-6.2-7.1-12.4-14.2-18.6-21.3
c10.3-2.4,36.5-6.8,65.1,5.3c15.3,6.5,26.1,15.5,32.7,22.2C323.7,180.8,318.1,186.4,312.5,192z"/>
<path class="st0" d="M329.4,175.1c38.8,69.7,77.6,139.4,116.4,209.1c-50.3-55.4-100.6-110.8-151-166.2c6.9,2.9,14.9,0.7,19.2-5.2
c4.6-6.2,4-15.1-1.6-20.8C318.1,186.4,323.7,180.8,329.4,175.1z"/>
<path class="st0" d="M445.8,384.2L445.8,384.2c-38.8-69.7-77.6-139.4-116.4-209.1c5.3,4.9,12.9,6,18.9,2.7
c7.8-4.2,8.3-13.4,8.3-13.8C386.4,237.4,416.1,310.8,445.8,384.2z"/>
</g>
<path class="st0" d="M162.2,150.4C108.3,213,54.4,275.7,0.5,338.3c0-97.1,0-194.3,0-291.4c0-4,0.6-15.1,8.1-26
C16,10.2,25.7,5.8,29.5,4.3C73.7,53,118,101.7,162.2,150.4z"/>
<path class="st0" d="M199.5,192c-5.3-6-10.6-12-15.8-18C122.6,228.8,61.6,283.6,0.5,338.3c0,4.1,0.6,15.5,8.6,26.7
c1.7,2.4,9.6,12.9,24.1,17.2c5.3,1.6,10,1.9,13.1,1.9C97.5,320.2,148.5,256.1,199.5,192z"/>
<path class="st0" d="M84.7,384.2c-12.7,0-25.5,0-38.2,0c58.2-56.2,116.5-112.5,174.7-168.7c8.3,9.1,16.6,18.2,24.9,27.2
c-2.2,1.1-5.5,3-8.4,6.4c-3.1,3.7-4.2,7.3-4.4,7.8C231.3,262.4,194.5,295.2,84.7,384.2z"/>
<path class="st0" d="M46.4,384.2c-15.3-15.3-30.6-30.6-45.9-45.9C52.8,277.5,105.1,216.8,157.4,156c-3.7,6.7-2.2,15.1,3.5,20
c5.4,4.6,13.3,5.1,19.4,1C135.7,246.1,91.1,315.1,46.4,384.2z"/>
<path class="st0" d="M49.6,384.3c-1.1,0-2.1,0-3.2-0.1c50.1-62.9,100.2-125.8,150.3-188.7c-3.5,6.6-2.1,14.8,3.4,19.7
c5.8,5.2,14.8,5.3,21,0.3C164,271.7,106.8,328,49.6,384.3z"/>
<path class="st0" d="M374.6,384.2c-96.6,0-193.3,0-289.9,0C16.5,308,1.3,223,28.5,194.5C57,164.7,150.6,177,233.3,256.9
c-3.9,11.8,2,24.8,13.3,29.6c11,4.7,24,0.4,30.2-10C309.3,312.4,342,348.3,374.6,384.2z"/>
<path class="st0" d="M219.7,226.7"/>
<path class="st0" d="M368.9,480c-74.9,0-149.8,0-224.7,0c-8.8,0-16-7.2-16-16c0-8.8,7.2-16,16-16c74.9,0,149.9,0,224.8,0.1
c8.3,0.7,14.7,7.6,14.7,15.9C383.7,472.3,377.2,479.3,368.9,480z"/>
<rect x="208.1" y="384.2" class="st0" width="31.9" height="63.9"/>
<rect x="272" y="384.2" class="st0" width="32" height="63.9"/>
<path class="st0" d="M410.3,395.5"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 480" style="enable-background:new 0 0 512 480;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<path class="st0" d="M466,0H46C20.6,0,0,20.6,0,46v292c0,25.4,20.6,46,46,46h162v64h-64c-8.8,0-16,7.2-16,16s7.2,16,16,16h224
c8.8,0,16-7.2,16-16s-7.2-16-16-16h-64v-64h162c25.4,0,46-20.6,46-46V46C512,20.6,491.4,0,466,0z M232,264c0-13.3,10.7-24,24-24
c13.3,0,24,10.7,24,24s-10.7,24-24,24C242.7,288,232,277.3,232,264z M272,448h-32v-64h32V448z M312.6,214.1
c-6.2,6.2-16.4,6.2-22.6,0c-18.7-18.8-49.1-18.8-67.9,0c0,0,0,0,0,0c-6.4,6.1-16.5,5.8-22.6-0.6c-5.9-6.2-5.9-15.9,0-22
c31.2-31.2,81.9-31.2,113.1,0c0,0,0,0,0,0C318.8,197.7,318.8,207.8,312.6,214.1z M352.2,174.5c-6.2,6.2-16.4,6.3-22.6,0c0,0,0,0,0,0
c-40.6-40.6-106.4-40.6-147.1,0c-6.2,6.3-16.4,6.3-22.6,0c-6.3-6.2-6.3-16.4,0-22.6c53.1-53.1,139.2-53.1,192.3,0c0,0,0,0,0,0
C358.4,158.1,358.4,168.2,352.2,174.5C352.2,174.5,352.2,174.5,352.2,174.5L352.2,174.5z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 B

After

Width:  |  Height:  |  Size: 4.9 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 969 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -1,170 +0,0 @@
/* A potentially shared website could appear in an iframe in the cowebsite space. */
#cowebsite {
position: fixed;
transition: transform 0.5s;
background-color: white;
&.loading {
background-color: gray;
}
main {
iframe {
width: 100%;
height: 100%;
}
}
aside {
background: gray;
align-items: center;
display: flex;
img {
margin: 3px;
pointer-events: none;
height: 20px;
}
}
.top-right-btn{
position: absolute;
background: none;
border: none;
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
img {
height: 20px;
background-color: rgba(0,0.0,0,0.3);
padding: 5px;
border-radius: 3px;
}
img:hover {
background-color: rgba(0,0,0,0.4);
}
}
}
@media (min-aspect-ratio: 1/1) {
#cowebsite {
right: 0;
top: 0;
width: 50%;
height: 100vh;
display: flex;
&.loading {
transform: translateX(90%);
}
&.hidden {
transform: translateX(100%);
}
main {
width: 100%;
}
aside {
width: 30px;
cursor: ew-resize;
img {
cursor: ew-resize;
transform: rotate(90deg);
}
}
.top-right-btn{
top: 10px;
right: -100px;
animation: right .2s ease;
img {
right: 15px;
}
}
#cowebsite-close {
right: -140px;
}
#cowebsite-fullscreen {
right: -100px;
}
}
#cowebsite:hover {
#cowebsite-close{
right: 10px;
}
#cowebsite-fullscreen{
right: 45px;
}
}
}
@media (max-aspect-ratio: 1/1) {
#cowebsite {
left: 0;
bottom: 0;
width: 100%;
height: 50%;
display: flex;
flex-direction: column;
&.loading {
transform: translateY(90%);
}
&.hidden {
transform: translateY(100%);
}
main {
height: 100%;
}
aside {
height: 30px;
cursor: ns-resize;
flex-direction: column;
img {
cursor: ns-resize;
}
}
.top-right-btn{
top: 10px;
right: -100px;
animation: right .2s ease;
img {
right: 15px;
}
}
#cowebsite-close {
right: -140px;
}
#cowebsite-fullscreen {
right: -100px;
}
}
#cowebsite:hover {
#cowebsite-close{
right: 10px;
}
#cowebsite-fullscreen{
right: 45px;
}
}
}

View file

@ -1,2 +0,0 @@
@import "cowebsite.scss";
@import "style.css";

View file

@ -1,41 +1,149 @@
{
"name": "App",
"icons": [
{
"src": "\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
"short_name": "WA",
"name": "WorkAdventure",
"icons": [
{
"src": "/static/images/favicons/apple-icon-57x57.png",
"sizes": "57x57",
"type": "image\/png"
},
{
"src": "/static/images/favicons/apple-icon-60x60.png",
"sizes": "60x60",
"type": "image\/png"
},
{
"src": "/static/images/favicons/apple-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png"
},
{
"src": "/static/images/favicons/apple-icon-76x76.png",
"sizes": "76x76",
"type": "image\/png"
},
{
"src": "/static/images/favicons/apple-icon-114x114.png",
"sizes": "114x114",
"type": "image\/png"
},
{
"src": "/static/images/favicons/apple-icon-120x120.png",
"sizes": "120x120",
"type": "image\/png"
},
{
"src": "/static/images/favicons/apple-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png"
},
{
"src": "/static/images/favicons/apple-icon-152x152.png",
"sizes": "152x152",
"type": "image\/png"
},
{
"src": "/static/images/favicons/apple-icon-180x180.png",
"sizes": "180x180",
"type": "image\/png"
},
{
"src": "/static/images/favicons/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "/static/images/favicons/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "/static/images/favicons/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "/static/images/favicons/favicon-16x16.png",
"sizes": "16x16",
"type": "image\/png",
"density": "1"
},
{
"src": "/static/images/favicons/favicon-32x32.png",
"sizes": "32x32",
"type": "image\/png",
"density": "1.5"
},
{
"src": "/static/images/favicons/favicon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "/static/images/favicons/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "1"
},
{
"src": "/static/images/favicons/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1"
},
{
"src": "/static/images/favicons/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "/static/images/favicons/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "/static/images/favicons/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "/static/images/favicons/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
],
"start_url": "/",
"background_color": "#000000",
"display_override": ["window-control-overlay", "minimal-ui"],
"display": "standalone",
"scope": "/",
"theme_color": "#000000",
"shortcuts": [
{
"name": "WorkAdventures",
"short_name": "WA",
"description": "WorkAdventure application",
"url": "/",
"icons": [{ "src": "/static/images/favicons/android-icon-192x192.png", "sizes": "192x192" }]
}
],
"description": "WorkAdventure application",
"screenshots": [],
"related_applications": [{
"platform": "web",
"url": "https://workadventu.re"
}, {
"platform": "play",
"url": "https://play.workadventu.re"
}]
}

8393
front/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -4,45 +4,74 @@
"main": "index.js",
"license": "SEE LICENSE IN LICENSE.txt",
"devDependencies": {
"@tsconfig/svelte": "^1.0.10",
"@types/google-protobuf": "^3.7.3",
"@types/jasmine": "^3.5.10",
"@types/mini-css-extract-plugin": "^1.4.3",
"@types/node": "^15.3.0",
"@types/quill": "^1.3.7",
"@typescript-eslint/eslint-plugin": "^2.26.0",
"@typescript-eslint/parser": "^2.26.0",
"css-loader": "^5.1.3",
"eslint": "^6.8.0",
"html-webpack-plugin": "^4.3.0",
"@types/webpack-dev-server": "^3.11.4",
"@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0",
"css-loader": "^5.2.4",
"eslint": "^7.26.0",
"fork-ts-checker-webpack-plugin": "^6.2.9",
"html-webpack-plugin": "^5.3.1",
"jasmine": "^3.5.0",
"mini-css-extract-plugin": "^1.3.9",
"sass": "^1.32.8",
"sass-loader": "10.1.1",
"ts-loader": "^6.2.2",
"ts-node": "^8.10.2",
"typescript": "^3.8.3",
"webpack": "^4.42.1",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3",
"webpack-merge": "^4.2.2"
"lint-staged": "^11.0.0",
"mini-css-extract-plugin": "^1.6.0",
"node-polyfill-webpack-plugin": "^1.1.2",
"npm-run-all": "^4.1.5",
"prettier": "^2.3.1",
"sass": "^1.32.12",
"sass-loader": "^11.1.0",
"svelte": "^3.38.2",
"svelte-check": "^2.1.0",
"svelte-loader": "^3.1.1",
"svelte-preprocess": "^4.7.3",
"ts-loader": "^9.1.2",
"ts-node": "^9.1.1",
"tsconfig-paths": "^3.9.0",
"typescript": "^4.2.4",
"webpack": "^5.37.0",
"webpack-cli": "^4.7.0",
"webpack-dev-server": "^3.11.2"
},
"dependencies": {
"@fontsource/press-start-2p": "^4.3.0",
"@types/simple-peer": "^9.6.0",
"@types/socket.io-client": "^1.4.32",
"axios": "^0.21.1",
"cross-env": "^7.0.3",
"generic-type-guard": "^3.2.0",
"google-protobuf": "^3.13.0",
"phaser": "3.24.1",
"phaser": "^3.54.0",
"phaser-animated-tiles": "workadventure/phaser-animated-tiles#da68bbededd605925621dd4f03bd27e69284b254",
"phaser3-rex-plugins": "^1.1.42",
"queue-typescript": "^1.0.1",
"quill": "^1.3.7",
"quill": "1.3.6",
"rxjs": "^6.6.3",
"simple-peer": "^9.6.2",
"socket.io-client": "^2.3.0",
"webpack-require-http": "^0.4.3"
"standardized-audio-context": "^25.2.4"
},
"scripts": {
"start": "webpack-dev-server --open",
"build": "webpack --config webpack.prod.js",
"test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
"start": "run-p templater serve svelte-check-watch",
"templater": "cross-env ./templater.sh",
"serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open",
"build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack",
"test": "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"
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts",
"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}'"
},
"lint-staged": {
"*.ts": [
"prettier --write"
]
}
}

View file

@ -0,0 +1 @@
iframe_api.d.ts

View file

@ -0,0 +1,27 @@
<h1 align="center">WorkAdventure - IFrame API typings for Typescript</h1>
<p align="center">This package contains Typescript typings for <a href="https://workadventu.re/map-building/scripting">WorkAdventure's map scripting API</a></p>
<hr/>
[WorkAdventure](https://workadventu.re) comes with a scripting API. Using this API, you can add some intelligence to your map.
You use this API by loading an external script directly from WorkAdventure (at https://play.workadventu.re/iframe_api.js), or this script is loaded
for you if you are using the "script" property of a map.
This project contains Typescript typings for the `WA` object provided by this script.
## Usage
This package is only useful if you are using Typescript to script your WorkAdventure maps.
## Download & Installation
```shell
$ npm install @workadventure/iframe-api-typings
```
or
```shell
$ yarn add @workadventure/iframe-api-typings
```

View file

@ -0,0 +1 @@
// This file is voluntarily empty.

View file

@ -0,0 +1,13 @@
{
"name": "@workadventure/iframe-api-typings",
"version": "VERSION_PLACEHOLDER",
"description": "Typescript typings for WorkAdventure iFrame API",
"main": "iframe_api.js",
"types": "iframe_api.d.ts",
"repository": "https://github.com/thecodingmachine/workadventure/",
"author": "David Négrier <d.negrier@thecodingmachine.com>",
"license": "MIT",
"publishConfig": {
"access": "public"
}
}

View file

@ -1,394 +0,0 @@
import {HtmlUtils} from "../WebRtc/HtmlUtils";
import {UserInputManager} from "../Phaser/UserInput/UserInputManager";
import {RoomConnection} from "../Connexion/RoomConnection";
import {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels";
import {ADMIN_URL} from "../Enum/EnvironmentVariable";
import {AdminMessageEventTypes} from "../Connexion/AdminMessagesService";
export const CLASS_CONSOLE_MESSAGE = 'main-console';
export const INPUT_CONSOLE_MESSAGE = 'input-send-text';
export const UPLOAD_CONSOLE_MESSAGE = 'input-upload-music';
export const INPUT_TYPE_CONSOLE = 'input-type';
export const VIDEO_QUALITY_SELECT = 'select-video-quality';
export const AUDIO_TYPE = AdminMessageEventTypes.audio;
export const MESSAGE_TYPE = AdminMessageEventTypes.admin;
interface EventTargetFiles extends EventTarget {
files: Array<File>;
}
/**
* @deprecated
*/
export class ConsoleGlobalMessageManager {
private readonly divMainConsole: HTMLDivElement;
private readonly divMessageConsole: HTMLDivElement;
//private readonly divSettingConsole: HTMLDivElement;
private readonly buttonMainConsole: HTMLDivElement;
private readonly buttonSendMainConsole: HTMLImageElement;
//private readonly buttonAdminMainConsole: HTMLImageElement;
//private readonly buttonSettingsMainConsole: HTMLImageElement;
private activeConsole: boolean = false;
private activeMessage: boolean = false;
private activeSetting: boolean = false;
private userInputManager!: UserInputManager;
private static cssLoaded: boolean = false;
constructor(private Connection: RoomConnection, userInputManager : UserInputManager, private isAdmin: Boolean) {
this.buttonMainConsole = document.createElement('div');
this.buttonMainConsole.classList.add('console');
this.buttonMainConsole.hidden = true;
this.divMainConsole = document.createElement('div');
this.divMainConsole.className = CLASS_CONSOLE_MESSAGE;
this.divMessageConsole = document.createElement('div');
this.divMessageConsole.className = 'message';
//this.divSettingConsole = document.createElement('div');
//this.divSettingConsole.className = 'setting';
this.buttonSendMainConsole = document.createElement('img');
this.buttonSendMainConsole.id = 'btn-send-message';
//this.buttonSettingsMainConsole = document.createElement('img');
//this.buttonAdminMainConsole = document.createElement('img');
this.userInputManager = userInputManager;
this.initialise();
}
initialise() {
for (const elem of document.getElementsByClassName(CLASS_CONSOLE_MESSAGE)) {
elem.remove();
}
const typeConsole = document.createElement('input');
typeConsole.id = INPUT_TYPE_CONSOLE;
typeConsole.value = MESSAGE_TYPE;
typeConsole.type = 'hidden';
const menu = document.createElement('div');
menu.classList.add('menu')
const textMessage = document.createElement('span');
textMessage.innerText = "Message";
textMessage.classList.add('active');
textMessage.addEventListener('click', () => {
textMessage.classList.add('active');
const messageSection = HtmlUtils.getElementByIdOrFail<HTMLInputElement>(this.getSectionId(INPUT_CONSOLE_MESSAGE));
messageSection.classList.add('active');
textAudio.classList.remove('active');
const audioSection = HtmlUtils.getElementByIdOrFail<HTMLInputElement>(this.getSectionId(UPLOAD_CONSOLE_MESSAGE));
audioSection.classList.remove('active');
typeConsole.value = MESSAGE_TYPE;
});
menu.appendChild(textMessage);
const textAudio = document.createElement('span');
textAudio.innerText = "Audio";
textAudio.addEventListener('click', () => {
textAudio.classList.add('active');
const audioSection = HtmlUtils.getElementByIdOrFail<HTMLInputElement>(this.getSectionId(UPLOAD_CONSOLE_MESSAGE));
audioSection.classList.add('active');
textMessage.classList.remove('active');
const messageSection = HtmlUtils.getElementByIdOrFail<HTMLInputElement>(this.getSectionId(INPUT_CONSOLE_MESSAGE));
messageSection.classList.remove('active');
typeConsole.value = AUDIO_TYPE;
});
menu.appendChild(textMessage);
menu.appendChild(textAudio);
this.divMessageConsole.appendChild(menu);
this.buttonSendMainConsole.src = 'resources/logos/send-yellow.svg';
this.buttonSendMainConsole.addEventListener('click', () => {
if(this.activeMessage){
this.disabledMessageConsole();
}else{
this.activeMessageConsole();
}
});
/*this.buttonAdminMainConsole.src = 'resources/logos/setting-yellow.svg';
this.buttonAdminMainConsole.addEventListener('click', () => {
window.open(ADMIN_URL, '_blank');
});*/
/*this.buttonSettingsMainConsole.src = 'resources/logos/monitor-yellow.svg';
this.buttonSettingsMainConsole.addEventListener('click', () => {
if(this.activeSetting){
this.disabledSettingConsole();
}else{
this.activeSettingConsole();
}
});*/
this.divMessageConsole.appendChild(typeConsole);
/*if(this.isAdmin) {
this.buttonMainConsole.appendChild(this.buttonSendMainConsole);
//this.buttonMainConsole.appendChild(this.buttonAdminMainConsole);
}*/
this.createTextMessagePart();
this.createUploadAudioPart();
//this.buttonMainConsole.appendChild(this.buttonSettingsMainConsole);
this.divMainConsole.appendChild(this.buttonMainConsole);
this.divMainConsole.appendChild(this.divMessageConsole);
//this.divMainConsole.appendChild(this.divSettingConsole);
const mainSectionDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
mainSectionDiv.appendChild(this.divMainConsole);
}
createTextMessagePart(){
const div = document.createElement('div');
div.id = INPUT_CONSOLE_MESSAGE
const buttonSend = document.createElement('button');
buttonSend.innerText = 'Send';
buttonSend.classList.add('btn');
buttonSend.addEventListener('click', (event: MouseEvent) => {
this.sendMessage();
this.disabledMessageConsole();
});
const buttonDiv = document.createElement('div');
buttonDiv.classList.add('btn-action');
buttonDiv.appendChild(buttonSend)
const section = document.createElement('section');
section.id = this.getSectionId(INPUT_CONSOLE_MESSAGE);
section.classList.add('active');
section.appendChild(div);
section.appendChild(buttonDiv);
this.divMessageConsole.appendChild(section);
(async () => {
// Start loading CSS
const cssPromise = ConsoleGlobalMessageManager.loadCss();
// Import quill
const Quill:any = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any
// Wait for CSS to be loaded
await cssPromise;
const toolbarOptions = [
['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
[{'size': ['small', false, 'large', 'huge']}], // custom dropdown
[{'header': [1, 2, 3, 4, 5, 6, false]}],
[{'color': []}, {'background': []}], // dropdown with defaults from theme
[{'font': []}],
[{'align': []}],
['clean'],
['link', 'image', 'video']
// remove formatting button
];
new Quill(`#${INPUT_CONSOLE_MESSAGE}`, {
theme: 'snow',
modules: {
toolbar: toolbarOptions
},
});
})();
}
createUploadAudioPart(){
const div = document.createElement('div');
div.classList.add('upload');
const label = document.createElement('label');
label.setAttribute('for', UPLOAD_CONSOLE_MESSAGE);
const img = document.createElement('img');
img.setAttribute('for', UPLOAD_CONSOLE_MESSAGE);
img.src = 'resources/logos/music-file.svg';
const input = document.createElement('input');
input.type = 'file';
input.id = UPLOAD_CONSOLE_MESSAGE
input.addEventListener('input', (e: Event) => {
if(!e.target){
return;
}
const eventTarget : EventTargetFiles = (e.target as EventTargetFiles);
if(!eventTarget || !eventTarget.files || eventTarget.files.length === 0){
return;
}
const file : File = eventTarget.files[0];
if(!file){
return;
}
try {
HtmlUtils.removeElementByIdOrFail('audi-message-filename');
}catch (err) {
console.error(err)
}
const p = document.createElement('p');
p.id = 'audi-message-filename';
p.innerText = `${file.name} : ${this.getFileSize(file.size)}`;
label.appendChild(p);
});
label.appendChild(img);
div.appendChild(label);
div.appendChild(input);
const buttonSend = document.createElement('button');
buttonSend.innerText = 'Send';
buttonSend.classList.add('btn');
buttonSend.addEventListener('click', (event: MouseEvent) => {
this.sendMessage();
this.disabledMessageConsole();
});
const buttonDiv = document.createElement('div');
buttonDiv.classList.add('btn-action');
buttonDiv.appendChild(buttonSend)
const section = document.createElement('section');
section.id = this.getSectionId(UPLOAD_CONSOLE_MESSAGE);
section.appendChild(div);
section.appendChild(buttonDiv);
this.divMessageConsole.appendChild(section);
}
private static loadCss(): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (ConsoleGlobalMessageManager.cssLoaded) {
resolve();
return;
}
const fileref = document.createElement("link")
fileref.setAttribute("rel", "stylesheet")
fileref.setAttribute("type", "text/css")
fileref.setAttribute("href", "https://cdn.quilljs.com/1.3.7/quill.snow.css");
document.getElementsByTagName("head")[0].appendChild(fileref);
ConsoleGlobalMessageManager.cssLoaded = true;
fileref.onload = () => {
resolve();
}
fileref.onerror = () => {
reject();
}
});
}
sendMessage(){
const inputType = HtmlUtils.getElementByIdOrFail<HTMLInputElement>(INPUT_TYPE_CONSOLE);
if(AUDIO_TYPE !== inputType.value && MESSAGE_TYPE !== inputType.value){
throw "Error event type";
}
if(AUDIO_TYPE === inputType.value){
return this.sendAudioMessage();
}
return this.sendTextMessage();
}
private sendTextMessage(){
const elements = document.getElementsByClassName('ql-editor');
const quillEditor = elements.item(0);
if(!quillEditor){
throw "Error get quill node";
}
const GlobalMessage : PlayGlobalMessageInterface = {
id: "1", // FIXME: use another ID?
message: quillEditor.innerHTML,
type: MESSAGE_TYPE
};
quillEditor.innerHTML = '';
this.Connection.emitGlobalMessage(GlobalMessage);
}
private async sendAudioMessage(){
const inputAudio = HtmlUtils.getElementByIdOrFail<HTMLInputElement>(UPLOAD_CONSOLE_MESSAGE);
const selectedFile = inputAudio.files ? inputAudio.files[0] : null;
if(!selectedFile){
throw 'no file selected';
}
const fd = new FormData();
fd.append('file', selectedFile);
const res = await this.Connection.uploadAudio(fd);
const GlobalMessage : PlayGlobalMessageInterface = {
id: (res as {id: string}).id,
message: (res as {path: string}).path,
type: AUDIO_TYPE
};
inputAudio.value = '';
try {
HtmlUtils.removeElementByIdOrFail('audi-message-filename');
}catch (err) {
console.error(err);
}
this.Connection.emitGlobalMessage(GlobalMessage);
}
active(){
this.userInputManager.clearAllKeys();
this.divMainConsole.style.top = '0';
this.activeConsole = true;
}
disabled(){
this.userInputManager.initKeyBoardEvent();
this.activeConsole = false;
this.divMainConsole.style.top = '-80%';
}
activeMessageConsole(){
if(!this.isAdmin){
throw "User is not admin";
}
if(this.activeMessage){
this.disabledMessageConsole();
return;
}
this.activeMessage = true;
this.active();
this.divMessageConsole.classList.add('active');
this.buttonMainConsole.hidden = false;
this.buttonSendMainConsole.classList.add('active');
//if button not
try{
HtmlUtils.getElementByIdOrFail('btn-send-message');
}catch (e) {
this.buttonMainConsole.appendChild(this.buttonSendMainConsole);
}
}
disabledMessageConsole(){
this.activeMessage = false;
this.disabled();
this.buttonMainConsole.hidden = true;
this.divMessageConsole.classList.remove('active');
this.buttonSendMainConsole.classList.remove('active');
}
private getSectionId(id: string) : string {
return `section-${id}`;
}
private getFileSize(number: number) :string {
if (number < 1024) {
return number + 'bytes';
} else if (number >= 1024 && number < 1048576) {
return (number / 1024).toFixed(1) + 'KB';
} else if (number >= 1048576) {
return (number / 1048576).toFixed(1) + 'MB';
}else{
return '';
}
}
}

View file

@ -1,8 +1,10 @@
import {HtmlUtils} from "./../WebRtc/HtmlUtils";
import {AUDIO_TYPE, MESSAGE_TYPE} from "./ConsoleGlobalMessageManager";
import {API_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable";
import {RoomConnection} from "../Connexion/RoomConnection";
import {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels";
import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable";
import type {RoomConnection} from "../Connexion/RoomConnection";
import type {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels";
import {soundPlayingStore} from "../Stores/SoundPlayingStore";
import {soundManager} from "../Phaser/Game/SoundManager";
import {AdminMessageEventTypes} from "../Connexion/AdminMessagesService";
export class GlobalMessageManager {
@ -34,54 +36,17 @@ export class GlobalMessageManager {
previousMessage.remove();
}
if(AUDIO_TYPE === message.type){
if(AdminMessageEventTypes.audio === message.type){
this.playAudioMessage(message.id, message.message);
}
if(MESSAGE_TYPE === message.type){
if(AdminMessageEventTypes.admin === message.type){
this.playTextMessage(message.id, message.message);
}
}
private playAudioMessage(messageId : string, urlMessage: string){
//delete previous elements
const previousDivAudio = document.getElementsByClassName('audio-playing');
for(let i = 0; i < previousDivAudio.length; i++){
previousDivAudio[i].remove();
}
//create new element
const divAudio : HTMLDivElement = document.createElement('div');
divAudio.id = `audio-playing-${messageId}`;
divAudio.classList.add('audio-playing');
const imgAudio : HTMLImageElement = document.createElement('img');
imgAudio.src = '/resources/logos/megaphone.svg';
const pAudio : HTMLParagraphElement = document.createElement('p');
pAudio.textContent = 'Message audio'
divAudio.appendChild(imgAudio);
divAudio.appendChild(pAudio);
const mainSectionDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
mainSectionDiv.appendChild(divAudio);
const messageAudio : HTMLAudioElement = document.createElement('audio');
messageAudio.id = this.getHtmlMessageId(messageId);
messageAudio.autoplay = true;
messageAudio.style.display = 'none';
messageAudio.onended = () => {
divAudio.classList.remove('active');
messageAudio.remove();
setTimeout(() => {
divAudio.remove();
}, 1000);
}
messageAudio.onplay = () => {
divAudio.classList.add('active');
}
const messageAudioSource : HTMLSourceElement = document.createElement('source');
messageAudioSource.src = `${UPLOADER_URL}${urlMessage}`;
messageAudio.appendChild(messageAudioSource);
mainSectionDiv.appendChild(messageAudio);
private playAudioMessage(messageId : string, urlMessage: string) {
soundPlayingStore.playSound(UPLOADER_URL + urlMessage);
}
private playTextMessage(messageId : string, htmlMessage: string){

View file

@ -1,4 +1,4 @@
import {TypeMessageInterface} from "./UserMessageManager";
import type {TypeMessageInterface} from "./UserMessageManager";
import {HtmlUtils} from "../WebRtc/HtmlUtils";
let modalTimeOut : NodeJS.Timeout;
@ -44,7 +44,13 @@ export class TypeMessageExt implements TypeMessageInterface{
mainSectionDiv.appendChild(div);
const reportMessageAudio = HtmlUtils.getElementByIdOrFail<HTMLAudioElement>('report-message');
reportMessageAudio.play();
// FIXME: this will fail on iOS
// We should move the sound playing into the GameScene and listen to the event of a report using a store
try {
reportMessageAudio.play();
} catch (e) {
console.error(e);
}
this.nbSecond = this.maxNbSecond;
setTimeout((c) => {
@ -86,4 +92,4 @@ export class Banned extends TypeMessageExt {
showMessage(message: string){
super.showMessage(message, false);
}
}
}

View file

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isButtonClickedEvent =
new tg.IsInterface().withProperties({
popupId: tg.isNumber,
buttonId: tg.isNumber,
}).get();
/**
* A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property.
*/
export type ButtonClickedEvent = tg.GuardedType<typeof isButtonClickedEvent>;

View file

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isChatEvent =
new tg.IsInterface().withProperties({
message: tg.isString,
author: tg.isString,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type ChatEvent = tg.GuardedType<typeof isChatEvent>;

View file

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isClosePopupEvent =
new tg.IsInterface().withProperties({
popupId: tg.isNumber,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type ClosePopupEvent = tg.GuardedType<typeof isClosePopupEvent>;

View file

@ -0,0 +1,13 @@
import * as tg from "generic-type-guard";
export const isDataLayerEvent =
new tg.IsInterface().withProperties({
data: tg.isObject
}).get();
/**
* A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers
*/
export type DataLayerEvent = tg.GuardedType<typeof isDataLayerEvent>;

View file

@ -0,0 +1,10 @@
import * as tg from "generic-type-guard";
export const isEnterLeaveEvent =
new tg.IsInterface().withProperties({
name: tg.isString,
}).get();
/**
* A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property.
*/
export type EnterLeaveEvent = tg.GuardedType<typeof isEnterLeaveEvent>;

View file

@ -0,0 +1,15 @@
import * as tg from "generic-type-guard";
export const isGameStateEvent =
new tg.IsInterface().withProperties({
roomId: tg.isString,
mapUrl: tg.isString,
nickname: tg.isUnion(tg.isString, tg.isNull),
uuid: tg.isUnion(tg.isString, tg.isUndefined),
startLayerName: tg.isUnion(tg.isString, tg.isNull),
tags : tg.isArray(tg.isString),
}).get();
/**
* A message sent from the game to the iFrame when the gameState is received by the script
*/
export type GameStateEvent = tg.GuardedType<typeof isGameStateEvent>;

View file

@ -0,0 +1,13 @@
import * as tg from "generic-type-guard";
export const isGoToPageEvent =
new tg.IsInterface().withProperties({
url: tg.isString,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type GoToPageEvent = tg.GuardedType<typeof isGoToPageEvent>;

View file

@ -0,0 +1,19 @@
import * as tg from "generic-type-guard";
export const isHasPlayerMovedEvent =
new tg.IsInterface().withProperties({
direction: tg.isElementOf('right', 'left', 'up', 'down'),
moving: tg.isBoolean,
x: tg.isNumber,
y: tg.isNumber
}).get();
/**
* A message sent from the game to the iFrame to notify a movement from the current player.
*/
export type HasPlayerMovedEvent = tg.GuardedType<typeof isHasPlayerMovedEvent>;
export type HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void

View file

@ -0,0 +1,78 @@
import type { GameStateEvent } from './GameStateEvent';
import type { ButtonClickedEvent } from './ButtonClickedEvent';
import type { ChatEvent } from './ChatEvent';
import type { ClosePopupEvent } from './ClosePopupEvent';
import type { EnterLeaveEvent } from './EnterLeaveEvent';
import type { GoToPageEvent } from './GoToPageEvent';
import type { LoadPageEvent } from './LoadPageEvent';
import type { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent';
import type { OpenPopupEvent } from './OpenPopupEvent';
import type { OpenTabEvent } from './OpenTabEvent';
import type { UserInputChatEvent } from './UserInputChatEvent';
import type { DataLayerEvent } from "./DataLayerEvent";
import type { LayerEvent } from './LayerEvent';
import type { SetPropertyEvent } from "./setPropertyEvent";
import type { LoadSoundEvent } from "./LoadSoundEvent";
import type { PlaySoundEvent } from "./PlaySoundEvent";
import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
import type { MenuItemRegisterEvent } from './ui/MenuItemRegisterEvent';
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
export interface TypedMessageEvent<T> extends MessageEvent {
data: T
}
export type IframeEventMap = {
//getState: GameStateEvent,
// updateTile: UpdateTileEvent
loadPage: LoadPageEvent
chat: ChatEvent,
openPopup: OpenPopupEvent
closePopup: ClosePopupEvent
openTab: OpenTabEvent
goToPage: GoToPageEvent
openCoWebSite: OpenCoWebSiteEvent
closeCoWebSite: null
disablePlayerControls: null
restorePlayerControls: null
displayBubble: null
removeBubble: null
onPlayerMove: undefined
showLayer: LayerEvent
hideLayer: LayerEvent
setProperty: SetPropertyEvent
getDataLayer: undefined
loadSound: LoadSoundEvent
playSound: PlaySoundEvent
stopSound: null,
getState: undefined,
registerMenuCommand: MenuItemRegisterEvent
}
export interface IframeEvent<T extends keyof IframeEventMap> {
type: T;
data: IframeEventMap[T];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeEventWrapper = (event: any): event is IframeEvent<keyof IframeEventMap> => typeof event.type === 'string';
export interface IframeResponseEventMap {
userInputChat: UserInputChatEvent
enterEvent: EnterLeaveEvent
leaveEvent: EnterLeaveEvent
buttonClickedEvent: ButtonClickedEvent
gameState: GameStateEvent
hasPlayerMoved: HasPlayerMovedEvent
dataLayer: DataLayerEvent
menuItemClicked: MenuItemClickedEvent
}
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
type: T;
data: IframeResponseEventMap[T];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeResponseEventWrapper = (event: { type?: string }): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === 'string';

View file

@ -0,0 +1,10 @@
import * as tg from "generic-type-guard";
export const isLayerEvent =
new tg.IsInterface().withProperties({
name: tg.isString,
}).get();
/**
* A message sent from the iFrame to the game to show/hide a layer.
*/
export type LayerEvent = tg.GuardedType<typeof isLayerEvent>;

View file

@ -0,0 +1,13 @@
import * as tg from "generic-type-guard";
export const isLoadPageEvent =
new tg.IsInterface().withProperties({
url: tg.isString,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type LoadPageEvent = tg.GuardedType<typeof isLoadPageEvent>;

View file

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isLoadSoundEvent =
new tg.IsInterface().withProperties({
url: tg.isString,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type LoadSoundEvent = tg.GuardedType<typeof isLoadSoundEvent>;

View file

@ -0,0 +1,13 @@
import * as tg from "generic-type-guard";
export const isOpenCoWebsite =
new tg.IsInterface().withProperties({
url: tg.isString,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type OpenCoWebSiteEvent = tg.GuardedType<typeof isOpenCoWebsite>;

View file

@ -0,0 +1,20 @@
import * as tg from "generic-type-guard";
const isButtonDescriptor =
new tg.IsInterface().withProperties({
label: tg.isString,
className: tg.isOptional(tg.isString)
}).get();
export const isOpenPopupEvent =
new tg.IsInterface().withProperties({
popupId: tg.isNumber,
targetObject: tg.isString,
message: tg.isString,
buttons: tg.isArray(isButtonDescriptor)
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type OpenPopupEvent = tg.GuardedType<typeof isOpenPopupEvent>;

View file

@ -0,0 +1,13 @@
import * as tg from "generic-type-guard";
export const isOpenTabEvent =
new tg.IsInterface().withProperties({
url: tg.isString,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type OpenTabEvent = tg.GuardedType<typeof isOpenTabEvent>;

View file

@ -0,0 +1,24 @@
import * as tg from "generic-type-guard";
const isSoundConfig =
new tg.IsInterface().withProperties({
volume: tg.isOptional(tg.isNumber),
loop: tg.isOptional(tg.isBoolean),
mute: tg.isOptional(tg.isBoolean),
rate: tg.isOptional(tg.isNumber),
detune: tg.isOptional(tg.isNumber),
seek: tg.isOptional(tg.isNumber),
delay: tg.isOptional(tg.isNumber)
}).get();
export const isPlaySoundEvent =
new tg.IsInterface().withProperties({
url: tg.isString,
config : tg.isOptional(isSoundConfig),
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type PlaySoundEvent = tg.GuardedType<typeof isPlaySoundEvent>;

View file

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isStopSoundEvent =
new tg.IsInterface().withProperties({
url: tg.isString,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type StopSoundEvent = tg.GuardedType<typeof isStopSoundEvent>;

View file

@ -0,0 +1,10 @@
import * as tg from "generic-type-guard";
export const isUserInputChatEvent =
new tg.IsInterface().withProperties({
message: tg.isString,
}).get();
/**
* A message sent from the game to the iFrame when a user types a message in the chat.
*/
export type UserInputChatEvent = tg.GuardedType<typeof isUserInputChatEvent>;

View file

@ -0,0 +1,12 @@
import * as tg from "generic-type-guard";
export const isSetPropertyEvent =
new tg.IsInterface().withProperties({
layerName: tg.isString,
propertyName: tg.isString,
propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined)))
}).get();
/**
* A message sent from the iFrame to the game to change the value of the property of the layer
*/
export type SetPropertyEvent = tg.GuardedType<typeof isSetPropertyEvent>;

View file

@ -0,0 +1,12 @@
import * as tg from "generic-type-guard";
export const isMenuItemClickedEvent =
new tg.IsInterface().withProperties({
menuItem: tg.isString
}).get();
/**
* A message sent from the game to the iFrame when a menu item is clicked.
*/
export type MenuItemClickedEvent = tg.GuardedType<typeof isMenuItemClickedEvent>;

View file

@ -0,0 +1,25 @@
import * as tg from "generic-type-guard";
import { Subject } from 'rxjs';
export const isMenuItemRegisterEvent =
new tg.IsInterface().withProperties({
menutItem: tg.isString
}).get();
/**
* A message sent from the iFrame to the game to add a new menu item.
*/
export type MenuItemRegisterEvent = tg.GuardedType<typeof isMenuItemRegisterEvent>;
export const isMenuItemRegisterIframeEvent =
new tg.IsInterface().withProperties({
type: tg.isSingletonString("registerMenuCommand"),
data: isMenuItemRegisterEvent
}).get();
const _registerMenuCommandStream: Subject<string> = new Subject();
export const registerMenuCommandStream = _registerMenuCommandStream.asObservable();
export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) {
_registerMenuCommandStream.next(event.menutItem)
}

View file

@ -0,0 +1,357 @@
import {Subject} from "rxjs";
import {ChatEvent, isChatEvent} from "./Events/ChatEvent";
import {HtmlUtils} from "../WebRtc/HtmlUtils";
import type {EnterLeaveEvent} from "./Events/EnterLeaveEvent";
import {isOpenPopupEvent, OpenPopupEvent} from "./Events/OpenPopupEvent";
import {isOpenTabEvent, OpenTabEvent} from "./Events/OpenTabEvent";
import type {ButtonClickedEvent} from "./Events/ButtonClickedEvent";
import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent";
import {scriptUtils} from "./ScriptUtils";
import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent";
import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent";
import {
IframeEvent,
IframeEventMap,
IframeResponseEvent,
IframeResponseEventMap,
isIframeEventWrapper,
TypedMessageEvent
} from "./Events/IframeEvent";
import type {UserInputChatEvent} from "./Events/UserInputChatEvent";
//import { isLoadPageEvent } from './Events/LoadPageEvent';
import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent";
import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent";
import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent";
import {isSetPropertyEvent, SetPropertyEvent} from "./Events/setPropertyEvent";
import {isLayerEvent, LayerEvent} from "./Events/LayerEvent";
import {isMenuItemRegisterEvent,} from "./Events/ui/MenuItemRegisterEvent";
import type {DataLayerEvent} from "./Events/DataLayerEvent";
import type {GameStateEvent} from "./Events/GameStateEvent";
import type {HasPlayerMovedEvent} from "./Events/HasPlayerMovedEvent";
import {isLoadPageEvent} from "./Events/LoadPageEvent";
import {handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent} from "./Events/ui/MenuItemRegisterEvent";
/**
* Listens to messages from iframes and turn those messages into easy to use observables.
* Also allows to send messages to those iframes.
*/
class IframeListener {
private readonly _chatStream: Subject<ChatEvent> = new Subject();
public readonly chatStream = this._chatStream.asObservable();
private readonly _openPopupStream: Subject<OpenPopupEvent> = new Subject();
public readonly openPopupStream = this._openPopupStream.asObservable();
private readonly _openTabStream: Subject<OpenTabEvent> = new Subject();
public readonly openTabStream = this._openTabStream.asObservable();
private readonly _goToPageStream: Subject<GoToPageEvent> = new Subject();
public readonly goToPageStream = this._goToPageStream.asObservable();
private readonly _loadPageStream: Subject<string> = new Subject();
public readonly loadPageStream = this._loadPageStream.asObservable();
private readonly _openCoWebSiteStream: Subject<OpenCoWebSiteEvent> = new Subject();
public readonly openCoWebSiteStream = this._openCoWebSiteStream.asObservable();
private readonly _closeCoWebSiteStream: Subject<void> = new Subject();
public readonly closeCoWebSiteStream = this._closeCoWebSiteStream.asObservable();
private readonly _disablePlayerControlStream: Subject<void> = new Subject();
public readonly disablePlayerControlStream = this._disablePlayerControlStream.asObservable();
private readonly _enablePlayerControlStream: Subject<void> = new Subject();
public readonly enablePlayerControlStream = this._enablePlayerControlStream.asObservable();
private readonly _closePopupStream: Subject<ClosePopupEvent> = new Subject();
public readonly closePopupStream = this._closePopupStream.asObservable();
private readonly _displayBubbleStream: Subject<void> = new Subject();
public readonly displayBubbleStream = this._displayBubbleStream.asObservable();
private readonly _removeBubbleStream: Subject<void> = new Subject();
public readonly removeBubbleStream = this._removeBubbleStream.asObservable();
private readonly _showLayerStream: Subject<LayerEvent> = new Subject();
public readonly showLayerStream = this._showLayerStream.asObservable();
private readonly _hideLayerStream: Subject<LayerEvent> = new Subject();
public readonly hideLayerStream = this._hideLayerStream.asObservable();
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
public readonly setPropertyStream = this._setPropertyStream.asObservable();
private readonly _gameStateStream: Subject<void> = new Subject();
public readonly gameStateStream = this._gameStateStream.asObservable();
private readonly _dataLayerChangeStream: Subject<void> = new Subject();
public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable();
private readonly _registerMenuCommandStream: Subject<string> = new Subject();
public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable();
private readonly _unregisterMenuCommandStream: Subject<string> = new Subject();
public readonly unregisterMenuCommandStream = this._unregisterMenuCommandStream.asObservable();
private readonly _playSoundStream: Subject<PlaySoundEvent> = new Subject();
public readonly playSoundStream = this._playSoundStream.asObservable();
private readonly _stopSoundStream: Subject<StopSoundEvent> = new Subject();
public readonly stopSoundStream = this._stopSoundStream.asObservable();
private readonly _loadSoundStream: Subject<LoadSoundEvent> = new Subject();
public readonly loadSoundStream = this._loadSoundStream.asObservable();
private readonly iframes = new Set<HTMLIFrameElement>();
private readonly iframeCloseCallbacks = new Map<HTMLIFrameElement, (() => void)[]>();
private readonly scripts = new Map<string, HTMLIFrameElement>();
private sendPlayerMove: boolean = false;
init() {
window.addEventListener("message", (message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
// Do we trust the sender of this message?
// Let's only accept messages from the iframe that are allowed.
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
let foundSrc: string | undefined;
let iframe: HTMLIFrameElement;
for (iframe of this.iframes) {
if (iframe.contentWindow === message.source) {
foundSrc = iframe.src;
break;
}
}
const payload = message.data;
if (foundSrc === undefined) {
if (isIframeEventWrapper(payload)) {
console.warn('It seems an iFrame is trying to communicate with WorkAdventure but was not explicitly granted the permission to do so. ' +
'If you are looking to use the WorkAdventure Scripting API inside an iFrame, you should allow the ' +
'iFrame to communicate with WorkAdventure by using the "openWebsiteAllowApi" property in your map (or passing "true" as a second' +
'parameter to WA.nav.openCoWebSite())');
}
return;
}
if (isIframeEventWrapper(payload)) {
if (payload.type === 'showLayer' && isLayerEvent(payload.data)) {
this._showLayerStream.next(payload.data);
} else if (payload.type === 'hideLayer' && isLayerEvent(payload.data)) {
this._hideLayerStream.next(payload.data);
} else if (payload.type === 'setProperty' && isSetPropertyEvent(payload.data)) {
this._setPropertyStream.next(payload.data);
} else if (payload.type === 'chat' && isChatEvent(payload.data)) {
this._chatStream.next(payload.data);
} else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) {
this._openPopupStream.next(payload.data);
} else if (payload.type === 'closePopup' && isClosePopupEvent(payload.data)) {
this._closePopupStream.next(payload.data);
}
else if (payload.type === 'openTab' && isOpenTabEvent(payload.data)) {
scriptUtils.openTab(payload.data.url);
}
else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) {
scriptUtils.goToPage(payload.data.url);
}
else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)) {
this._loadPageStream.next(payload.data.url);
}
else if (payload.type === 'playSound' && isPlaySoundEvent(payload.data)) {
this._playSoundStream.next(payload.data);
}
else if (payload.type === 'stopSound' && isStopSoundEvent(payload.data)) {
this._stopSoundStream.next(payload.data);
}
else if (payload.type === 'loadSound' && isLoadSoundEvent(payload.data)) {
this._loadSoundStream.next(payload.data);
}
else if (payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) {
scriptUtils.openCoWebsite(payload.data.url, foundSrc);
}
else if (payload.type === 'closeCoWebSite') {
scriptUtils.closeCoWebSite();
}
else if (payload.type === 'disablePlayerControls') {
this._disablePlayerControlStream.next();
}
else if (payload.type === 'restorePlayerControls') {
this._enablePlayerControlStream.next();
} else if (payload.type === 'displayBubble') {
this._displayBubbleStream.next();
} else if (payload.type === 'removeBubble') {
this._removeBubbleStream.next();
} else if (payload.type == "getState") {
this._gameStateStream.next();
} else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true
} else if (payload.type == "getDataLayer") {
this._dataLayerChangeStream.next();
} else if (isMenuItemRegisterIframeEvent(payload)) {
const data = payload.data.menutItem;
// @ts-ignore
this.iframeCloseCallbacks.get(iframe).push(() => {
this._unregisterMenuCommandStream.next(data);
})
handleMenuItemRegistrationEvent(payload.data)
}
}
}, false);
}
sendDataLayerEvent(dataLayerEvent: DataLayerEvent) {
this.postMessage({
'type' : 'dataLayer',
'data' : dataLayerEvent
})
}
sendGameStateEvent(gameStateEvent: GameStateEvent) {
this.postMessage({
'type': 'gameState',
'data': gameStateEvent
});
}
/**
* Allows the passed iFrame to send/receive messages via the API.
*/
registerIframe(iframe: HTMLIFrameElement): void {
this.iframes.add(iframe);
this.iframeCloseCallbacks.set(iframe, []);
}
unregisterIframe(iframe: HTMLIFrameElement): void {
this.iframeCloseCallbacks.get(iframe)?.forEach(callback => {
callback();
});
this.iframes.delete(iframe);
}
registerScript(scriptUrl: string): void {
console.log('Loading map related script at ', scriptUrl)
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
// Using external iframe mode (
const iframe = document.createElement('iframe');
iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = 'none';
iframe.src = '/iframe.html?script=' + encodeURIComponent(scriptUrl);
// We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add('allow-scripts');
iframe.sandbox.add('allow-top-navigation-by-user-activation');
document.body.prepend(iframe);
this.scripts.set(scriptUrl, iframe);
this.registerIframe(iframe);
} else {
// production code
const iframe = document.createElement('iframe');
iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = 'none';
// We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add('allow-scripts');
iframe.sandbox.add('allow-top-navigation-by-user-activation');
//iframe.src = "data:text/html;charset=utf-8," + escape(html);
iframe.srcdoc = '<!doctype html>\n' +
'\n' +
'<html lang="en">\n' +
'<head>\n' +
'<script src="' + window.location.protocol + '//' + window.location.host + '/iframe_api.js" ></script>\n' +
'<script src="' + scriptUrl + '" ></script>\n' +
'<title></title>\n' +
'</head>\n' +
'</html>\n';
document.body.prepend(iframe);
this.scripts.set(scriptUrl, iframe);
this.registerIframe(iframe);
}
}
private static getIFrameId(scriptUrl: string): string {
return 'script' + btoa(scriptUrl);
}
unregisterScript(scriptUrl: string): void {
const iFrameId = IframeListener.getIFrameId(scriptUrl);
const iframe = HtmlUtils.getElementByIdOrFail<HTMLIFrameElement>(iFrameId);
if (!iframe) {
throw new Error('Unknown iframe for script "' + scriptUrl + '"');
}
this.unregisterIframe(iframe);
iframe.remove();
this.scripts.delete(scriptUrl);
}
sendUserInputChat(message: string) {
this.postMessage({
'type': 'userInputChat',
'data': {
'message': message,
} as UserInputChatEvent
});
}
sendEnterEvent(name: string) {
this.postMessage({
'type': 'enterEvent',
'data': {
"name": name
} as EnterLeaveEvent
});
}
sendLeaveEvent(name: string) {
this.postMessage({
'type': 'leaveEvent',
'data': {
"name": name
} as EnterLeaveEvent
});
}
hasPlayerMoved(event: HasPlayerMovedEvent) {
if (this.sendPlayerMove) {
this.postMessage({
'type': 'hasPlayerMoved',
'data': event
});
}
}
sendButtonClickedEvent(popupId: number, buttonId: number): void {
this.postMessage({
'type': 'buttonClickedEvent',
'data': {
popupId,
buttonId
} as ButtonClickedEvent
});
}
/**
* Sends the message... to all allowed iframes.
*/
public postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) {
for (const iframe of this.iframes) {
iframe.contentWindow?.postMessage(message, '*');
}
}
}
export const iframeListener = new IframeListener();

View file

@ -0,0 +1,23 @@
import {coWebsiteManager} from "../WebRtc/CoWebsiteManager";
class ScriptUtils {
public openTab(url : string){
window.open(url);
}
public goToPage(url : string){
window.location.href = url;
}
public openCoWebsite(url: string, base: string) {
coWebsiteManager.loadCoWebsite(url, base);
}
public closeCoWebSite(){
coWebsiteManager.closeCoWebsite();
}
}
export const scriptUtils = new ScriptUtils();

View file

@ -0,0 +1,31 @@
import type * as tg from "generic-type-guard";
import type { IframeEvent, IframeEventMap, IframeResponseEventMap } from '../Events/IframeEvent';
export function sendToWorkadventure(content: IframeEvent<keyof IframeEventMap>) {
window.parent.postMessage(content, "*")
}
type GuardedType<Guard extends tg.TypeGuard<unknown>> = Guard extends tg.TypeGuard<infer T> ? T : never
export interface IframeCallback<Key extends keyof IframeResponseEventMap, T = IframeResponseEventMap[Key], Guard = tg.TypeGuard<T>> {
typeChecker: Guard,
callback: (payloadData: T) => void
}
export interface IframeCallbackContribution<Key extends keyof IframeResponseEventMap> extends IframeCallback<Key> {
type: Key
}
/**
* !! be aware that the implemented attributes (addMethodsAtRoot and subObjectIdentifier) must be readonly
*
*
*/
export abstract class IframeApiContribution<T extends {
callbacks: Array<IframeCallbackContribution<keyof IframeResponseEventMap>>,
}> {
abstract callbacks: T["callbacks"]
}

View file

@ -0,0 +1,39 @@
import {sendToWorkadventure} from "../IframeApiContribution";
import type {LoadSoundEvent} from "../../Events/LoadSoundEvent";
import type {PlaySoundEvent} from "../../Events/PlaySoundEvent";
import type {StopSoundEvent} from "../../Events/StopSoundEvent";
import SoundConfig = Phaser.Types.Sound.SoundConfig;
export class Sound {
constructor(private url: string) {
sendToWorkadventure({
"type": 'loadSound',
"data": {
url: this.url,
} as LoadSoundEvent
});
}
public play(config: SoundConfig) {
sendToWorkadventure({
"type": 'playSound',
"data": {
url: this.url,
config
} as PlaySoundEvent
});
return this.url;
}
public stop() {
sendToWorkadventure({
"type": 'stopSound',
"data": {
url: this.url,
} as StopSoundEvent
});
return this.url;
}
}

View file

@ -0,0 +1,18 @@
import type {Popup} from "./Popup";
export type ButtonClickedCallback = (popup: Popup) => void;
export interface ButtonDescriptor {
/**
* The label of the button
*/
label: string,
/**
* The type of the button. Can be one of "normal", "primary", "success", "warning", "error", "disabled"
*/
className?: "normal" | "primary" | "success" | "warning" | "error" | "disabled",
/**
* Callback called if the button is pressed
*/
callback: ButtonClickedCallback,
}

View file

@ -0,0 +1,11 @@
import type { MenuItemClickedEvent } from '../../Events/ui/MenuItemClickedEvent';
import { iframeListener } from '../../IframeListener';
export function sendMenuClickedEvent(menuItem: string) {
iframeListener.postMessage({
'type': 'menuItemClicked',
'data': {
menuItem: menuItem,
} as MenuItemClickedEvent
});
}

View file

@ -0,0 +1,19 @@
import {sendToWorkadventure} from "../IframeApiContribution";
import type {ClosePopupEvent} from "../../Events/ClosePopupEvent";
export class Popup {
constructor(private id: number) {
}
/**
* Closes the popup
*/
public close(): void {
sendToWorkadventure({
'type': 'closePopup',
'data': {
'popupId': this.id,
} as ClosePopupEvent
});
}
}

View file

@ -0,0 +1,38 @@
import type { ChatEvent } from '../Events/ChatEvent'
import { isUserInputChatEvent, UserInputChatEvent } from '../Events/UserInputChatEvent'
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'
import { apiCallback } from "./registeredCallbacks";
import {Subject} from "rxjs";
const chatStream = new Subject<string>();
class WorkadventureChatCommands extends IframeApiContribution<WorkadventureChatCommands> {
callbacks = [apiCallback({
callback: (event: UserInputChatEvent) => {
chatStream.next(event.message);
},
type: "userInputChat",
typeChecker: isUserInputChatEvent
})]
sendChatMessage(message: string, author: string) {
sendToWorkadventure({
type: 'chat',
data: {
'message': message,
'author': author
}
})
}
/**
* Listen to messages sent by the local user, in the chat.
*/
onChatMessage(callback: (message: string) => void) {
chatStream.subscribe(callback);
}
}
export default new WorkadventureChatCommands()

View file

@ -0,0 +1,16 @@
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
class WorkadventureControlsCommands extends IframeApiContribution<WorkadventureControlsCommands> {
callbacks = []
disablePlayerControls(): void {
sendToWorkadventure({ 'type': 'disablePlayerControls', data: null });
}
restorePlayerControls(): void {
sendToWorkadventure({ 'type': 'restorePlayerControls', data: null });
}
}
export default new WorkadventureControlsCommands();

View file

@ -0,0 +1,57 @@
import type { GoToPageEvent } from '../Events/GoToPageEvent';
import type { OpenTabEvent } from '../Events/OpenTabEvent';
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
import type {OpenCoWebSiteEvent} from "../Events/OpenCoWebSiteEvent";
import type {LoadPageEvent} from "../Events/LoadPageEvent";
class WorkadventureNavigationCommands extends IframeApiContribution<WorkadventureNavigationCommands> {
callbacks = []
openTab(url: string): void {
sendToWorkadventure({
"type": 'openTab',
"data": {
url
}
});
}
goToPage(url: string): void {
sendToWorkadventure({
"type": 'goToPage',
"data": {
url
}
});
}
goToRoom(url: string): void {
sendToWorkadventure({
"type": 'loadPage',
"data": {
url
}
});
}
openCoWebSite(url: string): void {
sendToWorkadventure({
"type": 'openCoWebSite',
"data": {
url
}
});
}
closeCoWebSite(): void {
sendToWorkadventure({
"type": 'closeCoWebSite',
data: null
});
}
}
export default new WorkadventureNavigationCommands();

View file

@ -0,0 +1,29 @@
import {IframeApiContribution, sendToWorkadventure} from "./IframeApiContribution";
import type {HasPlayerMovedEvent, HasPlayerMovedEventCallback} from "../Events/HasPlayerMovedEvent";
import {Subject} from "rxjs";
import {apiCallback} from "./registeredCallbacks";
import {isHasPlayerMovedEvent} from "../Events/HasPlayerMovedEvent";
const moveStream = new Subject<HasPlayerMovedEvent>();
class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
callbacks = [
apiCallback({
type: 'hasPlayerMoved',
typeChecker: isHasPlayerMovedEvent,
callback: (payloadData) => {
moveStream.next(payloadData);
}
}),
]
onPlayerMove(callback: HasPlayerMovedEventCallback): void {
moveStream.subscribe(callback);
sendToWorkadventure({
type: 'onPlayerMove',
data: null
})
}
}
export default new WorkadventurePlayerCommands();

View file

@ -0,0 +1,16 @@
import type {IframeResponseEventMap} from "../../Api/Events/IframeEvent";
import type {IframeCallback} from "../../Api/iframe/IframeApiContribution";
import type {IframeCallbackContribution} from "../../Api/iframe/IframeApiContribution";
export const registeredCallbacks: { [K in keyof IframeResponseEventMap]?: IframeCallback<K> } = {}
export function apiCallback<T extends keyof IframeResponseEventMap>(callbackData: IframeCallbackContribution<T>): IframeCallbackContribution<keyof IframeResponseEventMap> {
const iframeCallback = {
typeChecker: callbackData.typeChecker,
callback: callbackData.callback
} as IframeCallback<T>;
const newCallback = { [callbackData.type]: iframeCallback };
Object.assign(registeredCallbacks, newCallback)
return callbackData as unknown as IframeCallbackContribution<keyof IframeResponseEventMap>;
}

View file

@ -0,0 +1,134 @@
import { Subject } from "rxjs";
import { isDataLayerEvent } from "../Events/DataLayerEvent";
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
import { isGameStateEvent } from "../Events/GameStateEvent";
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks";
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
import type { DataLayerEvent } from "../Events/DataLayerEvent";
import type { GameStateEvent } from "../Events/GameStateEvent";
const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const dataLayerResolver = new Subject<DataLayerEvent>();
const stateResolvers = new Subject<GameStateEvent>();
let immutableDataPromise: Promise<GameStateEvent> | undefined = undefined;
interface Room {
id: string;
mapUrl: string;
map: ITiledMap;
startLayer: string | null;
}
interface User {
id: string | undefined;
nickName: string | null;
tags: string[];
}
function getGameState(): Promise<GameStateEvent> {
if (immutableDataPromise === undefined) {
immutableDataPromise = new Promise<GameStateEvent>((resolver, thrower) => {
stateResolvers.subscribe(resolver);
sendToWorkadventure({ type: "getState", data: null });
});
}
return immutableDataPromise;
}
function getDataLayer(): Promise<DataLayerEvent> {
return new Promise<DataLayerEvent>((resolver, thrower) => {
dataLayerResolver.subscribe(resolver);
sendToWorkadventure({ type: "getDataLayer", data: null });
});
}
class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> {
callbacks = [
apiCallback({
callback: (payloadData: EnterLeaveEvent) => {
enterStreams.get(payloadData.name)?.next();
},
type: "enterEvent",
typeChecker: isEnterLeaveEvent,
}),
apiCallback({
type: "leaveEvent",
typeChecker: isEnterLeaveEvent,
callback: (payloadData) => {
leaveStreams.get(payloadData.name)?.next();
},
}),
apiCallback({
type: "gameState",
typeChecker: isGameStateEvent,
callback: (payloadData) => {
stateResolvers.next(payloadData);
},
}),
apiCallback({
type: "dataLayer",
typeChecker: isDataLayerEvent,
callback: (payloadData) => {
dataLayerResolver.next(payloadData);
},
}),
];
onEnterZone(name: string, callback: () => void): void {
let subject = enterStreams.get(name);
if (subject === undefined) {
subject = new Subject<EnterLeaveEvent>();
enterStreams.set(name, subject);
}
subject.subscribe(callback);
}
onLeaveZone(name: string, callback: () => void): void {
let subject = leaveStreams.get(name);
if (subject === undefined) {
subject = new Subject<EnterLeaveEvent>();
leaveStreams.set(name, subject);
}
subject.subscribe(callback);
}
showLayer(layerName: string): void {
sendToWorkadventure({ type: "showLayer", data: { name: layerName } });
}
hideLayer(layerName: string): void {
sendToWorkadventure({ type: "hideLayer", data: { name: layerName } });
}
setProperty(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void {
sendToWorkadventure({
type: "setProperty",
data: {
layerName: layerName,
propertyName: propertyName,
propertyValue: propertyValue,
},
});
}
getCurrentRoom(): Promise<Room> {
return getGameState().then((gameState) => {
return getDataLayer().then((mapJson) => {
return {
id: gameState.roomId,
map: mapJson.data as ITiledMap,
mapUrl: gameState.mapUrl,
startLayer: gameState.startLayerName,
};
});
});
}
getCurrentUser(): Promise<User> {
return getGameState().then((gameState) => {
return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags };
});
}
}
export default new WorkadventureRoomCommands();

View file

@ -0,0 +1,17 @@
import type { LoadSoundEvent } from '../Events/LoadSoundEvent';
import type { PlaySoundEvent } from '../Events/PlaySoundEvent';
import type { StopSoundEvent } from '../Events/StopSoundEvent';
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
import {Sound} from "./Sound/Sound";
class WorkadventureSoundCommands extends IframeApiContribution<WorkadventureSoundCommands> {
callbacks = []
loadSound(url: string): Sound {
return new Sound(url);
}
}
export default new WorkadventureSoundCommands();

106
front/src/Api/iframe/ui.ts Normal file
View file

@ -0,0 +1,106 @@
import { isButtonClickedEvent } from '../Events/ButtonClickedEvent';
import { isMenuItemClickedEvent } from '../Events/ui/MenuItemClickedEvent';
import type { MenuItemRegisterEvent } from '../Events/ui/MenuItemRegisterEvent';
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
import { apiCallback } from "./registeredCallbacks";
import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor";
import { Popup } from "./Ui/Popup";
let popupId = 0;
const popups: Map<number, Popup> = new Map<number, Popup>();
const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<number, Map<number, ButtonClickedCallback>>();
const menuCallbacks: Map<string, (command: string) => void> = new Map()
interface ZonedPopupOptions {
zone: string
objectLayerName?: string,
popupText: string,
delay?: number
popupOptions: Array<ButtonDescriptor>
}
class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiCommands> {
callbacks = [apiCallback({
type: "buttonClickedEvent",
typeChecker: isButtonClickedEvent,
callback: (payloadData) => {
const callback = popupCallbacks.get(payloadData.popupId)?.get(payloadData.buttonId);
const popup = popups.get(payloadData.popupId);
if (popup === undefined) {
throw new Error('Could not find popup with ID "' + payloadData.popupId + '"');
}
if (callback) {
callback(popup);
}
}
}),
apiCallback({
type: "menuItemClicked",
typeChecker: isMenuItemClickedEvent,
callback: event => {
const callback = menuCallbacks.get(event.menuItem);
if (callback) {
callback(event.menuItem)
}
}
})];
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
popupId++;
const popup = new Popup(popupId);
const btnMap = new Map<number, () => void>();
popupCallbacks.set(popupId, btnMap);
let id = 0;
for (const button of buttons) {
const callback = button.callback;
if (callback) {
btnMap.set(id, () => {
callback(popup);
});
}
id++;
}
sendToWorkadventure({
'type': 'openPopup',
'data': {
popupId,
targetObject,
message,
buttons: buttons.map((button) => {
return {
label: button.label,
className: button.className
};
})
}
});
popups.set(popupId, popup)
return popup;
}
registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) {
menuCallbacks.set(commandDescriptor, callback);
sendToWorkadventure({
'type': 'registerMenuCommand',
'data': {
menutItem: commandDescriptor
}
});
}
displayBubble(): void {
sendToWorkadventure({ 'type': 'displayBubble', data: null });
}
removeBubble(): void {
sendToWorkadventure({ 'type': 'removeBubble', data: null });
}
}
export default new WorkAdventureUiCommands();

View file

@ -0,0 +1,97 @@
<script lang="typescript">
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 SelectCharacterScene from "./selectCharacter/SelectCharacterScene.svelte";
import {customCharacterSceneVisibleStore} from "../Stores/CustomCharacterStore";
import {errorStore} from "../Stores/ErrorStore";
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
import LoginScene from "./Login/LoginScene.svelte";
import {loginSceneVisibleStore} from "../Stores/LoginSceneStore";
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
import VisitCard from "./VisitCard/VisitCard.svelte";
import {requestVisitCardsStore} from "../Stores/GameStore";
import type {Game} from "../Phaser/Game/Game";
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
import AudioPlaying from "./UI/AudioPlaying.svelte";
import {soundPlayingStore} from "../Stores/SoundPlayingStore";
import ErrorDialog from "./UI/ErrorDialog.svelte";
import VideoOverlay from "./Video/VideoOverlay.svelte";
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility";
import {consoleGlobalMessageManagerVisibleStore} from "../Stores/ConsoleGlobalMessageManagerStore";
import ConsoleGlobalMessageManager from "./ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte";
export let game: Game;
</script>
<div>
{#if $loginSceneVisibleStore}
<div class="scrollable">
<LoginScene game={game}></LoginScene>
</div>
{/if}
{#if $selectCharacterSceneVisibleStore}
<div>
<SelectCharacterScene game={ game }></SelectCharacterScene>
</div>
{/if}
{#if $customCharacterSceneVisibleStore}
<div>
<CustomCharacterScene game={ game }></CustomCharacterScene>
</div>
{/if}
{#if $selectCompanionSceneVisibleStore}
<div>
<SelectCompanionScene game={ game }></SelectCompanionScene>
</div>
{/if}
{#if $enableCameraSceneVisibilityStore}
<div class="scrollable">
<EnableCameraScene game={game}></EnableCameraScene>
</div>
{/if}
{#if $soundPlayingStore}
<div>
<AudioPlaying url={$soundPlayingStore} />
</div>
{/if}
<!--
{#if $menuIconVisible}
<div>
<MenuIcon />
</div>
{/if}
-->
{#if $gameOverlayVisibilityStore}
<div>
<VideoOverlay></VideoOverlay>
<MyCamera></MyCamera>
<CameraControls></CameraControls>
</div>
{/if}
{#if $consoleGlobalMessageManagerVisibleStore}
<div>
<ConsoleGlobalMessageManager game={game}></ConsoleGlobalMessageManager>
</div>
{/if}
{#if $helpCameraSettingsVisibleStore}
<div>
<HelpCameraSettingsPopup></HelpCameraSettingsPopup>
</div>
{/if}
{#if $requestVisitCardsStore}
<VisitCard visitCardUrl={$requestVisitCardsStore}></VisitCard>
{/if}
{#if $errorStore.length > 0}
<div>
<ErrorDialog></ErrorDialog>
</div>
{/if}
</div>

View file

@ -0,0 +1,80 @@
<script lang="typescript">
import {requestedScreenSharingState, screenSharingAvailableStore} from "../Stores/ScreenSharingStore";
import {requestedCameraState, requestedMicrophoneState} from "../Stores/MediaStore";
import monitorImg from "./images/monitor.svg";
import monitorCloseImg from "./images/monitor-close.svg";
import cinemaImg from "./images/cinema.svg";
import cinemaCloseImg from "./images/cinema-close.svg";
import microphoneImg from "./images/microphone.svg";
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";
function screenSharingClick(): void {
if ($requestedScreenSharingState === true) {
requestedScreenSharingState.disableScreenSharing();
} else {
requestedScreenSharingState.enableScreenSharing();
}
}
function cameraClick(): void {
if ($requestedCameraState === true) {
requestedCameraState.disableWebcam();
} else {
requestedCameraState.enableWebcam();
}
}
function microphoneClick(): void {
if ($requestedMicrophoneState === true) {
requestedMicrophoneState.disableMicrophone();
} else {
requestedMicrophoneState.enableMicrophone();
}
}
function switchLayoutMode() {
if ($layoutModeStore === LayoutMode.Presentation) {
$layoutModeStore = LayoutMode.VideoChat;
} else {
$layoutModeStore = LayoutMode.Presentation;
}
}
</script>
<div>
<div class="btn-cam-action">
<div class="btn-layout" 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">
{/if}
</div>
<div class="btn-monitor" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore} class:enabled={$requestedScreenSharingState}>
{#if $requestedScreenSharingState}
<img src={monitorImg} alt="Start screen sharing">
{:else}
<img src={monitorCloseImg} alt="Stop screen sharing">
{/if}
</div>
<div class="btn-video" on:click={cameraClick} class:disabled={!$requestedCameraState}>
{#if $requestedCameraState}
<img src={cinemaImg} alt="Turn on webcam">
{:else}
<img src={cinemaCloseImg} alt="Turn off webcam">
{/if}
</div>
<div class="btn-micro" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState}>
{#if $requestedMicrophoneState}
<img src={microphoneImg} alt="Turn on microphone">
{:else}
<img src={microphoneCloseImg} alt="Turn off microphone">
{/if}
</div>
</div>
</div>

View file

@ -0,0 +1,44 @@
<script lang="typescript">
import InputTextGlobalMessage from "./InputTextGlobalMessage.svelte";
import UploadAudioGlobalMessage from "./UploadAudioGlobalMessage.svelte";
import {gameManager} from "../../Phaser/Game/GameManager";
import type {Game} from "../../Phaser/Game/Game";
export let game: Game;
let inputSendTextActive = true;
let uploadMusicActive = false;
function inputSendTextActivate() {
inputSendTextActive = true;
uploadMusicActive = false;
}
function inputUploadMusicActivate() {
uploadMusicActive = true;
inputSendTextActive = false;
}
</script>
<div class="main-console nes-container is-rounded">
<!-- <div class="console nes-container is-rounded">
<img class="btn-close" src="resources/logos/send-yellow.svg" alt="Close">
</div>-->
<div class="main-global-message">
<h2> Global Message </h2>
<div class="global-message">
<div class="menu">
<button class="nes-btn {inputSendTextActive ? 'is-disabled' : ''}" on:click|preventDefault={inputSendTextActivate}>Message</button>
<button class="nes-btn {uploadMusicActive ? 'is-disabled' : ''}" on:click|preventDefault={inputUploadMusicActivate}>Audio</button>
</div>
<div class="main-input">
{#if inputSendTextActive}
<InputTextGlobalMessage game={game} gameManager={gameManager}></InputTextGlobalMessage>
{/if}
{#if uploadMusicActive}
<UploadAudioGlobalMessage game={game} gameManager={gameManager}></UploadAudioGlobalMessage>
{/if}
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,99 @@
<script lang="ts">
import {consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
import {onMount} from "svelte";
import type {Game} from "../../Phaser/Game/Game";
import type {GameManager} from "../../Phaser/Game/GameManager";
import type {PlayGlobalMessageInterface} from "../../Connexion/ConnexionModels";
import {AdminMessageEventTypes} from "../../Connexion/AdminMessagesService";
import type {Quill} from "quill";
import {LoginSceneName} from "../../Phaser/Login/LoginScene";
//toolbar
export const toolbarOptions = [
['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
[{'size': ['small', false, 'large', 'huge']}], // custom dropdown
[{'header': [1, 2, 3, 4, 5, 6, false]}],
[{'color': []}, {'background': []}], // dropdown with defaults from theme
[{'font': []}],
[{'align': []}],
['clean'],
['link', 'image', 'video']
// remove formatting button
];
export let game: Game;
export let gameManager: GameManager;
let gameScene = gameManager.getCurrentGameScene(game.scene.getScene(LoginSceneName));
let quill: Quill;
let INPUT_CONSOLE_MESSAGE: HTMLDivElement;
const MESSAGE_TYPE = AdminMessageEventTypes.admin;
//Quill
onMount(async () => {
// Import quill
const {default: Quill} = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any
quill = new Quill(INPUT_CONSOLE_MESSAGE, {
theme: 'snow',
modules: {
toolbar: toolbarOptions
},
});
quill.on('selection-change', function (range, oldRange) {
if (range === null && oldRange !== null) {
consoleGlobalMessageManagerFocusStore.set(false);
} else if (range !== null && oldRange === null)
consoleGlobalMessageManagerFocusStore.set(true);
});
});
function disableConsole() {
consoleGlobalMessageManagerVisibleStore.set(false);
consoleGlobalMessageManagerFocusStore.set(false);
}
function SendTextMessage() {
if (gameScene == undefined) {
return;
}
const text = quill.getText(0, quill.getLength());
const GlobalMessage: PlayGlobalMessageInterface = {
id: "1", // FIXME: use another ID?
message: text,
type: MESSAGE_TYPE
};
quill.deleteText(0, quill.getLength());
gameScene.connection?.emitGlobalMessage(GlobalMessage);
disableConsole();
}
</script>
<section class="section-input-send-text">
<div class="input-send-text" bind:this={INPUT_CONSOLE_MESSAGE}></div>
<div class="btn-action">
<button class="nes-btn is-primary" on:click|preventDefault={SendTextMessage}>Send</button>
</div>
</section>
<style lang="scss">
@import 'https://cdn.quilljs.com/1.3.7/quill.snow.css';
</style>

View file

@ -0,0 +1,130 @@
<script lang="ts">
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import type {Game} from "../../Phaser/Game/Game";
import type {GameManager} from "../../Phaser/Game/GameManager";
import {consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore";
import {AdminMessageEventTypes} from "../../Connexion/AdminMessagesService";
import type {PlayGlobalMessageInterface} from "../../Connexion/ConnexionModels";
import uploadFile from "../images/music-file.svg";
import {LoginSceneName} from "../../Phaser/Login/LoginScene";
interface EventTargetFiles extends EventTarget {
files: Array<File>;
}
export let game: Game;
export let gameManager: GameManager;
let gameScene = gameManager.getCurrentGameScene(game.scene.getScene(LoginSceneName));
let fileinput: HTMLInputElement;
let filename: string;
let filesize: string;
let errorfile: boolean;
const AUDIO_TYPE = AdminMessageEventTypes.audio;
async function SendAudioMessage() {
if (gameScene == undefined) {
return;
}
const inputAudio = HtmlUtils.getElementByIdOrFail<HTMLInputElement>("input-send-audio");
const selectedFile = inputAudio.files ? inputAudio.files[0] : null;
if (!selectedFile) {
errorfile = true;
throw 'no file selected';
}
const fd = new FormData();
fd.append('file', selectedFile);
const res = await gameScene.connection?.uploadAudio(fd);
const GlobalMessage: PlayGlobalMessageInterface = {
id: (res as { id: string }).id,
message: (res as { path: string }).path,
type: AUDIO_TYPE
}
inputAudio.value = '';
gameScene.connection?.emitGlobalMessage(GlobalMessage);
disableConsole();
}
function inputAudioFile(event: Event) {
const eventTarget : EventTargetFiles = (event.target as EventTargetFiles);
if(!eventTarget || !eventTarget.files || eventTarget.files.length === 0){
return;
}
const file = eventTarget.files[0];
if(!file) {
return;
}
filename = file.name;
filesize = getFileSize(file.size);
errorfile = false;
}
function getFileSize(number: number) {
if (number < 1024) {
return number + 'bytes';
} else if (number >= 1024 && number < 1048576) {
return (number / 1024).toFixed(1) + 'KB';
} else if (number >= 1048576) {
return (number / 1048576).toFixed(1) + 'MB';
} else {
return '';
}
}
function disableConsole() {
consoleGlobalMessageManagerVisibleStore.set(false);
consoleGlobalMessageManagerFocusStore.set(false);
}
</script>
<section class="section-input-send-audio">
<div class="input-send-audio">
<img src="{uploadFile}" alt="Upload a file" on:click|preventDefault={ () => {fileinput.click();}}>
{#if filename != undefined}
<label for="input-send-audio">{filename} : {filesize}</label>
{/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)}}>
</div>
<div class="btn-action">
<button class="nes-btn is-primary" on:click|preventDefault={SendAudioMessage}>Send</button>
</div>
</section>
<style lang="scss">
//UploadAudioGlobalMessage
.section-input-send-audio {
margin: 10px;
}
.section-input-send-audio .input-send-audio {
text-align: center;
}
.section-input-send-audio #input-send-audio{
display: none;
}
.section-input-send-audio div.input-send-audio label{
color: white;
}
.section-input-send-audio div.input-send-audio p.err {
color: #ce372b;
text-align: center;
}
.section-input-send-audio div.input-send-audio img{
height: 150px;
cursor: url('../../../style/images/cursor_pointer.png'), pointer;
}
</style>

View file

@ -0,0 +1,119 @@
<script lang="typescript">
import type { Game } from "../../Phaser/Game/Game";
import {CustomizeScene, CustomizeSceneName} from "../../Phaser/Login/CustomizeScene";
export let game: Game;
const customCharacterScene = game.scene.getScene(CustomizeSceneName) as CustomizeScene;
let activeRow = customCharacterScene.activeRow;
function selectLeft() {
customCharacterScene.moveCursorHorizontally(-1);
}
function selectRight() {
customCharacterScene.moveCursorHorizontally(1);
}
function selectUp() {
customCharacterScene.moveCursorVertically(-1);
activeRow = customCharacterScene.activeRow;
}
function selectDown() {
customCharacterScene.moveCursorVertically(1);
activeRow = customCharacterScene.activeRow;
}
function previousScene() {
customCharacterScene.backToPreviousScene();
}
function finish() {
customCharacterScene.nextSceneToCamera();
}
</script>
<form class="customCharacterScene">
<section class="text-center">
<h2>Customize your WOKA</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>
</section>
<section class="action">
{#if activeRow === 0}
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={ previousScene }>Return</button>
{/if}
{#if activeRow !== 0}
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={ selectUp }>Back <img src="resources/objects/arrow_up_black.png" alt=""/></button>
{/if}
{#if activeRow === 5}
<button type="submit" class="customCharacterSceneFormSubmit nes-btn is-primary" on:click|preventDefault={ finish }>Finish</button>
{/if}
{#if activeRow !== 5}
<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 {
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;
}
}
}
@media only screen and (max-width: 800px) {
form.customCharacterScene button.customCharacterSceneButtonLeft{
left: 5vw;
}
form.customCharacterScene button.customCharacterSceneButtonRight{
right: 5vw;
}
}
</style>

View file

@ -0,0 +1,229 @@
<script lang="typescript">
import type {Game} from "../../Phaser/Game/Game";
import {EnableCameraScene, EnableCameraSceneName} from "../../Phaser/Login/EnableCameraScene";
import {
audioConstraintStore,
cameraListStore,
localStreamStore,
microphoneListStore,
videoConstraintStore
} from "../../Stores/MediaStore";
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;
const enableCameraScene = game.scene.getScene(EnableCameraSceneName) as EnableCameraScene;
function submit() {
enableCameraScene.login();
}
function srcObject(node: HTMLVideoElement, stream: MediaStream) {
node.srcObject = stream;
return {
update(newStream: MediaStream) {
if (node.srcObject != newStream) {
node.srcObject = newStream
}
}
}
}
let stream: MediaStream | null;
const unsubscribe = localStreamStore.subscribe(value => {
if (value.type === 'success') {
stream = value.stream;
if (stream !== null) {
const videoTracks = stream.getVideoTracks();
if (videoTracks.length > 0) {
selectedCamera = videoTracks[0].getSettings().deviceId;
}
const audioTracks = stream.getAudioTracks();
if (audioTracks.length > 0) {
selectedMicrophone = audioTracks[0].getSettings().deviceId;
}
}
} else {
stream = null;
selectedCamera = undefined;
selectedMicrophone = undefined;
}
});
onDestroy(unsubscribe);
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();
}
function selectCamera() {
videoConstraintStore.setDeviceId(selectedCamera);
}
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>
<section class="selectWebcamForm">
{#if $cameraListStore.length > 1 }
<div class="control-group">
<img src={cinemaImg} alt="Camera" />
<div class="nes-select is-dark">
<select bind:value={selectedCamera} on:change={selectCamera}>
{#each $cameraListStore as camera}
<option value={camera.deviceId}>
{normalizeDeviceName(camera.label)}
</option>
{/each}
</select>
</div>
</div>
{/if}
{#if $microphoneListStore.length > 1 }
<div class="control-group">
<img src={microphoneImg} alt="Microphone" />
<div class="nes-select is-dark">
<select bind:value={selectedMicrophone} on:change={selectMicrophone}>
{#each $microphoneListStore as microphone}
<option value={microphone.deviceId}>
{normalizeDeviceName(microphone.label)}
</option>
{/each}
</select>
</div>
</div>
{/if}
</section>
<section class="action">
<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;
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;
}
option {
font-family: "Press Start 2P";
}
}
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;
}
}
.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%;
}
}
</style>

View file

@ -0,0 +1,82 @@
<script lang="typescript">
import { AudioContext } from 'standardized-audio-context';
import {SoundMeter} from "../../Phaser/Components/SoundMeter";
import {onDestroy} from "svelte";
export let stream: MediaStream | null;
let volume = 0;
const NB_BARS = 20;
let timeout: ReturnType<typeof setTimeout>;
const soundMeter = new SoundMeter();
let display = false;
$: {
if (stream && stream.getAudioTracks().length > 0) {
display = true;
soundMeter.connectToSource(stream, new AudioContext());
if (timeout) {
clearInterval(timeout);
}
timeout = setInterval(() => {
try{
volume = parseInt((soundMeter.getVolume() / 100 * NB_BARS).toFixed(0));
//console.log(volume);
}catch(err){
}
}, 100);
} else {
display = false;
}
}
onDestroy(() => {
soundMeter.stop();
if (timeout) {
clearInterval(timeout);
}
})
function color(i: number, volume: number) {
const red = 255 * i / NB_BARS;
const green = 255 * (1 - i / NB_BARS);
let alpha = 1;
if (i >= volume) {
alpha = 0.5;
}
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>
{/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;
}
.horizontal-sound-meter div {
margin-left: 5px;
flex-grow: 1;
}
</style>

View file

@ -0,0 +1,73 @@
<script lang="typescript">
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";
let isAndroid = window.navigator.userAgent.includes('Android');
let isFirefox = window.navigator.userAgent.includes('Firefox');
let isChrome = window.navigator.userAgent.includes('Chrome');
function refresh() {
window.location.reload();
}
function close() {
helpCameraSettingsVisibleStore.set(false);
}
</script>
<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>
<img src={firefoxImg} alt="" />
{: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>
</section>
</form>
<style lang="scss">
.helpCameraSettings {
pointer-events: auto;
background: #eceeee;
margin-left: auto;
margin-right: auto;
margin-top: 10vh;
max-height: 80vh;
max-width: 80vw;
overflow: auto;
text-align: center;
h2 {
font-family: 'Press Start 2P';
}
section {
p {
margin: 15px;
font-family: 'Press Start 2P';
& .err {
color: #ff0000;
}
}
img {
max-width: 500px;
width: 100%;
}
}
}
</style>

View file

@ -0,0 +1,122 @@
<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 logoImg from "../images/logo.png";
import {gameManager} from "../../Phaser/Game/GameManager";
export let game: Game;
const loginScene = game.scene.getScene(LoginSceneName) as LoginScene;
let name = gameManager.getPlayerName() || '';
let startValidating = false;
function submit() {
startValidating = true;
let finalName = name.trim();
if (finalName !== '') {
loginScene.login(finalName);
}
}
</script>
<form class="loginScene" on:submit|preventDefault={submit}>
<section class="text-center">
<img src={logoImg} alt="WorkAdventure logo" />
</section>
<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} />
<section class="error-section">
{#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>
{/if}
<section class="action">
<button type="submit" class="nes-btn is-primary loginSceneFormSubmit">Continue</button>
</section>
</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;
color: #ebeeee;
}
a:hover {
font-weight: 700;
}
p {
text-align: left;
margin: 10px 10px;
}
img {
width: 100%;
margin: 20px 0;
}
}
}
</style>

View file

@ -0,0 +1,33 @@
<script lang="typescript">
</script>
<main class="menuIcon">
<section>
<button>
<img src="/static/images/menu.svg" alt="Open menu">
</button>
</section>
</main>
<style lang="scss">
.menuIcon button {
background-color: black;
color: white;
border-radius: 7px;
padding: 2px 8px;
img {
width: 14px;
padding-top: 0;
/*cursor: url('/resources/logos/cursor_pointer.png'), pointer;*/
}
}
.menuIcon section {
margin: 10px;
}
@media only screen and (max-height: 700px) {
.menuIcon section {
margin: 2px;
}
}
</style>

View file

@ -0,0 +1,46 @@
<script lang="typescript">
import {localStreamStore} from "../Stores/MediaStore";
import SoundMeterWidget from "./SoundMeterWidget.svelte";
import {onDestroy} from "svelte";
function srcObject(node: HTMLVideoElement, stream: MediaStream) {
node.srcObject = stream;
return {
update(newStream: MediaStream) {
if (node.srcObject != newStream) {
node.srcObject = newStream
}
}
}
}
let stream : MediaStream|null;
/*$: {
if ($localStreamStore.type === 'success') {
stream = $localStreamStore.stream;
} else {
stream = null;
}
}*/
const unsubscribe = localStreamStore.subscribe(value => {
if (value.type === 'success') {
stream = value.stream;
} else {
stream = null;
}
});
onDestroy(unsubscribe);
</script>
<div>
<div class="video-container div-myCamVideo" class:hide={!$localStreamStore.constraints.video}>
{#if $localStreamStore.type === "success" && $localStreamStore.stream }
<video class="myCamVideo" use:srcObject={$localStreamStore.stream} autoplay muted playsinline></video>
<SoundMeterWidget stream={stream}></SoundMeterWidget>
{/if}
</div>
</div>

View file

@ -0,0 +1,87 @@
<script lang="typescript">
import type {Game} from "../../Phaser/Game/Game";
import {SelectCompanionScene, SelectCompanionSceneName} from "../../Phaser/Login/SelectCompanionScene";
export let game: Game;
const selectCompanionScene = game.scene.getScene(SelectCompanionSceneName) as SelectCompanionScene;
function selectLeft() {
selectCompanionScene.moveToLeft();
}
function selectRight() {
selectCompanionScene.moveToRight();
}
function noCompanion() {
selectCompanionScene.closeScene();
}
function selectCompanion() {
selectCompanionScene.selectCompanion();
}
</script>
<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>
</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>
</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 {
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;
}
}
@media only screen and (max-width: 800px) {
form.selectCompanionScene button.selectCharacterButtonLeft{
left: 5vw;
}
form.selectCompanionScene button.selectCharacterButtonRight{
right: 5vw;
}
}
</style>

View file

@ -0,0 +1,51 @@
<script lang="typescript">
import { AudioContext } from 'standardized-audio-context';
import {SoundMeter} from "../Phaser/Components/SoundMeter";
import {onDestroy} from "svelte";
export let stream: MediaStream|null;
let volume = 0;
let timeout: ReturnType<typeof setTimeout>;
const soundMeter = new SoundMeter();
let display = false;
$: {
if (stream && stream.getAudioTracks().length > 0) {
display = true;
soundMeter.connectToSource(stream, new AudioContext());
if (timeout) {
clearInterval(timeout);
}
timeout = setInterval(() => {
try{
volume = soundMeter.getVolume();
//console.log(volume);
}catch(err){
}
}, 100);
} else {
display = false;
}
}
onDestroy(() => {
soundMeter.stop();
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>
</div>

View file

@ -0,0 +1,52 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import megaphoneImg from "./images/megaphone.svg";
import {soundPlayingStore} from "../../Stores/SoundPlayingStore";
import {afterUpdate} from "svelte";
export let url: string;
let audio: HTMLAudioElement;
function soundEnded() {
soundPlayingStore.soundEnded();
}
afterUpdate(() => {
audio.play();
});
</script>
<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>
</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;
img {
border-radius: 50%;
background-color: #ffda01;
padding: 10px;
}
p {
color: white;
margin-left: 10px;
margin-top: 14px;
}
}
</style>

View file

@ -0,0 +1,48 @@
<script lang="ts">
import {errorStore} from "../../Stores/ErrorStore";
function close(): boolean {
errorStore.clearMessages();
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>
</div>
</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;
.button-bar {
text-align: center;
}
.body {
max-height: 50vh;
}
p {
font-family: "Press Start 2P";
&.title {
text-align: center;
}
}
}
</style>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 451.7 512" style="enable-background:new 0 0 451.7 512;" xml:space="preserve">
<path d="M436.9,212.6L237.2,12.9c-11.7-11.7-30.7-11.7-42.4,0s-11.7,30.7,0,42.4L394.5,255c11.5,11.9,30.5,12.2,42.4,0.7
c11.9-11.5,12.2-30.5,0.7-42.4C437.4,213.1,437.2,212.8,436.9,212.6z"/>
<path d="M179.5,83.1l-1.5,7.5c-10.4,53-36,103.4-70.6,144.3l109,108.3c40.7-34.9,90.2-61.5,143.1-72.3l7.5-1.5L179.5,83.1z"/>
<path d="M87.4,257l-74.2,74.2c-17.6,17.6-17.6,46.1,0,63.6c0,0,0,0,0,0l42.4,42.4c17.6,17.6,46.1,17.6,63.6,0c0,0,0,0,0,0l74.2-74.2
L87.4,257z M98,373.7c-6.1,5.6-15.6,5.3-21.2-0.8c-5.4-5.8-5.4-14.7,0-20.5l21.2-21.2c6-5.8,15.5-5.6,21.2,0.4
c5.6,5.8,5.6,15,0,20.8L98,373.7z"/>
<path d="M256.1,445.3l20.4-20.4c17.6-17.6,17.6-46.1,0-63.6l-15.1-15.2c-8.4,5.7-16.4,11.7-24.2,18.3l18.1,18.1
c5.8,5.9,5.8,15.3,0,21.2l-20.7,20.8l-30.5-29.5l-42.4,42.4l68.1,65.9c11.7,11.7,30.7,11.7,42.4,0c11.7-11.7,11.7-30.7,0-42.4l0,0
L256.1,445.3z"/>
<path d="M316.7,0c-8.3,0-15,6.7-15,15v30c0,8.3,6.7,15,15,15c8.3,0,15-6.7,15-15V15C331.7,6.7,325,0,316.7,0z"/>
<path d="M436.7,120h-30c-8.3,0-15,6.7-15,15s6.7,15,15,15h30c8.3,0,15-6.7,15-15S445,120,436.7,120z"/>
<path d="M417.3,34.4c-5.9-5.9-15.4-5.9-21.2,0l-30,30c-6,5.8-6.1,15.3-0.4,21.2c5.8,6,15.3,6.1,21.2,0.4c0.1-0.1,0.2-0.2,0.4-0.4
l30-30C423.2,49.7,423.2,40.2,417.3,34.4z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Some files were not shown because too many files have changed in this diff Show more