merged develop

This commit is contained in:
Hanusiak Piotr 2022-01-31 10:00:39 +01:00
commit 5aba99403e
144 changed files with 4800 additions and 2045 deletions

View file

@ -15,6 +15,9 @@ export class DebugController {
(async () => { (async () => {
const query = parse(req.getQuery()); const query = parse(req.getQuery());
if (ADMIN_API_TOKEN === "") {
return res.writeStatus("401 Unauthorized").end("No token configured!");
}
if (query.token !== ADMIN_API_TOKEN) { if (query.token !== ADMIN_API_TOKEN) {
return res.writeStatus("401 Unauthorized").end("Invalid token sent!"); return res.writeStatus("401 Unauthorized").end("Invalid token sent!");
} }

View file

@ -2,7 +2,7 @@ const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIM
const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48; const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48;
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false; const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false;
const ADMIN_API_URL = process.env.ADMIN_API_URL || ""; const ADMIN_API_URL = process.env.ADMIN_API_URL || "";
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken"; const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "";
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80; const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL; const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL;
const JITSI_ISS = process.env.JITSI_ISS || ""; const JITSI_ISS = process.env.JITSI_ISS || "";

View file

@ -1504,9 +1504,9 @@ natural-compare@^1.4.0:
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
node-fetch@^2.6.5: node-fetch@^2.6.5:
version "2.6.6" version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA== integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies: dependencies:
whatwg-url "^5.0.0" whatwg-url "^5.0.0"

View file

@ -83,7 +83,8 @@
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
"TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443", "TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443",
"JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false", "JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false",
"START_ROOM_URL": "/_/global/maps-"+url+"/starter/map.json" "START_ROOM_URL": "/_/global/maps-"+url+"/starter/map.json",
"ICON_URL": "//icon-"+url,
} }
}, },
"uploader": { "uploader": {
@ -109,7 +110,15 @@
"redis": { "redis": {
"image": "redis:6", "image": "redis:6",
"ports": [6379] "ports": [6379]
} },
"iconserver": {
"image": "matthiasluedtke/iconserver:v3.13.0",
"host": {
"url": "icon-"+url,
"containerPort": 8080,
},
"ports": [8080]
},
}, },
"config": { "config": {
k8sextension(k8sConf):: k8sextension(k8sConf)::
@ -210,6 +219,16 @@
} }
} }
}, },
iconserver+: {
ingress+: {
spec+: {
tls+: [{
hosts: ["icon-"+url],
secretName: "certificate-tls"
}]
}
}
},
} }
} }
} }

View file

@ -52,17 +52,17 @@ WA.nav.goToRoom("/_/global/<path to global map>.json#start-layer-2")
### Opening/closing web page in Co-Websites ### Opening/closing web page in Co-Websites
``` ```
WA.nav.openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = "", position: number = 0): Promise<CoWebsite> WA.nav.openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = "", position: number, closable: boolean, lazy: boolean): Promise<CoWebsite>
``` ```
Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. `allowApi` allows the webpage to use the "IFrame API" and execute script (it is equivalent to putting the `openWebsiteAllowApi` property in the map). `allowPolicy` grants additional access rights to the iFrame. The `allowPolicy` parameter is turned into an [`allow` feature policy in the iFrame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-allow), position in whitch slot the web page will be open. Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. `allowApi` allows the webpage to use the "IFrame API" and execute script (it is equivalent to putting the `openWebsiteAllowApi` property in the map). `allowPolicy` grants additional access rights to the iFrame. The `allowPolicy` parameter is turned into an [`allow` feature policy in the iFrame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-allow), position in whitch slot the web page will be open, closable allow to close the webpage also you need to close it by the api and lazy
You can have only 5 co-wbesites open simultaneously. it's to add the cowebsite but don't load it.
Example: Example:
```javascript ```javascript
const coWebsite = await WA.nav.openCoWebSite('https://www.wikipedia.org/'); const coWebsite = await WA.nav.openCoWebSite('https://www.wikipedia.org/');
const coWebsiteWorkAdventure = await WA.nav.openCoWebSite('https://workadventu.re/', true, "", 1); const coWebsiteWorkAdventure = await WA.nav.openCoWebSite('https://workadventu.re/', true, "", 1, true, true);
// ... // ...
coWebsite.close(); coWebsite.close();
``` ```

View file

@ -65,3 +65,24 @@ How to use entry point :
* To enter via this entry point, simply add a hash with the entry point name to the URL ("#[_entryPointName_]"). For instance: "`https://workadventu.re/_/global/mymap.com/path/map.json#my-entry-point`". * To enter via this entry point, simply add a hash with the entry point name to the URL ("#[_entryPointName_]"). For instance: "`https://workadventu.re/_/global/mymap.com/path/map.json#my-entry-point`".
* You can of course use the "#" notation in an exit scene URL (so an exit scene URL will point to a given entry scene URL) * You can of course use the "#" notation in an exit scene URL (so an exit scene URL will point to a given entry scene URL)
## Defining destination point with moveTo parameter
We are able to direct a Woka to the desired place immediately after spawn. To make users spawn on an entry point and then, walk automatically to a meeting room, simply add `moveTo` as an additional parameter of URL:
*Use default entry point*
```
.../my_map.json#&moveTo=exit
```
*Define entry point and moveTo parameter like this...*
```
.../my_map.json#start&moveTo=meeting-room
```
*...or like this*
```
.../my_map.json#moveTo=meeting-room&start
```
For this to work, moveTo must be equal to the layer name of interest. This layer should have at least one tile defined. In case of layer having many tiles, user will go to one of them, randomly selected.
![](images/moveTo-layer-example.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View file

@ -1,23 +1,35 @@
FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder
WORKDIR /usr/src
WORKDIR /usr/src/messages
COPY messages . COPY messages .
RUN yarn install && yarn ts-proto RUN yarn install && yarn ts-proto
# we are rebuilding on each deploy to cope with the PUSHER_URL environment URL WORKDIR /usr/src/front
FROM thecodingmachine/nodejs:14-apache COPY front .
COPY --chown=docker:docker front . # move messages to front
COPY --from=builder --chown=docker:docker /usr/src/ts-proto-generated/protos /var/www/html/src/Messages/ts-proto-generated RUN cp -r ../messages/ts-proto-generated/protos/* src/Messages/ts-proto-generated
RUN sed -i 's/import { Observable } from "rxjs";/import type { Observable } from "rxjs";/g' /var/www/html/src/Messages/ts-proto-generated/messages.ts RUN sed -i 's/import { Observable } from "rxjs";/import type { Observable } from "rxjs";/g' src/Messages/ts-proto-generated/messages.ts
COPY --from=builder --chown=docker:docker /usr/src/JsonMessages /var/www/html/src/Messages/JsonMessages RUN cp -r ../messages/JsonMessages/* src/Messages/JsonMessages
RUN yarn install && yarn run typesafe-i18n && yarn build
# Removing the iframe.html file from the final image as this adds a XSS attack. # 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 # iframe.html is only in dev mode to circumvent a limitation
RUN rm dist/iframe.html RUN rm dist/iframe.html
RUN yarn install FROM thecodingmachine/nodejs:14-apache
COPY --from=builder --chown=docker:docker /usr/src/front/dist dist
COPY front/templater.sh .
USER root
RUN DEBIAN_FRONTEND=noninteractive apt-get update \
&& apt-get install -y \
gettext-base \
&& rm -rf /var/lib/apt/lists/*
USER docker
ENV NODE_ENV=production
ENV STARTUP_COMMAND_0="./templater.sh" ENV STARTUP_COMMAND_0="./templater.sh"
ENV STARTUP_COMMAND_1="yarn run build" ENV STARTUP_COMMAND_1="envsubst < dist/env-config.template.js > dist/env-config.js"
ENV APACHE_DOCUMENT_ROOT=dist/ ENV APACHE_DOCUMENT_ROOT=dist/

View file

@ -1,4 +1,6 @@
index.html index.html
index.tmpl.html.tmp
/js/ /js/
/fonts/
style.*.css style.*.css
!env-config.template.js
*.png

27
front/dist/env-config.template.js vendored Normal file
View file

@ -0,0 +1,27 @@
window.env = {
SKIP_RENDER_OPTIMIZATIONS: '${SKIP_RENDER_OPTIMIZATIONS}',
DISABLE_NOTIFICATIONS: '${DISABLE_NOTIFICATIONS}',
PUSHER_URL: '${PUSHER_URL}',
UPLOADER_URL: '${UPLOADER_URL}',
ADMIN_URL: '${ADMIN_URL}',
CONTACT_URL: '${CONTACT_URL}',
PROFILE_URL: '${PROFILE_URL}',
ICON_URL: '${ICON_URL}',
DEBUG_MODE: '${DEBUG_MODE}',
STUN_SERVER: '${STUN_SERVER}',
TURN_SERVER: '${TURN_SERVER}',
TURN_USER: '${TURN_USER}',
TURN_PASSWORD: '${TURN_PASSWORD}',
JITSI_URL: '${JITSI_URL}',
JITSI_PRIVATE_MODE: '${JITSI_PRIVATE_MODE}',
START_ROOM_URL: '${START_ROOM_URL}',
MAX_USERNAME_LENGTH: '${MAX_USERNAME_LENGTH}',
MAX_PER_GROUP: '${MAX_PER_GROUP}',
DISPLAY_TERMS_OF_USE: '${DISPLAY_TERMS_OF_USE}',
POSTHOG_API_KEY: '${POSTHOG_API_KEY}',
POSTHOG_URL: '${POSTHOG_URL}',
NODE_ENV: '${NODE_ENV}',
DISABLE_ANONYMOUS: '${DISABLE_ANONYMOUS}',
OPID_LOGIN_SCREEN_PROVIDER: '${OPID_LOGIN_SCREEN_PROVIDER}',
FALLBACK_LOCALE: '${FALLBACK_LOCALE}',
};

68
front/dist/index.ejs vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,126 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!-- TRACK CODE -->
<!-- END TRACK CODE -->
<link rel="apple-touch-icon" sizes="57x57" href="static/images/favicons/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="static/images/favicons/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="static/images/favicons/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="static/images/favicons/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="static/images/favicons/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="static/images/favicons/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="static/images/favicons/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="static/images/favicons/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="static/images/favicons/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="static/images/favicons/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="static/images/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="static/images/favicons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="static/images/favicons/favicon-16x16.png">
<link rel="manifest" href="static/images/favicons/manifest.json">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-TileImage" content="static/images/favicons/ms-icon-144x144.png">
<meta name="theme-color" content="#000000">
<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="cowebsite-container">
<div id="cowebsite-container-main">
<div id="cowebsite-slot-1">
<div class="actions">
<button type="button" class="nes-btn is-primary expand">></button>
<button type="button" class="nes-btn is-error close">&times;</button>
</div>
</div>
</div>
<div id="cowebsite-container-sub">
<div id="cowebsite-slot-2">
<div class="overlay">
<div class="actions">
<button type="button" title="Close" class="nes-btn is-error close">&times;</button>
</div>
<div class="actions-move">
<button type="button" title="Expand" class="nes-btn is-primary expand">></button>
<button type="button" title="Hightlight" class="nes-btn is-secondary hightlight">&Xi;</button>
</div>
</div>
</div>
<div id="cowebsite-slot-3">
<div class="overlay">
<div class="actions">
<button type="button" title="Close" class="nes-btn is-error close">&times;</button>
</div>
<div class="actions-move">
<button type="button" title="Expand" class="nes-btn is-primary expand">></button>
<button type="button" title="Hightlight" class="nes-btn is-secondary hightlight">&Xi;</button>
</div>
</div>
</div>
<div id="cowebsite-slot-4">
<div class="overlay">
<div class="actions">
<button type="button" title="Close" class="nes-btn is-error close">&times;</button>
</div>
<div class="actions-move">
<button type="button" title="Expand" class="nes-btn is-primary expand">></button>
<button type="button" title="Hightlight" class="nes-btn is-secondary hightlight">&Xi;</button>
</div>
</div>
</div>
</div>
</div>
<div id="svelte-overlay"></div>
<div id="game-overlay" class="game-overlay">
<div id="main-section" class="main-section">
</div>
<aside id="sidebar" class="sidebar">
</aside>
<div id="chat-mode" class="chat-mode three-col" style="display: none;">
</div>
</div>
</div>
<div id="cowebsite" class="cowebsite hidden">
<aside id="cowebsite-aside" class="noselect">
<div id="cowebsite-aside-buttons">
<button class="top-right-btn nes-btn is-error" id="cowebsite-close" alt="close all co-websites">
&times;
</button>
<button class="top-right-btn nes-btn is-primary" id="cowebsite-fullscreen" alt="fullscreen mode">
<img id="cowebsite-fullscreen-close" style="display: none;" src="resources/logos/fullscreen-exit.svg"/>
<img id="cowebsite-fullscreen-open" src="resources/logos/fullscreen.svg"/>
</button>
</div>
<div id="cowebsite-aside-holder">
<img src="/static/images/menu.svg" alt="hold to resize"/>
</div>
<div id="cowebsite-sub-icons"></div>
</aside>
<main id="cowebsite-slot-0"></main>
</div>
<div id="cowebsite-buffer"></div>
</div>
<div id="activeScreenSharing" class="active-screen-sharing active">
</div>
<audio id="report-message">
<source src="/resources/objects/report-message.mp3" type="audio/mp3">
</audio>
</body>
</html>

View file

@ -0,0 +1 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><defs><image width="12" height="14" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAOAQMAAAAhc2+vAAAAAXNSR0IB2cksfwAAAAZQTFRFAAAA////pdmf3QAAAAJ0Uk5TAP9bkSK1AAAAJUlEQVR4nGNgOMDAoMDw/wMDCDgwMDQwQIAASBgE/j8ACRswAACLjwYPIknTggAAAABJRU5ErkJggg=="/><image width="12" height="12" id="img2" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMAgMAAAArG7R0AAAAAXNSR0IB2cksfwAAAAlQTFRFAAAA/////wAAzV63nAAAAAN0Uk5TAP8BHlUJOgAAACJJREFUeJxjYGAQYGBgYGEIDQ0F0xA+BwMDEi80NASqigEAOD8CVqGVgwsAAAAASUVORK5CYII="/></defs><style></style><use href="#img1" x="2" y="1" /><use href="#img2" x="2" y="2" /></svg>

After

Width:  |  Height:  |  Size: 717 B

View file

@ -15,6 +15,7 @@
"@typescript-eslint/eslint-plugin": "^5.6.0", "@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0", "@typescript-eslint/parser": "^5.6.0",
"css-loader": "^5.2.4", "css-loader": "^5.2.4",
"css-minimizer-webpack-plugin": "^3.3.1",
"eslint": "^8.4.1", "eslint": "^8.4.1",
"eslint-plugin-svelte3": "^3.2.1", "eslint-plugin-svelte3": "^3.2.1",
"fork-ts-checker-webpack-plugin": "^6.5.0", "fork-ts-checker-webpack-plugin": "^6.5.0",

View file

@ -6,13 +6,14 @@ export const isOpenCoWebsiteEvent = new tg.IsInterface()
allowApi: tg.isOptional(tg.isBoolean), allowApi: tg.isOptional(tg.isBoolean),
allowPolicy: tg.isOptional(tg.isString), allowPolicy: tg.isOptional(tg.isString),
position: tg.isOptional(tg.isNumber), position: tg.isOptional(tg.isNumber),
closable: tg.isOptional(tg.isBoolean),
lazy: tg.isOptional(tg.isBoolean),
}) })
.get(); .get();
export const isCoWebsite = new tg.IsInterface() export const isCoWebsite = new tg.IsInterface()
.withProperties({ .withProperties({
id: tg.isString, id: tg.isString,
position: tg.isNumber,
}) })
.get(); .get();

View file

@ -8,7 +8,6 @@ import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent";
import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent"; import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent";
import { scriptUtils } from "./ScriptUtils"; import { scriptUtils } from "./ScriptUtils";
import { isGoToPageEvent } from "./Events/GoToPageEvent"; import { isGoToPageEvent } from "./Events/GoToPageEvent";
import { isCloseCoWebsite, CloseCoWebsiteEvent } from "./Events/CloseCoWebsiteEvent";
import { import {
IframeErrorAnswerEvent, IframeErrorAnswerEvent,
IframeQueryMap, IframeQueryMap,

View file

@ -1,4 +1,3 @@
import { coWebsiteManager, CoWebsite } from "../WebRtc/CoWebsiteManager";
import { playersStore } from "../Stores/PlayersStore"; import { playersStore } from "../Stores/PlayersStore";
import { chatMessagesStore } from "../Stores/ChatStore"; import { chatMessagesStore } from "../Stores/ChatStore";
import type { ChatEvent } from "./Events/ChatEvent"; import type { ChatEvent } from "./Events/ChatEvent";

View file

@ -1,7 +1,7 @@
import { IframeApiContribution, sendToWorkadventure, queryWorkadventure } from "./IframeApiContribution"; import { IframeApiContribution, sendToWorkadventure, queryWorkadventure } from "./IframeApiContribution";
export class CoWebsite { export class CoWebsite {
constructor(private readonly id: string, public readonly position: number) {} constructor(private readonly id: string) {}
close() { close() {
return queryWorkadventure({ return queryWorkadventure({
@ -41,7 +41,14 @@ export class WorkadventureNavigationCommands extends IframeApiContribution<Worka
}); });
} }
async openCoWebSite(url: string, allowApi?: boolean, allowPolicy?: string, position?: number): Promise<CoWebsite> { async openCoWebSite(
url: string,
allowApi?: boolean,
allowPolicy?: string,
position?: number,
closable?: boolean,
lazy?: boolean
): Promise<CoWebsite> {
const result = await queryWorkadventure({ const result = await queryWorkadventure({
type: "openCoWebsite", type: "openCoWebsite",
data: { data: {
@ -49,9 +56,11 @@ export class WorkadventureNavigationCommands extends IframeApiContribution<Worka
allowApi, allowApi,
allowPolicy, allowPolicy,
position, position,
closable,
lazy,
}, },
}); });
return new CoWebsite(result.id, result.position); return new CoWebsite(result.id);
} }
async getCoWebSites(): Promise<CoWebsite[]> { async getCoWebSites(): Promise<CoWebsite[]> {
@ -59,7 +68,7 @@ export class WorkadventureNavigationCommands extends IframeApiContribution<Worka
type: "getCoWebsites", type: "getCoWebsites",
data: undefined, data: undefined,
}); });
return result.map((cowebsiteEvent) => new CoWebsite(cowebsiteEvent.id, cowebsiteEvent.position)); return result.map((cowebsiteEvent) => new CoWebsite(cowebsiteEvent.id));
} }
/** /**

View file

@ -1,171 +1,52 @@
<script lang="typescript"> <script lang="typescript">
import MenuIcon from "./Menu/MenuIcon.svelte";
import { menuIconVisiblilityStore, menuVisiblilityStore } from "../Stores/MenuStore";
import { emoteMenuStore } from "../Stores/EmoteStore";
import { enableCameraSceneVisibilityStore } from "../Stores/MediaStore";
import 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 Chat from "./Chat/Chat.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 type { Game } from "../Phaser/Game/Game";
import { chatVisibilityStore } from "../Stores/ChatStore"; import { chatVisibilityStore } from "../Stores/ChatStore";
import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore"; import { customCharacterSceneVisibleStore } from "../Stores/CustomCharacterStore";
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte"; import { errorStore } from "../Stores/ErrorStore";
import { showLimitRoomModalStore, showShareLinkMapModalStore } from "../Stores/ModalStore"; import { loginSceneVisibleStore } from "../Stores/LoginSceneStore";
import LimitRoomModal from "./Modal/LimitRoomModal.svelte"; import { enableCameraSceneVisibilityStore } from "../Stores/MediaStore";
import ShareLinkMapModal from "./Modal/ShareLinkMapModal.svelte"; import { selectCharacterSceneVisibleStore } from "../Stores/SelectCharacterStore";
import AudioPlaying from "./UI/AudioPlaying.svelte"; import { selectCompanionSceneVisibleStore } from "../Stores/SelectCompanionStore";
import { soundPlayingStore } from "../Stores/SoundPlayingStore"; import Chat from "./Chat/Chat.svelte";
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
import LoginScene from "./Login/LoginScene.svelte";
import MainLayout from "./MainLayout.svelte";
import SelectCharacterScene from "./selectCharacter/SelectCharacterScene.svelte";
import SelectCompanionScene from "./SelectCompanion/SelectCompanionScene.svelte";
import ErrorDialog from "./UI/ErrorDialog.svelte"; import ErrorDialog from "./UI/ErrorDialog.svelte";
import Menu from "./Menu/Menu.svelte";
import EmoteMenu from "./EmoteMenu/EmoteMenu.svelte";
import VideoOverlay from "./Video/VideoOverlay.svelte";
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
import BanMessageContainer from "./TypeMessage/BanMessageContainer.svelte";
import TextMessageContainer from "./TypeMessage/TextMessageContainer.svelte";
import { banMessageStore } from "../Stores/TypeMessageStore/BanMessageStore";
import { textMessageStore } from "../Stores/TypeMessageStore/TextMessageStore";
import { warningContainerStore } from "../Stores/MenuStore";
import WarningContainer from "./WarningContainer/WarningContainer.svelte";
import { layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore";
import LayoutManager from "./LayoutManager/LayoutManager.svelte";
import { audioManagerVisibilityStore } from "../Stores/AudioManagerStore";
import AudioManager from "./AudioManager/AudioManager.svelte";
import { showReportScreenStore, userReportEmpty } from "../Stores/ShowReportScreenStore";
import ReportMenu from "./ReportMenu/ReportMenu.svelte";
import { followStateStore } from "../Stores/FollowStore";
import { peerStore } from "../Stores/PeerStore";
import FollowMenu from "./FollowMenu/FollowMenu.svelte";
import ActionsMenu from './ActionsMenu/ActionsMenu.svelte';
import { actionsMenuStore } from '../Stores/ActionsMenuStore';
export let game: Game; export let game: Game;
</script> </script>
<div> {#if $errorStore.length > 0}
{#if $loginSceneVisibleStore} <div>
<div class="scrollable"> <ErrorDialog />
<LoginScene {game} /> </div>
</div> {:else if $loginSceneVisibleStore}
{/if} <div class="scrollable">
{#if $selectCharacterSceneVisibleStore} <LoginScene {game} />
<div> </div>
<SelectCharacterScene {game} /> {:else if $selectCharacterSceneVisibleStore}
</div> <div>
{/if} <SelectCharacterScene {game} />
{#if $customCharacterSceneVisibleStore} </div>
<div> {:else if $customCharacterSceneVisibleStore}
<CustomCharacterScene {game} /> <div>
</div> <CustomCharacterScene {game} />
{/if} </div>
{#if $selectCompanionSceneVisibleStore} {:else if $selectCompanionSceneVisibleStore}
<div> <div>
<SelectCompanionScene {game} /> <SelectCompanionScene {game} />
</div> </div>
{/if} {:else if $enableCameraSceneVisibilityStore}
{#if $enableCameraSceneVisibilityStore} <div class="scrollable">
<div class="scrollable"> <EnableCameraScene {game} />
<EnableCameraScene {game} /> </div>
</div> {:else}
{/if} <MainLayout />
{#if $banMessageStore.length > 0}
<div>
<BanMessageContainer />
</div>
{:else if $textMessageStore.length > 0}
<div>
<TextMessageContainer />
</div>
{/if}
{#if $soundPlayingStore}
<div>
<AudioPlaying url={$soundPlayingStore} />
</div>
{/if}
{#if $audioManagerVisibilityStore}
<div>
<AudioManager />
</div>
{/if}
{#if $layoutManagerVisibilityStore}
<div>
<LayoutManager />
</div>
{/if}
{#if $showReportScreenStore !== userReportEmpty}
<div>
<ReportMenu />
</div>
{/if}
{#if $followStateStore !== "off" || $peerStore.size > 0}
<div>
<FollowMenu />
</div>
{/if}
{#if $menuIconVisiblilityStore}
<div>
<MenuIcon />
</div>
{/if}
{#if $menuVisiblilityStore}
<div>
<Menu />
</div>
{/if}
{#if $emoteMenuStore}
<div>
<EmoteMenu />
</div>
{/if}
{#if $gameOverlayVisibilityStore}
<div>
<VideoOverlay />
<MyCamera />
<CameraControls />
</div>
{/if}
{#if $helpCameraSettingsVisibleStore}
<div>
<HelpCameraSettingsPopup />
</div>
{/if}
{#if $showLimitRoomModalStore}
<div>
<LimitRoomModal />
</div>
{/if}
{#if $showShareLinkMapModalStore}
<div>
<ShareLinkMapModal />
</div>
{/if}
{#if $requestVisitCardsStore}
<VisitCard visitCardUrl={$requestVisitCardsStore} />
{/if}
{#if $actionsMenuStore}
<ActionsMenu />
{/if}
{#if $errorStore.length > 0}
<div>
<ErrorDialog />
</div>
{/if}
{#if $chatVisibilityStore} {#if $chatVisibilityStore}
<Chat /> <Chat />
{/if} {/if}
{#if $warningContainerStore} {/if}
<WarningContainer />
{/if}
</div>

View file

@ -157,13 +157,16 @@
<style lang="scss"> <style lang="scss">
div.main-audio-manager.nes-container.is-rounded { div.main-audio-manager.nes-container.is-rounded {
position: relative; position: absolute;
top: 0.5rem; top: 1%;
max-height: clamp(150px, 10vh, 15vh); //replace @media for small screen max-height: clamp(150px, 10vh, 15vh); //replace @media for small screen
width: clamp(200px, 15vw, 15vw); width: clamp(200px, 15vw, 15vw);
padding: 3px 3px; padding: 3px 3px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
left: 0;
right: 0;
z-index: 550;
background-color: rgb(0, 0, 0, 0.5); background-color: rgb(0, 0, 0, 0.5);
display: grid; display: grid;

View file

@ -9,10 +9,15 @@
import microphoneCloseImg from "./images/microphone-close.svg"; import microphoneCloseImg from "./images/microphone-close.svg";
import layoutPresentationImg from "./images/layout-presentation.svg"; import layoutPresentationImg from "./images/layout-presentation.svg";
import layoutChatImg from "./images/layout-chat.svg"; import layoutChatImg from "./images/layout-chat.svg";
import { layoutModeStore } from "../Stores/StreamableCollectionStore"; import followImg from "./images/follow.svg";
import { LayoutMode } from "../WebRtc/LayoutManager"; import { LayoutMode } from "../WebRtc/LayoutManager";
import { peerStore } from "../Stores/PeerStore"; import { peerStore } from "../Stores/PeerStore";
import { onDestroy } from "svelte"; import { onDestroy } from "svelte";
import { embedScreenLayout } from "../Stores/EmbedScreensStore";
import { followRoleStore, followStateStore, followUsersStore } from "../Stores/FollowStore";
import { gameManager } from "../Phaser/Game/GameManager";
const gameScene = gameManager.getCurrentGameScene();
function screenSharingClick(): void { function screenSharingClick(): void {
if (isSilent) return; if (isSilent) return;
@ -42,10 +47,26 @@
} }
function switchLayoutMode() { function switchLayoutMode() {
if ($layoutModeStore === LayoutMode.Presentation) { if ($embedScreenLayout === LayoutMode.Presentation) {
$layoutModeStore = LayoutMode.VideoChat; $embedScreenLayout = LayoutMode.VideoChat;
} else { } else {
$layoutModeStore = LayoutMode.Presentation; $embedScreenLayout = LayoutMode.Presentation;
}
}
function followClick() {
switch ($followStateStore) {
case "off":
gameScene.connection?.emitFollowRequest();
followRoleStore.set("leader");
followStateStore.set("active");
break;
case "requesting":
case "active":
case "ending":
gameScene.connection?.emitFollowAbort();
followUsersStore.stopFollowing();
break;
} }
} }
@ -56,40 +77,162 @@
onDestroy(unsubscribeIsSilent); onDestroy(unsubscribeIsSilent);
</script> </script>
<div> <div class="btn-cam-action">
<div class="btn-cam-action"> <div class="btn-layout" on:click={switchLayoutMode} class:hide={$peerStore.size === 0}>
<div class="btn-layout" on:click={switchLayoutMode} class:hide={$peerStore.size === 0}> {#if $embedScreenLayout === LayoutMode.Presentation}
{#if $layoutModeStore === LayoutMode.Presentation} <img class="noselect" src={layoutPresentationImg} style="padding: 2px" alt="Switch to mosaic mode" />
<img src={layoutPresentationImg} style="padding: 2px" alt="Switch to mosaic mode" /> {:else}
{:else} <img class="noselect" src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode" />
<img src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode" /> {/if}
{/if} </div>
</div>
<div <div
class="btn-monitor" class="btn-follow"
on:click={screenSharingClick} class:hide={($peerStore.size === 0 && $followStateStore === "off") || isSilent}
class:hide={!$screenSharingAvailableStore || isSilent} class:disabled={$followStateStore !== "off"}
class:enabled={$requestedScreenSharingState} on:click={followClick}
> >
{#if $requestedScreenSharingState && !isSilent} <img class="noselect" src={followImg} alt="" />
<img src={monitorImg} alt="Start screen sharing" /> </div>
{:else}
<img src={monitorCloseImg} alt="Stop screen sharing" /> <div
{/if} class="btn-monitor"
</div> on:click={screenSharingClick}
<div class="btn-video" on:click={cameraClick} class:disabled={!$requestedCameraState || isSilent}> class:hide={!$screenSharingAvailableStore || isSilent}
{#if $requestedCameraState && !isSilent} class:enabled={$requestedScreenSharingState}
<img src={cinemaImg} alt="Turn on webcam" /> >
{:else} {#if $requestedScreenSharingState && !isSilent}
<img src={cinemaCloseImg} alt="Turn off webcam" /> <img class="noselect" src={monitorImg} alt="Start screen sharing" />
{/if} {:else}
</div> <img class="noselect" src={monitorCloseImg} alt="Stop screen sharing" />
<div class="btn-micro" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState || isSilent}> {/if}
{#if $requestedMicrophoneState && !isSilent} </div>
<img src={microphoneImg} alt="Turn on microphone" />
{:else} <div class="btn-video" on:click={cameraClick} class:disabled={!$requestedCameraState || isSilent}>
<img src={microphoneCloseImg} alt="Turn off microphone" /> {#if $requestedCameraState && !isSilent}
{/if} <img class="noselect" src={cinemaImg} alt="Turn on webcam" />
</div> {:else}
<img class="noselect" src={cinemaCloseImg} alt="Turn off webcam" />
{/if}
</div>
<div class="btn-micro" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState || isSilent}>
{#if $requestedMicrophoneState && !isSilent}
<img class="noselect" src={microphoneImg} alt="Turn on microphone" />
{:else}
<img class="noselect" src={microphoneCloseImg} alt="Turn off microphone" />
{/if}
</div> </div>
</div> </div>
<style lang="scss">
@import "../../style/breakpoints.scss";
.btn-cam-action {
pointer-events: all;
position: absolute;
display: inline-flex;
bottom: 10px;
right: 15px;
width: 360px;
height: 40px;
text-align: center;
align-content: center;
justify-content: flex-end;
z-index: 251;
&:hover {
div.hide {
transform: translateY(60px);
}
}
}
/*btn animation*/
.btn-cam-action div {
cursor: url("../../style/images/cursor_pointer.png"), pointer;
display: flex;
align-items: center;
justify-content: center;
border: solid 0px black;
width: 44px;
height: 44px;
background: #666;
box-shadow: 2px 2px 24px #444;
border-radius: 48px;
transform: translateY(15px);
transition-timing-function: ease-in-out;
transition: all 0.3s;
margin: 0 4%;
&.hide {
transform: translateY(60px);
}
}
.btn-cam-action div.disabled {
background: #d75555;
}
.btn-cam-action div.enabled {
background: #73c973;
}
.btn-cam-action:hover div {
transform: translateY(0);
}
.btn-cam-action div:hover {
background: #407cf7;
box-shadow: 4px 4px 48px #666;
transition: 120ms;
}
.btn-micro {
pointer-events: auto;
}
.btn-video {
pointer-events: auto;
transition: all 0.25s;
}
.btn-monitor {
pointer-events: auto;
}
.btn-layout {
pointer-events: auto;
transition: all 0.15s;
}
.btn-cam-action div img {
height: 22px;
width: 30px;
position: relative;
cursor: url("../../style/images/cursor_pointer.png"), pointer;
}
.btn-follow {
pointer-events: auto;
img {
filter: brightness(0) invert(1);
}
}
@media (hover: none) {
/**
* If we cannot hover over elements, let's display camera button in full.
*/
.btn-cam-action {
div {
transform: translateY(0px);
}
}
}
@include media-breakpoint-up(sm) {
.btn-cam-action {
right: 0;
width: 100%;
height: 40%;
max-height: 40px;
div {
width: 20%;
max-height: 44px;
}
}
}
</style>

View file

@ -43,7 +43,7 @@
<svelte:window on:keydown={onKeyDown} on:click={onClick} /> <svelte:window on:keydown={onKeyDown} on:click={onClick} />
<aside class="chatWindow" transition:fly={{ x: -1000, duration: 500 }} bind:this={chatWindowElement}> <aside class="chatWindow" transition:fly={{ x: -1000, duration: 500 }} bind:this={chatWindowElement}>
<p class="close-icon" on:click={closeChat}>&times</p> <p class="close-icon noselect" on:click={closeChat}>&times</p>
<section class="messagesList" bind:this={listDom}> <section class="messagesList" bind:this={listDom}>
<ul> <ul>
<li><p class="system-text">{$LL.chat.intro()}</p></li> <li><p class="system-text">{$LL.chat.intro()}</p></li>
@ -78,7 +78,7 @@
} }
aside.chatWindow { aside.chatWindow {
z-index: 100; z-index: 1000;
pointer-events: auto; pointer-events: auto;
position: absolute; position: absolute;
top: 0; top: 0;

View file

@ -83,6 +83,8 @@
</form> </form>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
form.customCharacterScene { form.customCharacterScene {
font-family: "Press Start 2P"; font-family: "Press Start 2P";
pointer-events: auto; pointer-events: auto;
@ -129,7 +131,7 @@
} }
} }
@media only screen and (max-width: 800px) { @include media-breakpoint-up(md) {
form.customCharacterScene button.customCharacterSceneButtonLeft { form.customCharacterScene button.customCharacterSceneButtonLeft {
left: 5vw; left: 5vw;
} }

View file

@ -0,0 +1,32 @@
<script lang="typescript">
import type { EmbedScreen } from "../../Stores/EmbedScreensStore";
import { streamableCollectionStore } from "../../Stores/StreamableCollectionStore";
import MediaBox from "../Video/MediaBox.svelte";
export let highlightedEmbedScreen: EmbedScreen | null;
export let full = false;
$: clickable = !full;
</script>
<aside class="cameras-container" class:full>
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
{#if !highlightedEmbedScreen || highlightedEmbedScreen.type !== "streamable" || (highlightedEmbedScreen.type === "streamable" && highlightedEmbedScreen.embed !== peer)}
<MediaBox streamable={peer} isClickable={clickable} />
{/if}
{/each}
</aside>
<style lang="scss">
.cameras-container {
flex: 0 0 25%;
overflow-y: auto;
overflow-x: hidden;
&:first-child {
margin-top: 2%;
}
&.full {
flex: 0 0 100%;
}
}
</style>

View file

@ -0,0 +1,272 @@
<script lang="typescript">
import { onMount } from "svelte";
import { ICON_URL } from "../../Enum/EnvironmentVariable";
import { coWebsitesNotAsleep, mainCoWebsite } from "../../Stores/CoWebsiteStore";
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
import type { CoWebsite } from "../../WebRtc/CoWebsiteManager";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
export let index: number;
export let coWebsite: CoWebsite;
export let vertical: boolean;
let icon: HTMLImageElement;
let iconLoaded = false;
let state = coWebsite.state;
const coWebsiteUrl = coWebsite.iframe.src;
const urlObject = new URL(coWebsiteUrl);
onMount(() => {
icon.src = `${ICON_URL}/icon?url=${urlObject.hostname}&size=64..96..256&fallback_icon_color=14304c`;
icon.alt = urlObject.hostname;
icon.onload = () => {
iconLoaded = true;
};
});
async function onClick() {
if (vertical) {
coWebsiteManager.goToMain(coWebsite);
} else if ($mainCoWebsite) {
if ($mainCoWebsite.iframe.id === coWebsite.iframe.id) {
const coWebsites = $coWebsitesNotAsleep;
const newMain = $highlightedEmbedScreen ?? coWebsites.length > 1 ? coWebsites[1] : undefined;
if (newMain) {
coWebsiteManager.goToMain(coWebsite);
}
} else {
highlightedEmbedScreen.toggleHighlight({
type: "cowebsite",
embed: coWebsite,
});
}
}
if ($state === "asleep") {
await coWebsiteManager.loadCoWebsite(coWebsite);
}
coWebsiteManager.resizeAllIframes();
}
function noDrag() {
return false;
}
let isHighlight: boolean = false;
let isMain: boolean = false;
$: {
isMain = $mainCoWebsite !== undefined && $mainCoWebsite.iframe === coWebsite.iframe;
isHighlight =
$highlightedEmbedScreen !== null &&
$highlightedEmbedScreen.type === "cowebsite" &&
$highlightedEmbedScreen.embed.iframe === coWebsite.iframe;
}
</script>
<div
id={"cowebsite-thumbnail-" + index}
class="cowebsite-thumbnail nes-pointer"
class:asleep={$state === "asleep"}
class:loading={$state === "loading"}
class:ready={$state === "ready"}
class:displayed={isMain || isHighlight}
class:vertical
on:click={onClick}
>
<img
class="cowebsite-icon noselect nes-pointer"
class:hide={!iconLoaded}
bind:this={icon}
on:dragstart|preventDefault={noDrag}
alt=""
/>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
class="cowebsite-icon"
class:hide={iconLoaded}
style="margin: auto; background: rgba(0, 0, 0, 0) none repeat scroll 0% 0%; shape-rendering: auto;"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid"
>
<rect x="19" y="19" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0s"
calcMode="discrete"
/>
</rect><rect x="40" y="19" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.125s"
calcMode="discrete"
/>
</rect><rect x="61" y="19" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.25s"
calcMode="discrete"
/>
</rect><rect x="19" y="40" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.875s"
calcMode="discrete"
/>
</rect><rect x="61" y="40" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.375s"
calcMode="discrete"
/>
</rect><rect x="19" y="61" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.75s"
calcMode="discrete"
/>
</rect><rect x="40" y="61" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.625s"
calcMode="discrete"
/>
</rect><rect x="61" y="61" width="20" height="20" fill="#14304c">
<animate
attributeName="fill"
values="#365dff;#14304c;#14304c"
keyTimes="0;0.125;1"
dur="1s"
repeatCount="indefinite"
begin="0.5s"
calcMode="discrete"
/>
</rect>
</svg>
</div>
<style lang="scss">
.cowebsite-thumbnail {
position: relative;
padding: 0;
background-color: rgba(#000000, 0.6);
margin: 12px;
margin-top: auto;
margin-bottom: auto;
&::before {
content: "";
position: absolute;
width: 58px;
height: 58px;
left: -8px;
top: -8px;
margin: 4px;
border-style: solid;
border-width: 4px;
border-image-slice: 3;
border-image-width: 3;
border-image-repeat: stretch;
border-image-source: url('data:image/svg+xml;utf8,<?xml version="1.0" encoding="UTF-8" ?><svg version="1.1" width="8" height="8" xmlns="http://www.w3.org/2000/svg"><path d="M3 1 h1 v1 h-1 z M4 1 h1 v1 h-1 z M2 2 h1 v1 h-1 z M5 2 h1 v1 h-1 z M1 3 h1 v1 h-1 z M6 3 h1 v1 h-1 z M1 4 h1 v1 h-1 z M6 4 h1 v1 h-1 z M2 5 h1 v1 h-1 z M5 5 h1 v1 h-1 z M3 6 h1 v1 h-1 z M4 6 h1 v1 h-1 z" fill="rgb(33,37,41)" /></svg>');
border-image-outset: 1;
}
&.vertical {
margin: 7px;
&::before {
width: 48px;
height: 48px;
}
.cowebsite-icon {
width: 40px;
height: 40px;
}
}
&.displayed {
&:not(.vertical) {
animation: activeThumbnail 300ms ease-in 0s forwards;
}
}
&.asleep {
filter: grayscale(100%);
--webkit-filter: grayscale(100%);
}
&.loading {
animation: 2500ms ease-in-out 0s infinite alternate backgroundLoading;
}
&.ready {
&::before {
border-image-source: url('data:image/svg+xml;utf8,<?xml version="1.0" encoding="UTF-8" ?><svg version="1.1" width="8" height="8" xmlns="http://www.w3.org/2000/svg"><path d="M3 1 h1 v1 h-1 z M4 1 h1 v1 h-1 z M2 2 h1 v1 h-1 z M5 2 h1 v1 h-1 z M1 3 h1 v1 h-1 z M6 3 h1 v1 h-1 z M1 4 h1 v1 h-1 z M6 4 h1 v1 h-1 z M2 5 h1 v1 h-1 z M5 5 h1 v1 h-1 z M3 6 h1 v1 h-1 z M4 6 h1 v1 h-1 z" fill="rgb(38, 74, 110)" /></svg>');
}
}
@keyframes backgroundLoading {
0% {
background-color: rgba(#000000, 0.6);
}
100% {
background-color: #25598e;
}
}
@keyframes activeThumbnail {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-15px);
}
}
.cowebsite-icon {
width: 50px;
height: 50px;
object-fit: cover;
&.hide {
display: none;
}
}
}
</style>

View file

@ -0,0 +1,42 @@
<script lang="typescript">
import { coWebsites } from "../../Stores/CoWebsiteStore";
import CoWebsiteThumbnail from "./CoWebsiteThumbnailSlot.svelte";
export let vertical = false;
</script>
{#if $coWebsites.length > 0}
<div id="cowebsite-thumbnail-container" class:vertical>
{#each [...$coWebsites.values()] as coWebsite, index (coWebsite.iframe.id)}
<CoWebsiteThumbnail {index} {coWebsite} {vertical} />
{/each}
</div>
{/if}
<style lang="scss">
#cowebsite-thumbnail-container {
pointer-events: all;
height: 100px;
width: 100%;
display: flex;
position: absolute;
bottom: 5px;
left: 2%;
overflow-x: auto;
overflow-y: hidden;
&.vertical {
height: auto !important;
width: auto !important;
bottom: auto !important;
left: auto !important;
position: relative;
overflow-x: hidden;
overflow-y: auto;
flex-direction: column;
align-items: center;
padding-top: 4px;
padding-bottom: 4px;
}
}
</style>

View file

@ -0,0 +1,22 @@
<script lang="typescript">
import PresentationLayout from "./Layouts/PresentationLayout.svelte";
import MozaicLayout from "./Layouts/MozaicLayout.svelte";
import { LayoutMode } from "../../WebRtc/LayoutManager";
import { embedScreenLayout } from "../../Stores/EmbedScreensStore";
</script>
<div id="embedScreensContainer">
{#if $embedScreenLayout === LayoutMode.Presentation}
<PresentationLayout />
{:else}
<MozaicLayout />
{/if}
</div>
<style lang="scss">
#embedScreensContainer {
display: flex;
padding-top: 2%;
height: 100%;
}
</style>

View file

@ -0,0 +1,61 @@
<script lang="ts">
import { onMount } from "svelte";
import { highlightedEmbedScreen } from "../../../Stores/EmbedScreensStore";
import { streamableCollectionStore } from "../../../Stores/StreamableCollectionStore";
import MediaBox from "../../Video/MediaBox.svelte";
let layoutDom: HTMLDivElement;
const resizeObserver = new ResizeObserver(() => {});
onMount(() => {
resizeObserver.observe(layoutDom);
highlightedEmbedScreen.removeHighlight();
});
</script>
<div id="mozaic-layout" bind:this={layoutDom}>
<div
class="media-container"
class:full-width={$streamableCollectionStore.size === 1 || $streamableCollectionStore.size === 2}
class:quarter={$streamableCollectionStore.size === 3 || $streamableCollectionStore.size === 4}
>
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
<MediaBox
streamable={peer}
mozaicFullWidth={$streamableCollectionStore.size === 1 || $streamableCollectionStore.size === 2}
mozaicQuarter={$streamableCollectionStore.size === 3 || $streamableCollectionStore.size === 4}
/>
{/each}
</div>
</div>
<style lang="scss">
#mozaic-layout {
height: 100%;
width: 100%;
overflow-y: auto;
overflow-x: hidden;
.media-container {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: 33.3% 33.3% 33.3%;
align-items: center;
justify-content: center;
overflow-y: auto;
overflow-x: hidden;
&.full-width {
grid-template-columns: 100%;
grid-template-rows: 50% 50%;
}
&.quarter {
grid-template-columns: 50% 50%;
grid-template-rows: 50% 50%;
}
}
}
</style>

View file

@ -0,0 +1,143 @@
<script lang="ts">
import { highlightedEmbedScreen } from "../../../Stores/EmbedScreensStore";
import CamerasContainer from "../CamerasContainer.svelte";
import MediaBox from "../../Video/MediaBox.svelte";
import { coWebsiteManager } from "../../../WebRtc/CoWebsiteManager";
import { afterUpdate, onMount } from "svelte";
import { isMediaBreakpointDown, isMediaBreakpointUp } from "../../../Utils/BreakpointsUtils";
import { peerStore } from "../../../Stores/PeerStore";
function closeCoWebsite() {
if ($highlightedEmbedScreen?.type === "cowebsite") {
if ($highlightedEmbedScreen.embed.closable) {
coWebsiteManager.closeCoWebsite($highlightedEmbedScreen.embed).catch(() => {
console.error("Error during co-website highlighted closing");
});
} else {
coWebsiteManager.unloadCoWebsite($highlightedEmbedScreen.embed).catch(() => {
console.error("Error during co-website highlighted unloading");
});
}
}
}
afterUpdate(() => {
if ($highlightedEmbedScreen) {
coWebsiteManager.resizeAllIframes();
}
});
let layoutDom: HTMLDivElement;
let displayCoWebsiteContainer = isMediaBreakpointDown("lg");
let displayFullMedias = isMediaBreakpointUp("sm");
const resizeObserver = new ResizeObserver(() => {
displayCoWebsiteContainer = isMediaBreakpointDown("lg");
displayFullMedias = isMediaBreakpointUp("sm");
if (!displayCoWebsiteContainer && $highlightedEmbedScreen && $highlightedEmbedScreen.type === "cowebsite") {
highlightedEmbedScreen.removeHighlight();
}
if (displayFullMedias) {
highlightedEmbedScreen.removeHighlight();
}
});
onMount(() => {
resizeObserver.observe(layoutDom);
});
</script>
<div id="presentation-layout" bind:this={layoutDom} class:full-medias={displayFullMedias}>
{#if displayFullMedias}
<div id="full-medias">
<CamerasContainer full={true} highlightedEmbedScreen={$highlightedEmbedScreen} />
</div>
{:else}
<div id="embed-left-block" class:full={$peerStore.size === 0}>
<div id="main-embed-screen">
{#if $highlightedEmbedScreen}
{#if $highlightedEmbedScreen.type === "streamable"}
{#key $highlightedEmbedScreen.embed.uniqueId}
<MediaBox
isHightlighted={true}
isClickable={true}
streamable={$highlightedEmbedScreen.embed}
/>
{/key}
{:else if $highlightedEmbedScreen.type === "cowebsite"}
{#key $highlightedEmbedScreen.embed.iframe.id}
<div
id={"cowebsite-slot-" + $highlightedEmbedScreen.embed.iframe.id}
class="highlighted-cowebsite nes-container is-rounded"
>
<div class="actions">
<button type="button" class="nes-btn is-error close" on:click={closeCoWebsite}
>&times;</button
>
</div>
</div>
{/key}
{/if}
{/if}
</div>
</div>
{#if $peerStore.size > 0}
<CamerasContainer highlightedEmbedScreen={$highlightedEmbedScreen} />
{/if}
{/if}
</div>
<style lang="scss">
#presentation-layout {
height: 100%;
width: 100%;
display: flex;
&.full-medias {
overflow-y: auto;
overflow-x: hidden;
}
}
#embed-left-block {
display: flex;
flex-direction: column;
flex: 0 0 75%;
height: 100%;
width: 75%;
&.full {
flex: 0 0 98% !important;
width: 98% !important;
}
}
#main-embed-screen {
height: 100%;
margin-bottom: 3%;
.highlighted-cowebsite {
height: 100% !important;
width: 96%;
background-color: rgba(#000000, 0.6);
margin: 0 !important;
.actions {
z-index: 200;
position: relative;
display: flex;
flex-direction: row;
justify-content: end;
gap: 2%;
button {
pointer-events: all;
}
}
}
}
</style>

View file

@ -3,7 +3,8 @@
import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore"; import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore";
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
import { EmojiButton } from "@joeattardi/emoji-button"; import { EmojiButton } from "@joeattardi/emoji-button";
import { isMobile } from "../../Enum/EnvironmentVariable"; import LL from "../../i18n/i18n-svelte";
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
let emojiContainer: HTMLElement; let emojiContainer: HTMLElement;
let picker: EmojiButton; let picker: EmojiButton;
@ -15,10 +16,31 @@
rootElement: emojiContainer, rootElement: emojiContainer,
styleProperties: { styleProperties: {
"--font": "Press Start 2P", "--font": "Press Start 2P",
"--text-color": "whitesmoke",
"--secondary-text-color": "whitesmoke",
"--category-button-color": "whitesmoke",
}, },
emojisPerRow: isMobile() ? 6 : 8, emojisPerRow: isMediaBreakpointUp("md") ? 6 : 8,
autoFocusSearch: false, autoFocusSearch: false,
style: "twemoji", style: "twemoji",
showPreview: false,
i18n: {
search: $LL.emoji.search(),
categories: {
recents: $LL.emoji.categories.recents(),
smileys: $LL.emoji.categories.smileys(),
people: $LL.emoji.categories.people(),
animals: $LL.emoji.categories.animals(),
food: $LL.emoji.categories.food(),
activities: $LL.emoji.categories.activities(),
travel: $LL.emoji.categories.travel(),
objects: $LL.emoji.categories.objects(),
symbols: $LL.emoji.categories.symbols(),
flags: $LL.emoji.categories.flags(),
custom: $LL.emoji.categories.custom(),
},
notFound: $LL.emoji.notFound(),
},
}); });
//the timeout is here to prevent the menu from flashing //the timeout is here to prevent the menu from flashing
setTimeout(() => picker.showPicker(emojiContainer), 100); setTimeout(() => picker.showPicker(emojiContainer), 100);
@ -64,6 +86,8 @@
height: 100%; height: 100%;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
position: absolute;
z-index: 300;
} }
.emote-menu { .emote-menu {

View file

@ -127,6 +127,8 @@
</form> </form>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
.enableCameraScene { .enableCameraScene {
pointer-events: auto; pointer-events: auto;
margin: 20px auto 0; margin: 20px auto 0;
@ -214,7 +216,7 @@
} }
} }
@media only screen and (max-width: 800px) { @include media-breakpoint-up(md) {
.enableCameraScene h2 { .enableCameraScene h2 {
font-size: 80%; font-size: 80%;
} }

View file

@ -0,0 +1,33 @@
<script lang="typescript">
import followImg from "../images/follow.svg";
export let hidden: Boolean;
let cancelButton = false;
</script>
<div class="btn-follow" class:hide={hidden} class:cancel={cancelButton}>
<img src={followImg} alt="" />
</div>
<style lang="scss">
.btn-follow {
cursor: url("../../../style/images/cursor_pointer.png"), pointer;
display: flex;
align-items: center;
justify-content: center;
border: solid 0px black;
width: 44px;
height: 44px;
background: #666;
box-shadow: 2px 2px 24px #444;
border-radius: 48px;
transform: translateY(15px);
transition-timing-function: ease-in-out;
margin: 0 4%;
img {
filter: brightness(0) invert(1);
}
}
</style>

View file

@ -1,9 +1,5 @@
<!--
vim: ft=typescript
-->
<script lang="ts"> <script lang="ts">
import { gameManager } from "../../Phaser/Game/GameManager"; import { gameManager } from "../../Phaser/Game/GameManager";
import followImg from "../images/follow.svg";
import { followStateStore, followRoleStore, followUsersStore } from "../../Stores/FollowStore"; import { followStateStore, followRoleStore, followUsersStore } from "../../Stores/FollowStore";
import LL from "../../i18n/i18n-svelte"; import LL from "../../i18n/i18n-svelte";
@ -14,10 +10,6 @@ vim: ft=typescript
return user ? user.playerName : ""; return user ? user.playerName : "";
} }
function sendFollowRequest() {
gameScene.CurrentPlayer.sendFollowRequest();
}
function acceptFollowRequest() { function acceptFollowRequest() {
gameScene.CurrentPlayer.startFollowing(); gameScene.CurrentPlayer.startFollowing();
} }
@ -83,7 +75,7 @@ vim: ft=typescript
{#if $followStateStore === "active" || $followStateStore === "ending"} {#if $followStateStore === "active" || $followStateStore === "ending"}
<div class="interact-status nes-container is-rounded"> <div class="interact-status nes-container is-rounded">
<section class="interact-status"> <section>
{#if $followRoleStore === "follower"} {#if $followRoleStore === "follower"}
<p>{$LL.follow.interactStatus.following({ leader: name($followUsersStore[0]) })}</p> <p>{$LL.follow.interactStatus.following({ leader: name($followUsersStore[0]) })}</p>
{:else if $followUsersStore.length === 0} {:else if $followUsersStore.length === 0}
@ -109,48 +101,27 @@ vim: ft=typescript
</div> </div>
{/if} {/if}
{#if $followStateStore === "off"}
<button
type="button"
class="nes-btn is-primary follow-menu-button"
on:click|preventDefault={sendFollowRequest}
title="Ask others to follow"><img class="background-img" src={followImg} alt="" /></button
>
{/if}
{#if $followStateStore === "active" || $followStateStore === "ending"}
{#if $followRoleStore === "follower"}
<button
type="button"
class="nes-btn is-error follow-menu-button"
on:click|preventDefault={reset}
title="Stop following"><img class="background-img" src={followImg} alt="" /></button
>
{:else}
<button
type="button"
class="nes-btn is-error follow-menu-button"
on:click|preventDefault={reset}
title="Stop leading the way"><img class="background-img" src={followImg} alt="" /></button
>
{/if}
{/if}
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
.nes-container { .nes-container {
padding: 5px; padding: 5px;
} }
div.interact-status { .interact-status {
background-color: #333333; background-color: #333333;
color: whitesmoke; color: whitesmoke;
position: relative; position: absolute;
height: 2.7em; max-height: 2.7em;
width: 40vw; width: 40vw;
top: 87vh; top: 87vh;
margin: auto;
text-align: center; text-align: center;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
z-index: 400;
} }
div.interact-menu { div.interact-menu {
@ -158,10 +129,14 @@ vim: ft=typescript
background-color: #333333; background-color: #333333;
color: whitesmoke; color: whitesmoke;
position: relative; position: absolute;
width: 60vw; width: 60vw;
top: 60vh; top: 60vh;
margin: auto; left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
z-index: 150;
section.interact-menu-title { section.interact-menu-title {
margin-bottom: 20px; margin-bottom: 20px;
@ -189,23 +164,16 @@ vim: ft=typescript
} }
} }
.follow-menu-button { @include media-breakpoint-up(md) {
position: absolute; .interact-status {
bottom: 10px; width: 90vw;
left: 10px;
pointer-events: all;
}
@media only screen and (max-width: 800px) {
div.interact-status {
width: 100vw;
top: 78vh; top: 78vh;
font-size: 0.75em; font-size: 0.75em;
} }
div.interact-menu { div.interact-menu {
height: 21vh; max-height: 21vh;
width: 100vw; width: 90vw;
font-size: 0.75em; font-size: 0.75em;
} }
} }

View file

@ -22,7 +22,7 @@
<form <form
class="helpCameraSettings nes-container" class="helpCameraSettings nes-container"
on:submit|preventDefault={close} on:submit|preventDefault={close}
transition:fly={{ y: -900, duration: 500 }} transition:fly={{ y: -50, duration: 500 }}
> >
<section> <section>
<h2>{$LL.camera.help.title()}</h2> <h2>{$LL.camera.help.title()}</h2>
@ -55,9 +55,12 @@
background: #eceeee; background: #eceeee;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
margin-top: 10vh; left: 0;
right: 0;
margin-top: 4%;
max-height: 80vh; max-height: 80vh;
max-width: 80vw; max-width: 80vw;
z-index: 600;
overflow: auto; overflow: auto;
text-align: center; text-align: center;

View file

@ -21,9 +21,11 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 40px; bottom: 40px;
margin: 0 auto; margin-right: auto;
margin-left: auto;
padding: 0; padding: 0;
width: clamp(200px, 20vw, 20vw); width: clamp(200px, 20vw, 20vw);
z-index: 155;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -31,6 +33,10 @@
animation: moveMessage 0.5s; animation: moveMessage 0.5s;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-timing-function: ease-in-out; animation-timing-function: ease-in-out;
div {
margin-bottom: 5%;
}
} }
div.nes-container.is-rounded { div.nes-container.is-rounded {

View file

@ -0,0 +1,176 @@
<script lang="typescript">
import { onMount } from "svelte";
import { audioManagerVisibilityStore } from "../Stores/AudioManagerStore";
import { embedScreenLayout, hasEmbedScreen } from "../Stores/EmbedScreensStore";
import { emoteMenuStore } from "../Stores/EmoteStore";
import { myCameraVisibilityStore } from "../Stores/MyCameraStoreVisibility";
import { requestVisitCardsStore } from "../Stores/GameStore";
import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
import { layoutManagerActionVisibilityStore } from "../Stores/LayoutManagerStore";
import { menuIconVisiblilityStore, menuVisiblilityStore, warningContainerStore } from "../Stores/MenuStore";
import { showReportScreenStore, userReportEmpty } from "../Stores/ShowReportScreenStore";
import AudioManager from "./AudioManager/AudioManager.svelte";
import CameraControls from "./CameraControls.svelte";
import EmbedScreensContainer from "./EmbedScreens/EmbedScreensContainer.svelte";
import EmoteMenu from "./EmoteMenu/EmoteMenu.svelte";
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
import LayoutActionManager from "./LayoutActionManager/LayoutActionManager.svelte";
import Menu from "./Menu/Menu.svelte";
import MenuIcon from "./Menu/MenuIcon.svelte";
import MyCamera from "./MyCamera.svelte";
import ReportMenu from "./ReportMenu/ReportMenu.svelte";
import VisitCard from "./VisitCard/VisitCard.svelte";
import WarningContainer from "./WarningContainer/WarningContainer.svelte";
import { isMediaBreakpointDown, isMediaBreakpointUp } from "../Utils/BreakpointsUtils";
import CoWebsitesContainer from "./EmbedScreens/CoWebsitesContainer.svelte";
import FollowMenu from "./FollowMenu/FollowMenu.svelte";
import { followStateStore } from "../Stores/FollowStore";
import { peerStore } from "../Stores/PeerStore";
import { banMessageStore } from "../Stores/TypeMessageStore/BanMessageStore";
import BanMessageContainer from "./TypeMessage/BanMessageContainer.svelte";
import { textMessageStore } from "../Stores/TypeMessageStore/TextMessageStore";
import TextMessageContainer from "./TypeMessage/TextMessageContainer.svelte";
import { soundPlayingStore } from "../Stores/SoundPlayingStore";
import AudioPlaying from "./UI/AudioPlaying.svelte";
import { showLimitRoomModalStore, showShareLinkMapModalStore } from "../Stores/ModalStore";
import LimitRoomModal from "./Modal/LimitRoomModal.svelte";
import ShareLinkMapModal from "./Modal/ShareLinkMapModal.svelte";
import { LayoutMode } from "../WebRtc/LayoutManager";
import { actionsMenuStore } from '../Stores/ActionsMenuStore';
import ActionsMenu from './ActionsMenu/ActionsMenu.svelte';
let mainLayout: HTMLDivElement;
let displayCoWebsiteContainerMd = isMediaBreakpointUp("md");
let displayCoWebsiteContainerLg = isMediaBreakpointDown("lg");
const resizeObserver = new ResizeObserver(() => {
displayCoWebsiteContainerMd = isMediaBreakpointUp("md");
displayCoWebsiteContainerLg = isMediaBreakpointDown("lg");
});
onMount(() => {
resizeObserver.observe(mainLayout);
});
</script>
<div id="main-layout" bind:this={mainLayout}>
<aside id="main-layout-left-aside">
{#if $menuIconVisiblilityStore}
<MenuIcon />
{/if}
{#if $embedScreenLayout === LayoutMode.VideoChat || displayCoWebsiteContainerMd}
<CoWebsitesContainer vertical={true} />
{/if}
</aside>
<section id="main-layout-main">
{#if $menuVisiblilityStore}
<Menu />
{/if}
{#if $banMessageStore.length > 0}
<BanMessageContainer />
{:else if $textMessageStore.length > 0}
<TextMessageContainer />
{/if}
{#if $soundPlayingStore}
<AudioPlaying url={$soundPlayingStore} />
{/if}
{#if $warningContainerStore}
<WarningContainer />
{/if}
{#if $showReportScreenStore !== userReportEmpty}
<ReportMenu />
{/if}
{#if $helpCameraSettingsVisibleStore}
<HelpCameraSettingsPopup />
{/if}
{#if $audioManagerVisibilityStore}
<AudioManager />
{/if}
{#if $showLimitRoomModalStore}
<LimitRoomModal />
{/if}
{#if $showShareLinkMapModalStore}
<ShareLinkMapModal />
{/if}
{#if $followStateStore !== "off" || $peerStore.size > 0}
<FollowMenu />
{/if}
{#if $actionsMenuStore}
<ActionsMenu />
{/if}
{#if $requestVisitCardsStore}
<VisitCard visitCardUrl={$requestVisitCardsStore} />
{/if}
{#if $emoteMenuStore}
<EmoteMenu />
{/if}
{#if hasEmbedScreen}
<EmbedScreensContainer />
{/if}
</section>
<section id="main-layout-baseline">
{#if displayCoWebsiteContainerLg}
<CoWebsitesContainer />
{/if}
{#if $layoutManagerActionVisibilityStore}
<LayoutActionManager />
{/if}
{#if $myCameraVisibilityStore}
<MyCamera />
<CameraControls />
{/if}
</section>
</div>
<style lang="scss">
@import "../../style/breakpoints.scss";
#main-layout {
display: grid;
grid-template-columns: 120px calc(100% - 120px);
grid-template-rows: 80% 20%;
&-left-aside {
min-width: 80px;
}
&-baseline {
grid-column: 1/3;
}
}
@include media-breakpoint-up(md) {
#main-layout {
grid-template-columns: 15% 85%;
&-left-aside {
min-width: auto;
}
}
}
@include media-breakpoint-up(sm) {
#main-layout {
grid-template-columns: 20% 80%;
}
}
</style>

View file

@ -100,6 +100,8 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
.string-HTML { .string-HTML {
white-space: pre-line; white-space: pre-line;
} }
@ -126,7 +128,7 @@
} }
} }
@media only screen and (max-width: 800px), only screen and (max-height: 800px) { @include media-breakpoint-up(md) {
div.about-room-main { div.about-room-main {
section.container-overflow { section.container-overflow {
height: calc(100% - 120px); height: calc(100% - 120px);

View file

@ -67,6 +67,8 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
div.global-message-main { div.global-message-main {
height: calc(100% - 50px); height: calc(100% - 50px);
display: grid; display: grid;
@ -109,7 +111,7 @@
} }
} }
@media only screen and (max-width: 800px), only screen and (max-height: 800px) { @include media-breakpoint-up(md) {
.global-message-content { .global-message-content {
height: calc(100% - 5px); height: calc(100% - 5px);
} }

View file

@ -36,6 +36,8 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
div.guest-main { div.guest-main {
height: calc(100% - 56px); height: calc(100% - 56px);
@ -57,7 +59,7 @@
} }
} }
@media only screen and (max-width: 900px), only screen and (max-height: 600px) { @include media-breakpoint-up(md) {
div.guest-main { div.guest-main {
section.share-url.not-mobile { section.share-url.not-mobile {
display: none; display: none;

View file

@ -125,6 +125,8 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
.nes-container { .nes-container {
padding: 5px; padding: 5px;
} }
@ -136,11 +138,15 @@
pointer-events: auto; pointer-events: auto;
height: 80%; height: 80%;
width: 75%; width: 75%;
top: 10%; top: 4%;
position: relative; left: 0;
z-index: 80; right: 0;
margin: auto; margin-left: auto;
margin-right: auto;
position: absolute;
z-index: 900;
display: grid; display: grid;
grid-template-columns: var(--size-first-columns-grid) calc(100% - var(--size-first-columns-grid)); grid-template-columns: var(--size-first-columns-grid) calc(100% - var(--size-first-columns-grid));
@ -173,12 +179,12 @@
} }
} }
@media only screen and (max-width: 800px) { @include media-breakpoint-up(md) {
div.menu-container-main { div.menu-container-main {
--size-first-columns-grid: 120px; --size-first-columns-grid: 120px;
height: 70%; height: 70%;
top: 55px; top: 55px;
width: 100%; width: 95%;
font-size: 0.5em; font-size: 0.5em;
div.menu-nav-sidebar { div.menu-nav-sidebar {

View file

@ -14,6 +14,7 @@
function showMenu() { function showMenu() {
menuVisiblilityStore.set(!get(menuVisiblilityStore)); menuVisiblilityStore.set(!get(menuVisiblilityStore));
} }
function showChat() { function showChat() {
chatVisibilityStore.set(true); chatVisibilityStore.set(true);
} }
@ -21,72 +22,97 @@
function register() { function register() {
window.open(`${ADMIN_URL}/second-step-register`, "_self"); window.open(`${ADMIN_URL}/second-step-register`, "_self");
} }
function showInvite() { function showInvite() {
showShareLinkMapModalStore.set(true); showShareLinkMapModalStore.set(true);
} }
function noDrag() {
return false;
}
</script> </script>
<svelte:window /> <svelte:window />
<main class="menuIcon"> <main class="menuIcon noselect">
{#if $limitMapStore} {#if $limitMapStore}
<img <img
src={logoInvite} src={logoInvite}
alt={$LL.menu.icon.open.invite()} alt={$LL.menu.icon.open.invite()}
class="nes-pointer" class="nes-pointer"
draggable="false"
on:dragstart|preventDefault={noDrag}
on:click|preventDefault={showInvite} on:click|preventDefault={showInvite}
/> />
<img <img
src={logoRegister} src={logoRegister}
alt={$LL.menu.icon.open.register()} alt={$LL.menu.icon.open.register()}
class="nes-pointer" class="nes-pointer"
draggable="false"
on:dragstart|preventDefault={noDrag}
on:click|preventDefault={register} on:click|preventDefault={register}
/> />
{:else} {:else}
<img src={logoWA} alt={$LL.menu.icon.open.menu()} class="nes-pointer" on:click|preventDefault={showMenu} /> <img
<img src={logoTalk} alt={$LL.menu.icon.open.chat()} class="nes-pointer" on:click|preventDefault={showChat} /> src={logoWA}
alt={$LL.menu.icon.open.menu()}
class="nes-pointer"
draggable="false"
on:dragstart|preventDefault={noDrag}
on:click|preventDefault={showMenu}
/>
<img
src={logoTalk}
alt={$LL.menu.icon.open.chat()}
class="nes-pointer"
draggable="false"
on:dragstart|preventDefault={noDrag}
on:click|preventDefault={showChat}
/>
{/if} {/if}
</main> </main>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
.menuIcon { .menuIcon {
display: inline-grid; display: flex;
z-index: 90; flex-direction: column;
align-items: center;
margin-top: 20%;
z-index: 800;
position: relative; position: relative;
margin: 25px;
img { img {
pointer-events: auto; pointer-events: auto;
width: 60px; width: 60px;
padding-top: 0; padding-top: 0;
margin: 3px; margin: 5%;
image-rendering: pixelated;
} }
} }
.menuIcon img:hover { .menuIcon img:hover {
transform: scale(1.2); transform: scale(1.2);
} }
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
@include media-breakpoint-up(sm) {
.menuIcon { .menuIcon {
display: inline-grid; margin-top: 10%;
z-index: 90;
position: relative;
margin: 25px;
img { img {
pointer-events: auto; pointer-events: auto;
width: 60px; width: 60px;
padding-top: 0; padding-top: 0;
margin: 3px;
} }
} }
.menuIcon img:hover { .menuIcon img:hover {
transform: scale(1.2); transform: scale(1.2);
} }
@media only screen and (max-width: 800px), only screen and (max-height: 800px) { }
.menuIcon {
margin: 3px; @include media-breakpoint-up(md) {
img { .menuIcon {
width: 50px; img {
} width: 50px;
} }
} }
} }

View file

@ -102,6 +102,8 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
div.customize-main { div.customize-main {
width: 100%; width: 100%;
display: inline-flex; display: inline-flex;
@ -161,7 +163,7 @@
} }
} }
@media only screen and (max-width: 800px) { @include media-breakpoint-up(md) {
div.customize-main.content section button { div.customize-main.content section button {
width: 130px; width: 130px;
} }

View file

@ -2,11 +2,11 @@
import { localUserStore } from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
import { videoConstraintStore } from "../../Stores/MediaStore"; import { videoConstraintStore } from "../../Stores/MediaStore";
import { HtmlUtils } from "../../WebRtc/HtmlUtils"; import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { isMobile } from "../../Enum/EnvironmentVariable";
import { menuVisiblilityStore } from "../../Stores/MenuStore"; import { menuVisiblilityStore } from "../../Stores/MenuStore";
import LL, { locale } from "../../i18n/i18n-svelte"; import LL, { locale } from "../../i18n/i18n-svelte";
import type { Locales } from "../../i18n/i18n-types"; import type { Locales } from "../../i18n/i18n-types";
import { displayableLocales, setCurrentLocale } from "../../i18n/locales"; import { displayableLocales, setCurrentLocale } from "../../i18n/locales";
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
let fullscreen: boolean = localUserStore.getFullscreen(); let fullscreen: boolean = localUserStore.getFullscreen();
let notification: boolean = localUserStore.getNotification() === "granted"; let notification: boolean = localUserStore.getNotification() === "granted";
@ -85,6 +85,8 @@
function closeMenu() { function closeMenu() {
menuVisiblilityStore.set(false); menuVisiblilityStore.set(false);
} }
const isMobile = isMediaBreakpointUp("md");
</script> </script>
<div class="settings-main" on:submit|preventDefault={saveSetting}> <div class="settings-main" on:submit|preventDefault={saveSetting}>
@ -93,25 +95,25 @@
<div class="nes-select is-dark"> <div class="nes-select is-dark">
<select bind:value={valueGame}> <select bind:value={valueGame}>
<option value={120} <option value={120}
>{isMobile() >{isMobile
? $LL.menu.settings.gameQuality.short.high() ? $LL.menu.settings.gameQuality.short.high()
: $LL.menu.settings.gameQuality.long.high()}</option : $LL.menu.settings.gameQuality.long.high()}</option
> >
<option value={60} <option value={60}
>{isMobile() >{isMobile
? $LL.menu.settings.gameQuality.short.medium() ? $LL.menu.settings.gameQuality.short.medium()
: $LL.menu.settings.gameQuality.long.medium()}</option : $LL.menu.settings.gameQuality.long.medium()}</option
> >
<option value={40} <option value={40}
>{isMobile() >{isMobile
? $LL.menu.settings.gameQuality.short.minimum()
: $LL.menu.settings.gameQuality.long.minimum()}</option
>
<option value={20}
>{isMobile()
? $LL.menu.settings.gameQuality.short.small() ? $LL.menu.settings.gameQuality.short.small()
: $LL.menu.settings.gameQuality.long.small()}</option : $LL.menu.settings.gameQuality.long.small()}</option
> >
<option value={20}
>{isMobile
? $LL.menu.settings.gameQuality.short.minimum()
: $LL.menu.settings.gameQuality.long.minimum()}</option
>
</select> </select>
</div> </div>
</section> </section>
@ -120,25 +122,25 @@
<div class="nes-select is-dark"> <div class="nes-select is-dark">
<select bind:value={valueVideo}> <select bind:value={valueVideo}>
<option value={30} <option value={30}
>{isMobile() >{isMobile
? $LL.menu.settings.videoQuality.short.high() ? $LL.menu.settings.videoQuality.short.high()
: $LL.menu.settings.videoQuality.long.high()}</option : $LL.menu.settings.videoQuality.long.high()}</option
> >
<option value={20} <option value={20}
>{isMobile() >{isMobile
? $LL.menu.settings.videoQuality.short.medium() ? $LL.menu.settings.videoQuality.short.medium()
: $LL.menu.settings.videoQuality.long.medium()}</option : $LL.menu.settings.videoQuality.long.medium()}</option
> >
<option value={10} <option value={10}
>{isMobile() >{isMobile
? $LL.menu.settings.videoQuality.short.minimum()
: $LL.menu.settings.videoQuality.long.minimum()}</option
>
<option value={5}
>{isMobile()
? $LL.menu.settings.videoQuality.short.small() ? $LL.menu.settings.videoQuality.short.small()
: $LL.menu.settings.videoQuality.long.small()}</option : $LL.menu.settings.videoQuality.long.small()}</option
> >
<option value={5}
>{isMobile
? $LL.menu.settings.videoQuality.short.minimum()
: $LL.menu.settings.videoQuality.long.minimum()}</option
>
</select> </select>
</div> </div>
</section> </section>
@ -199,6 +201,8 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
div.settings-main { div.settings-main {
height: calc(100% - 40px); height: calc(100% - 40px);
overflow-y: auto; overflow-y: auto;
@ -236,7 +240,7 @@
} }
} }
@media only screen and (max-width: 800px), only screen and (max-height: 800px) { @include media-breakpoint-up(md) {
div.settings-main { div.settings-main {
section { section {
padding: 0; padding: 0;

View file

@ -32,6 +32,7 @@
max-width: 80vw; max-width: 80vw;
overflow: auto; overflow: auto;
text-align: center; text-align: center;
z-index: 500;
h2 { h2 {
font-family: "Press Start 2P"; font-family: "Press Start 2P";

View file

@ -75,6 +75,7 @@
max-width: 80vw; max-width: 80vw;
overflow: auto; overflow: auto;
text-align: center; text-align: center;
z-index: 450;
h2 { h2 {
font-family: "Press Start 2P"; font-family: "Press Start 2P";

View file

@ -2,7 +2,7 @@
import { obtainedMediaConstraintStore } from "../Stores/MediaStore"; import { obtainedMediaConstraintStore } from "../Stores/MediaStore";
import { localStreamStore, isSilentStore } from "../Stores/MediaStore"; import { localStreamStore, isSilentStore } from "../Stores/MediaStore";
import SoundMeterWidget from "./SoundMeterWidget.svelte"; import SoundMeterWidget from "./SoundMeterWidget.svelte";
import { onDestroy } from "svelte"; import { onDestroy, onMount } from "svelte";
import { srcObject } from "./Video/utils"; import { srcObject } from "./Video/utils";
import LL from "../i18n/i18n-svelte"; import LL from "../i18n/i18n-svelte";
@ -23,15 +23,75 @@
isSilent = value; isSilent = value;
}); });
let cameraContainer: HTMLDivElement;
onMount(() => {
cameraContainer.addEventListener("transitionend", () => {
if (cameraContainer.classList.contains("hide")) {
cameraContainer.style.visibility = "hidden";
}
});
cameraContainer.addEventListener("transitionstart", () => {
if (!cameraContainer.classList.contains("hide")) {
cameraContainer.style.visibility = "visible";
}
});
});
onDestroy(unsubscribeIsSilent); onDestroy(unsubscribeIsSilent);
</script> </script>
<div> <div
<div class="video-container div-myCamVideo" class:hide={!$obtainedMediaConstraintStore.video || isSilent}> class="nes-container is-rounded my-cam-video-container"
{#if $localStreamStore.type === "success" && $localStreamStore.stream} class:hide={($localStreamStore.type !== "success" || !$obtainedMediaConstraintStore.video) && !isSilent}
<video class="myCamVideo" use:srcObject={stream} autoplay muted playsinline /> bind:this={cameraContainer}
<SoundMeterWidget {stream} /> >
{/if} {#if isSilent}
</div> <div class="is-silent">{$LL.camera.my.silentZone()}</div>
<div class="is-silent" class:hide={isSilent}>{$LL.camera.my.silentZone()}</div> {:else if $localStreamStore.type === "success" && $localStreamStore.stream}
<video class="my-cam-video" use:srcObject={stream} autoplay muted playsinline />
<SoundMeterWidget {stream} />
{/if}
</div> </div>
<style lang="scss">
@import "../../style/breakpoints.scss";
.my-cam-video-container {
position: absolute;
right: 15px;
bottom: 30px;
max-height: 20%;
transition: transform 1000ms;
padding: 0;
background-color: rgba(#000000, 0.6);
background-clip: content-box;
overflow: hidden;
line-height: 0;
z-index: 250;
&.nes-container.is-rounded {
border-image-outset: 1;
}
}
.my-cam-video-container.hide {
transform: translateX(200%);
}
.my-cam-video {
background-color: #00000099;
max-height: 20vh;
max-width: max(25vw, 150px);
width: 100%;
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
}
.is-silent {
font-size: 2em;
color: white;
padding: 40px 20px;
}
</style>

View file

@ -108,12 +108,16 @@
pointer-events: auto; pointer-events: auto;
background-color: #333333; background-color: #333333;
color: whitesmoke; color: whitesmoke;
z-index: 650;
position: relative; position: absolute;
height: 70vh; height: 70vh;
width: 50vw; width: 50vw;
top: 10vh; top: 4%;
margin: auto;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
section.report-menu-title { section.report-menu-title {
display: grid; display: grid;
@ -137,13 +141,4 @@
display: none; display: none;
} }
} }
@media only screen and (max-width: 800px) {
div.report-menu-main {
top: 21vh;
height: 60vh;
width: 100vw;
font-size: 0.5em;
}
}
</style> </style>

View file

@ -47,6 +47,8 @@
</form> </form>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
form.selectCompanionScene { form.selectCompanionScene {
font-family: "Press Start 2P"; font-family: "Press Start 2P";
pointer-events: auto; pointer-events: auto;
@ -85,7 +87,7 @@
} }
} }
@media only screen and (max-width: 800px) { @include media-breakpoint-up(md) {
form.selectCompanionScene button.selectCharacterButtonLeft { form.selectCompanionScene button.selectCharacterButtonLeft {
left: 5vw; left: 5vw;
} }

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { fly, fade } from "svelte/transition"; import { fly, fade } from "svelte/transition";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { gameManager } from "../../Phaser/Game/GameManager";
import type { Message } from "../../Stores/TypeMessageStore/MessageStore"; import type { Message } from "../../Stores/TypeMessageStore/MessageStore";
import { banMessageStore } from "../../Stores/TypeMessageStore/BanMessageStore"; import { banMessageStore } from "../../Stores/TypeMessageStore/BanMessageStore";
import LL from "../../i18n/i18n-svelte"; import LL from "../../i18n/i18n-svelte";
@ -13,6 +14,8 @@
onMount(() => { onMount(() => {
timeToRead(); timeToRead();
const gameScene = gameManager.getCurrentGameScene();
gameScene.playSound("audio-report-message");
}); });
function timeToRead() { function timeToRead() {
@ -53,18 +56,19 @@
on:click|preventDefault={closeBanMessage}>{nameButton}</button on:click|preventDefault={closeBanMessage}>{nameButton}</button
> >
</div> </div>
<!-- svelte-ignore a11y-media-has-caption -->
<audio id="report-message" autoplay>
<source src="/resources/objects/report-message.mp3" type="audio/mp3" />
</audio>
</div> </div>
<style lang="scss"> <style lang="scss">
div.main-ban-message { div.main-ban-message {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: absolute;
top: 15vh; top: 4%;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
z-index: 850;
height: 70vh; height: 70vh;
width: 60vw; width: 60vw;

View file

@ -11,3 +11,9 @@
</div> </div>
{/each} {/each}
</div> </div>
<style lang="scss">
.main-ban-message-container {
z-index: 800;
}
</style>

View file

@ -42,14 +42,17 @@
div.main-text-message { div.main-text-message {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: absolute;
max-height: 25vh; max-height: 25%;
width: 80vw; width: 60%;
margin-right: auto; margin-right: auto;
margin-left: auto; margin-left: auto;
margin-bottom: 16px; top: 6%;
margin-top: 0; left: 0;
right: 0;
padding-bottom: 0; padding-bottom: 0;
z-index: 240;
pointer-events: auto; pointer-events: auto;
background-color: #333333; background-color: #333333;

View file

@ -15,7 +15,8 @@
</div> </div>
<style lang="scss"> <style lang="scss">
div.main-text-message-container { .main-text-message-container {
padding-top: 16px; padding-top: 16px;
z-index: 800;
} }
</style> </style>

View file

@ -37,6 +37,7 @@
background-color: black; background-color: black;
border-radius: 30px 0 0 30px; border-radius: 30px 0 0 30px;
display: inline-flex; display: inline-flex;
z-index: 750;
img { img {
border-radius: 50%; border-radius: 50%;

View file

@ -25,11 +25,17 @@
<style lang="scss"> <style lang="scss">
div.error-div { div.error-div {
pointer-events: auto; pointer-events: auto;
margin-top: 10vh; margin-top: 4%;
margin-right: auto; margin-right: auto;
margin-left: auto; margin-left: auto;
left: 0;
right: 0;
position: absolute;
width: max-content; width: max-content;
max-width: 80vw; max-width: 80vw;
z-index: 230;
height: auto !important;
background-clip: padding-box;
.button-bar { .button-bar {
text-align: center; text-align: center;

View file

@ -1,15 +1,33 @@
<script lang="typescript"> <script lang="typescript">
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
import type { EmbedScreen } from "../../Stores/EmbedScreensStore";
import type { ScreenSharingLocalMedia } from "../../Stores/ScreenSharingStore"; import type { ScreenSharingLocalMedia } from "../../Stores/ScreenSharingStore";
import { videoFocusStore } from "../../Stores/VideoFocusStore"; import type { Streamable } from "../../Stores/StreamableCollectionStore";
import { srcObject } from "./utils"; import { srcObject } from "./utils";
export let clickable = false;
export let peer: ScreenSharingLocalMedia; export let peer: ScreenSharingLocalMedia;
let stream = peer.stream; let stream = peer.stream;
export let cssClass: string | undefined; export let cssClass: string | undefined;
let embedScreen: EmbedScreen;
if (stream) {
embedScreen = {
type: "streamable",
embed: stream as unknown as Streamable,
};
}
</script> </script>
<div class="video-container {cssClass ? cssClass : ''}" class:hide={!stream}> <div class="video-container {cssClass ? cssClass : ''}" class:hide={!stream}>
{#if stream} {#if stream}
<video use:srcObject={stream} autoplay muted playsinline on:click={() => videoFocusStore.toggleFocus(peer)} /> <video
use:srcObject={stream}
autoplay
muted
playsinline
on:click={() => (clickable ? highlightedEmbedScreen.toggleHighlight(embedScreen) : null)}
/>
{/if} {/if}
</div> </div>

View file

@ -7,14 +7,110 @@
import type { Streamable } from "../../Stores/StreamableCollectionStore"; import type { Streamable } from "../../Stores/StreamableCollectionStore";
export let streamable: Streamable; export let streamable: Streamable;
export let isHightlighted = false;
export let isClickable = false;
export let mozaicFullWidth = false;
export let mozaicQuarter = false;
</script> </script>
<div class="media-container"> <div
{#if streamable instanceof VideoPeer} class="media-container nes-container is-rounded {isHightlighted ? 'hightlighted' : ''}"
<VideoMediaBox peer={streamable} /> class:clickable={isClickable}
{:else if streamable instanceof ScreenSharingPeer} class:mozaic-full-width={mozaicFullWidth}
<ScreenSharingMediaBox peer={streamable} /> class:mozaic-quarter={mozaicQuarter}
{:else} >
<LocalStreamMediaBox peer={streamable} cssClass="" /> <div>
{/if} {#if streamable instanceof VideoPeer}
<VideoMediaBox peer={streamable} clickable={isClickable} />
{:else if streamable instanceof ScreenSharingPeer}
<ScreenSharingMediaBox peer={streamable} clickable={isClickable} />
{:else}
<LocalStreamMediaBox peer={streamable} clickable={isClickable} cssClass="" />
{/if}
</div>
</div> </div>
<style lang="scss">
@import "../../../style/breakpoints.scss";
.media-container {
display: flex;
margin-top: 4%;
margin-bottom: 4%;
margin-left: auto;
margin-right: auto;
transition: margin-left 0.2s, margin-right 0.2s, margin-bottom 0.2s, margin-top 0.2s, max-height 0.2s,
max-width 0.2s;
pointer-events: auto;
padding: 0;
max-height: 85%;
max-width: 85%;
&:hover {
margin-top: 2%;
margin-bottom: 2%;
}
&.hightlighted {
margin-top: 0% !important;
margin-bottom: 0% !important;
margin-left: 0% !important;
max-height: 100% !important;
max-width: 96% !important;
&:hover {
margin-top: 0% !important;
margin-bottom: 0% !important;
}
}
&.mozaic-full-width {
width: 95%;
max-width: 95%;
margin-left: 3%;
margin-right: 3%;
margin-top: auto;
margin-bottom: auto;
&:hover {
margin-top: auto;
margin-bottom: auto;
}
}
&.mozaic-quarter {
width: 95%;
max-width: 95%;
margin-top: auto;
margin-bottom: auto;
&:hover {
margin-top: auto;
margin-bottom: auto;
}
}
&.nes-container.is-rounded {
border-image-outset: 1;
}
&.clickable {
cursor: url("../../../style/images/cursor_pointer.png"), pointer;
}
> div {
background-color: rgba(0, 0, 0, 0.6);
display: flex;
width: 100%;
}
}
@include media-breakpoint-only(md) {
.media-container {
margin-top: 10%;
margin-bottom: 10%;
}
}
</style>

View file

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

View file

@ -1,12 +1,26 @@
<script lang="ts"> <script lang="ts">
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
import type { EmbedScreen } from "../../Stores/EmbedScreensStore";
import type { Streamable } from "../../Stores/StreamableCollectionStore";
import type { ScreenSharingPeer } from "../../WebRtc/ScreenSharingPeer"; import type { ScreenSharingPeer } from "../../WebRtc/ScreenSharingPeer";
import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { getColorByString, srcObject } from "./utils"; import { getColorByString, srcObject } from "./utils";
export let clickable = false;
export let peer: ScreenSharingPeer; export let peer: ScreenSharingPeer;
let streamStore = peer.streamStore; let streamStore = peer.streamStore;
let name = peer.userName; let name = peer.userName;
let statusStore = peer.statusStore; let statusStore = peer.statusStore;
let embedScreen: EmbedScreen;
if (peer) {
embedScreen = {
type: "streamable",
embed: peer as unknown as Streamable,
};
}
</script> </script>
<div class="video-container"> <div class="video-container">
@ -16,11 +30,17 @@
{#if $statusStore === "error"} {#if $statusStore === "error"}
<div class="rtc-error" /> <div class="rtc-error" />
{/if} {/if}
{#if $streamStore === null} {#if $streamStore !== null}
<i style="background-color: {getColorByString(name)};">{name}</i>
{:else}
<!-- svelte-ignore a11y-media-has-caption --> <!-- svelte-ignore a11y-media-has-caption -->
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)} /> <i class="container">
<span style="background-color: {getColorByString(name)};">{name}</span>
</i>
<video
use:srcObject={$streamStore}
autoplay
playsinline
on:click={() => (clickable ? highlightedEmbedScreen.toggleHighlight(embedScreen) : null)}
/>
{/if} {/if}
</div> </div>
@ -29,5 +49,10 @@
video { video {
width: 100%; width: 100%;
} }
i {
span {
padding: 2px 32px;
}
}
} }
</style> </style>

View file

@ -4,11 +4,17 @@
import microphoneCloseImg from "../images/microphone-close.svg"; import microphoneCloseImg from "../images/microphone-close.svg";
import reportImg from "./images/report.svg"; import reportImg from "./images/report.svg";
import blockSignImg from "./images/blockSign.svg"; import blockSignImg from "./images/blockSign.svg";
import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { showReportScreenStore } from "../../Stores/ShowReportScreenStore"; import { showReportScreenStore } from "../../Stores/ShowReportScreenStore";
import { getColorByString, srcObject } from "./utils"; import { getColorByString, srcObject } from "./utils";
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
import type { EmbedScreen } from "../../Stores/EmbedScreensStore";
import type { Streamable } from "../../Stores/StreamableCollectionStore";
import Woka from "../Woka/Woka.svelte"; import Woka from "../Woka/Woka.svelte";
import { onMount } from "svelte";
import { isMediaBreakpointOnly } from "../../Utils/BreakpointsUtils";
export let clickable = false;
export let peer: VideoPeer; export let peer: VideoPeer;
let streamStore = peer.streamStore; let streamStore = peer.streamStore;
@ -19,9 +25,32 @@
function openReport(peer: VideoPeer): void { function openReport(peer: VideoPeer): void {
showReportScreenStore.set({ userId: peer.userId, userName: peer.userName }); showReportScreenStore.set({ userId: peer.userId, userName: peer.userName });
} }
let embedScreen: EmbedScreen;
let videoContainer: HTMLDivElement;
let minimized = isMediaBreakpointOnly("md");
if (peer) {
embedScreen = {
type: "streamable",
embed: peer as unknown as Streamable,
};
}
function noDrag() {
return false;
}
const resizeObserver = new ResizeObserver(() => {
minimized = isMediaBreakpointOnly("md");
});
onMount(() => {
resizeObserver.observe(videoContainer);
});
</script> </script>
<div class="video-container"> <div class="video-container" class:no-clikable={!clickable} bind:this={videoContainer}>
{#if $statusStore === "connecting"} {#if $statusStore === "connecting"}
<div class="connecting-spinner" /> <div class="connecting-spinner" />
{/if} {/if}
@ -29,43 +58,46 @@
<div class="rtc-error" /> <div class="rtc-error" />
{/if} {/if}
<!-- {#if !$constraintStore || $constraintStore.video === false} --> <!-- {#if !$constraintStore || $constraintStore.video === false} -->
<i <i class="container">
class="container {!$constraintStore || $constraintStore.video === false ? '' : 'minimized'}" <span style="background-color: {getColorByString(name)};">{name}</span>
style="background-color: {getColorByString(name)};"
>
<span>{peer.userName}</span>
<div class="woka-icon"><Woka userId={peer.userId} placeholderSrc={""} /></div>
</i> </i>
<div class="woka-icon {($constraintStore && $constraintStore.video !== false) || minimized ? '' : 'no-video'}">
<Woka userId={peer.userId} placeholderSrc={""} />
</div>
<!-- {/if} --> <!-- {/if} -->
{#if $constraintStore && $constraintStore.audio === false} {#if $constraintStore && $constraintStore.audio === false}
<img src={microphoneCloseImg} class="active" alt="Muted" /> <img
src={microphoneCloseImg}
class="active noselect"
draggable="false"
on:dragstart|preventDefault={noDrag}
alt="Muted"
/>
{/if} {/if}
<button class="report" on:click={() => openReport(peer)}> <button class="report" on:click={() => openReport(peer)}>
<img alt="Report this user" src={reportImg} /> <img alt="Report this user" draggable="false" on:dragstart|preventDefault={noDrag} src={reportImg} />
<span>Report/Block</span> <span class="noselect">Report/Block</span>
</button> </button>
<!-- svelte-ignore a11y-media-has-caption --> <!-- svelte-ignore a11y-media-has-caption -->
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)} /> <video
<img src={blockSignImg} class="block-logo" alt="Block" /> class:no-video={!$constraintStore || $constraintStore.video === false}
use:srcObject={$streamStore}
autoplay
playsinline
on:click={() => (clickable ? highlightedEmbedScreen.toggleHighlight(embedScreen) : null)}
/>
<img src={blockSignImg} draggable="false" on:dragstart|preventDefault={noDrag} class="block-logo" alt="Block" />
{#if $constraintStore && $constraintStore.audio !== false} {#if $constraintStore && $constraintStore.audio !== false}
<SoundMeterWidget stream={$streamStore} /> <SoundMeterWidget stream={$streamStore} />
{/if} {/if}
</div> </div>
<style> <style lang="scss">
.container { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-top: 15px;
} }
video.no-video {
.minimized { visibility: collapse;
left: auto;
transform: scale(0.5);
opacity: 0.5;
}
.woka-icon {
margin-right: 3px;
} }
</style> </style>

View file

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

View file

@ -57,6 +57,7 @@
height: 120px; height: 120px;
margin: auto; margin: auto;
animation: spin 2s linear infinite; animation: spin 2s linear infinite;
z-index: 350;
} }
@keyframes spin { @keyframes spin {

View file

@ -27,18 +27,28 @@
<style lang="scss"> <style lang="scss">
main.warningMain { main.warningMain {
pointer-events: auto; pointer-events: auto;
width: 100vw; width: 80%;
background-color: red; background-color: #f9e81e;
color: #14304c;
text-align: center; text-align: center;
position: absolute; position: absolute;
left: 50%;
top: 4%;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
transform: translate(-50%, 0); transform: translate(-50%, 0);
font-family: Lato; font-family: Lato;
min-width: 300px; min-width: 300px;
opacity: 0.9; opacity: 0.9;
z-index: 2; z-index: 700;
h2 { h2 {
padding: 5px; padding: 5px;
} }
a {
color: #ff475a;
}
} }
</style> </style>

View file

@ -22,10 +22,21 @@
src = source ?? placeholderSrc; src = source ?? placeholderSrc;
}); });
function noDrag() {
return false;
}
onDestroy(unsubscribe); onDestroy(unsubscribe);
</script> </script>
<img {src} alt="" class="nes-pointer" style="--theme-width: {width}; --theme-height: {height}" /> <img
{src}
alt=""
class="nes-pointer noselect"
style="--theme-width: {width}; --theme-height: {height}"
draggable="false"
on:dragstart|preventDefault={noDrag}
/>
<style> <style>
img { img {

View file

@ -49,6 +49,8 @@
</form> </form>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
form.selectCharacterScene { form.selectCharacterScene {
font-family: "Press Start 2P"; font-family: "Press Start 2P";
pointer-events: auto; pointer-events: auto;
@ -91,7 +93,7 @@
} }
} }
@media only screen and (max-width: 800px) { @include media-breakpoint-up(md) {
form.selectCharacterScene button.selectCharacterButtonLeft { form.selectCharacterScene button.selectCharacterButtonLeft {
left: 5vw; left: 5vw;
} }

View file

@ -183,14 +183,14 @@ class ConnectionManager {
window.location.hash; window.location.hash;
} }
//Set last room visited! (connected or nor, must to be saved in localstorage and cache API)
//use href to keep # value
await localUserStore.setLastRoomUrl(new URL(roomPath).href);
//get detail map for anonymous login and set texture in local storage //get detail map for anonymous login and set texture in local storage
//before set token of user we must load room and all information. For example the mandatory authentication could be require on current room //before set token of user we must load room and all information. For example the mandatory authentication could be require on current room
this._currentRoom = await Room.createRoom(new URL(roomPath)); this._currentRoom = await Room.createRoom(new URL(roomPath));
//Set last room visited! (connected or nor, must to be saved in localstorage and cache API)
//use href to keep # value
await localUserStore.setLastRoomUrl(this._currentRoom.href);
//todo: add here some kind of warning if authToken has expired. //todo: add here some kind of warning if authToken has expired.
if (!this.authToken && !this._currentRoom.authenticationMandatory) { if (!this.authToken && !this._currentRoom.authenticationMandatory) {
await this.anonymousLogin(); await this.anonymousLogin();

View file

@ -1,33 +1,46 @@
const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true"; declare global {
const START_ROOM_URL: string = interface Window {
process.env.START_ROOM_URL || "/_/global/maps.workadventure.localhost/Floor1/floor1.json"; env?: Record<string, string>;
const PUSHER_URL = process.env.PUSHER_URL || "//pusher.workadventure.localhost"; }
export const ADMIN_URL = process.env.ADMIN_URL || "//workadventu.re"; }
const UPLOADER_URL = process.env.UPLOADER_URL || "//uploader.workadventure.localhost";
const ICON_URL = process.env.ICON_URL || "//icon.workadventure.localhost"; const getEnv = (key: string): string | undefined => {
const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302"; if (global.window?.env) {
const TURN_SERVER: string = process.env.TURN_SERVER || ""; return global.window.env[key];
const SKIP_RENDER_OPTIMIZATIONS: boolean = process.env.SKIP_RENDER_OPTIMIZATIONS == "true"; }
const DISABLE_NOTIFICATIONS: boolean = process.env.DISABLE_NOTIFICATIONS == "true"; if (global.process?.env) {
const TURN_USER: string = process.env.TURN_USER || ""; return global.process.env[key];
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || ""; }
const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL; return;
const JITSI_PRIVATE_MODE: boolean = process.env.JITSI_PRIVATE_MODE == "true"; };
const DEBUG_MODE: boolean = getEnv("DEBUG_MODE") == "true";
const START_ROOM_URL: string = getEnv("START_ROOM_URL") || "/_/global/maps.workadventure.localhost/Floor1/floor1.json";
const PUSHER_URL = getEnv("PUSHER_URL") || "//pusher.workadventure.localhost";
export const ADMIN_URL = getEnv("ADMIN_URL") || "//workadventu.re";
const UPLOADER_URL = getEnv("UPLOADER_URL") || "//uploader.workadventure.localhost";
const ICON_URL = getEnv("ICON_URL") || "//icon.workadventure.localhost";
const STUN_SERVER: string = getEnv("STUN_SERVER") || "stun:stun.l.google.com:19302";
const TURN_SERVER: string = getEnv("TURN_SERVER") || "";
const SKIP_RENDER_OPTIMIZATIONS: boolean = getEnv("SKIP_RENDER_OPTIMIZATIONS") == "true";
const DISABLE_NOTIFICATIONS: boolean = getEnv("DISABLE_NOTIFICATIONS") == "true";
const TURN_USER: string = getEnv("TURN_USER") || "";
const TURN_PASSWORD: string = getEnv("TURN_PASSWORD") || "";
const JITSI_URL: string | undefined = getEnv("JITSI_URL") === "" ? undefined : getEnv("JITSI_URL");
const JITSI_PRIVATE_MODE: boolean = getEnv("JITSI_PRIVATE_MODE") == "true";
const POSITION_DELAY = 200; // Wait 200ms between sending position events const POSITION_DELAY = 200; // Wait 200ms between sending position events
const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player
export const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || "") || 8; export const MAX_USERNAME_LENGTH = parseInt(getEnv("MAX_USERNAME_LENGTH") || "") || 8;
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4"); export const MAX_PER_GROUP = parseInt(getEnv("MAX_PER_GROUP") || "4");
export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == "true"; export const DISPLAY_TERMS_OF_USE = getEnv("DISPLAY_TERMS_OF_USE") == "true";
export const NODE_ENV = process.env.NODE_ENV || "development"; export const NODE_ENV = getEnv("NODE_ENV") || "development";
export const CONTACT_URL = process.env.CONTACT_URL || undefined; export const CONTACT_URL = getEnv("CONTACT_URL") || undefined;
export const PROFILE_URL = process.env.PROFILE_URL || undefined; export const PROFILE_URL = getEnv("PROFILE_URL") || undefined;
export const POSTHOG_API_KEY: string = (process.env.POSTHOG_API_KEY as string) || ""; export const POSTHOG_API_KEY: string = (getEnv("POSTHOG_API_KEY") as string) || "";
export const POSTHOG_URL = process.env.POSTHOG_URL || undefined; export const POSTHOG_URL = getEnv("POSTHOG_URL") || undefined;
export const DISABLE_ANONYMOUS: boolean = process.env.DISABLE_ANONYMOUS === "true"; export const DISABLE_ANONYMOUS: boolean = getEnv("DISABLE_ANONYMOUS") === "true";
export const OPID_LOGIN_SCREEN_PROVIDER = process.env.OPID_LOGIN_SCREEN_PROVIDER; export const OPID_LOGIN_SCREEN_PROVIDER = getEnv("OPID_LOGIN_SCREEN_PROVIDER");
const FALLBACK_LOCALE = process.env.FALLBACK_LOCALE || undefined; const FALLBACK_LOCALE = getEnv("FALLBACK_LOCALE") || undefined;
export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600;
export { export {
DEBUG_MODE, DEBUG_MODE,

View file

@ -63,8 +63,6 @@ export class SoundMeter {
//this.slow = 0.95 * that.slow + 0.05 * that.instant; //this.slow = 0.95 * that.slow + 0.05 * that.instant;
//this.clip = clipcount / input.length; //this.clip = clipcount / input.length;
//console.log('instant', this.instant, 'clip', this.clip);
return this.instant; return this.instant;
} }

View file

@ -1,4 +1,10 @@
import type { ITiledMap, ITiledMapLayer, ITiledMapObject, ITiledMapProperty } from "../Map/ITiledMap"; import type {
ITiledMap,
ITiledMapLayer,
ITiledMapObject,
ITiledMapProperty,
ITiledMapTileLayer,
} from "../Map/ITiledMap";
import { flattenGroupLayersMap } from "../Map/LayersFlattener"; import { flattenGroupLayersMap } from "../Map/LayersFlattener";
import TilemapLayer = Phaser.Tilemaps.TilemapLayer; import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes"; import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
@ -291,6 +297,31 @@ export class GameMap {
} }
} }
public getRandomPositionFromLayer(layerName: string): { x: number; y: number } {
const layer = this.findLayer(layerName) as ITiledMapTileLayer;
if (!layer) {
throw new Error(`No layer "${layerName}" was found`);
}
const tiles = layer.data;
if (!tiles) {
throw new Error(`No tiles in "${layerName}" were found`);
}
if (typeof tiles === "string") {
throw new Error("The content of a JSON map must be filled as a JSON array, not as a string");
}
const possiblePositions: { x: number; y: number }[] = [];
tiles.forEach((objectKey: number, key: number) => {
if (objectKey === 0) {
return;
}
possiblePositions.push({ x: key % layer.width, y: Math.floor(key / layer.width) });
});
if (possiblePositions.length > 0) {
return MathUtils.randomFromArray(possiblePositions);
}
throw new Error("No possible position found");
}
private getLayersByKey(key: number): Array<ITiledMapLayer> { private getLayersByKey(key: number): Array<ITiledMapLayer> {
return this.flatLayers.filter((flatLayer) => flatLayer.type === "tilelayer" && flatLayer.data[key] !== 0); return this.flatLayers.filter((flatLayer) => flatLayer.type === "tilelayer" && flatLayer.data[key] !== 0);
} }

View file

@ -20,7 +20,6 @@ export enum GameMapProperties {
OPEN_WEBSITE = "openWebsite", OPEN_WEBSITE = "openWebsite",
OPEN_WEBSITE_ALLOW_API = "openWebsiteAllowApi", OPEN_WEBSITE_ALLOW_API = "openWebsiteAllowApi",
OPEN_WEBSITE_POLICY = "openWebsitePolicy", OPEN_WEBSITE_POLICY = "openWebsitePolicy",
OPEN_WEBSITE_WIDTH = "openWebsiteWidth",
OPEN_WEBSITE_POSITION = "openWebsitePosition", OPEN_WEBSITE_POSITION = "openWebsitePosition",
OPEN_WEBSITE_TRIGGER = "openWebsiteTrigger", OPEN_WEBSITE_TRIGGER = "openWebsiteTrigger",
OPEN_WEBSITE_TRIGGER_MESSAGE = "openWebsiteTriggerMessage", OPEN_WEBSITE_TRIGGER_MESSAGE = "openWebsiteTriggerMessage",

View file

@ -9,21 +9,21 @@ import { get } from "svelte/store";
import { ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager"; import { ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager";
import type { ITiledMapLayer } from "../Map/ITiledMap"; import type { ITiledMapLayer } from "../Map/ITiledMap";
import { GameMapProperties } from "./GameMapProperties"; import { GameMapProperties } from "./GameMapProperties";
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
enum OpenCoWebsiteState { enum OpenCoWebsiteState {
LOADING, ASLEEP,
OPENED, OPENED,
MUST_BE_CLOSE, MUST_BE_CLOSE,
} }
interface OpenCoWebsite { interface OpenCoWebsite {
coWebsite: CoWebsite | undefined; coWebsite: CoWebsite;
state: OpenCoWebsiteState; state: OpenCoWebsiteState;
} }
export class GameMapPropertiesListener { export class GameMapPropertiesListener {
private coWebsitesOpenByLayer = new Map<ITiledMapLayer, OpenCoWebsite>(); private coWebsitesOpenByLayer = new Map<ITiledMapLayer, OpenCoWebsite>();
private coWebsitesActionTriggerByLayer = new Map<ITiledMapLayer, string>();
constructor(private scene: GameScene, private gameMap: GameMap) {} constructor(private scene: GameScene, private gameMap: GameMap) {}
@ -64,10 +64,8 @@ export class GameMapPropertiesListener {
let openWebsiteProperty: string | undefined; let openWebsiteProperty: string | undefined;
let allowApiProperty: boolean | undefined; let allowApiProperty: boolean | undefined;
let websitePolicyProperty: string | undefined; let websitePolicyProperty: string | undefined;
let websiteWidthProperty: number | undefined;
let websitePositionProperty: number | undefined; let websitePositionProperty: number | undefined;
let websiteTriggerProperty: string | undefined; let websiteTriggerProperty: string | undefined;
let websiteTriggerMessageProperty: string | undefined;
layer.properties.forEach((property) => { layer.properties.forEach((property) => {
switch (property.name) { switch (property.name) {
@ -80,18 +78,12 @@ export class GameMapPropertiesListener {
case GameMapProperties.OPEN_WEBSITE_POLICY: case GameMapProperties.OPEN_WEBSITE_POLICY:
websitePolicyProperty = property.value as string | undefined; websitePolicyProperty = property.value as string | undefined;
break; break;
case GameMapProperties.OPEN_WEBSITE_WIDTH:
websiteWidthProperty = property.value as number | undefined;
break;
case GameMapProperties.OPEN_WEBSITE_POSITION: case GameMapProperties.OPEN_WEBSITE_POSITION:
websitePositionProperty = property.value as number | undefined; websitePositionProperty = property.value as number | undefined;
break; break;
case GameMapProperties.OPEN_WEBSITE_TRIGGER: case GameMapProperties.OPEN_WEBSITE_TRIGGER:
websiteTriggerProperty = property.value as string | undefined; websiteTriggerProperty = property.value as string | undefined;
break; break;
case GameMapProperties.OPEN_WEBSITE_TRIGGER_MESSAGE:
websiteTriggerMessageProperty = property.value as string | undefined;
break;
} }
}); });
@ -105,27 +97,30 @@ export class GameMapPropertiesListener {
return; return;
} }
const coWebsite = coWebsiteManager.addCoWebsite(
openWebsiteProperty,
this.scene.MapUrlFile,
allowApiProperty,
websitePolicyProperty,
websitePositionProperty,
false
);
this.coWebsitesOpenByLayer.set(layer, { this.coWebsitesOpenByLayer.set(layer, {
coWebsite: undefined, coWebsite: coWebsite,
state: OpenCoWebsiteState.LOADING, state: OpenCoWebsiteState.ASLEEP,
}); });
const openWebsiteFunction = () => { const openWebsiteFunction = () => {
coWebsiteManager coWebsiteManager
.loadCoWebsite( .loadCoWebsite(coWebsite)
openWebsiteProperty as string,
this.scene.MapUrlFile,
allowApiProperty,
websitePolicyProperty,
websiteWidthProperty,
websitePositionProperty
)
.then((coWebsite) => { .then((coWebsite) => {
const coWebsiteOpen = this.coWebsitesOpenByLayer.get(layer); const coWebsiteOpen = this.coWebsitesOpenByLayer.get(layer);
if (coWebsiteOpen && coWebsiteOpen.state === OpenCoWebsiteState.MUST_BE_CLOSE) { if (coWebsiteOpen && coWebsiteOpen.state === OpenCoWebsiteState.MUST_BE_CLOSE) {
coWebsiteManager.closeCoWebsite(coWebsite).catch((e) => console.error(e)); coWebsiteManager.closeCoWebsite(coWebsite).catch(() => {
console.error("Error during a co-website closing");
});
this.coWebsitesOpenByLayer.delete(layer); this.coWebsitesOpenByLayer.delete(layer);
this.coWebsitesActionTriggerByLayer.delete(layer);
} else { } else {
this.coWebsitesOpenByLayer.set(layer, { this.coWebsitesOpenByLayer.set(layer, {
coWebsite, coWebsite,
@ -133,27 +128,17 @@ export class GameMapPropertiesListener {
}); });
} }
}) })
.catch((e) => console.error(e)); .catch(() => {
console.error("Error during loading a co-website: " + coWebsite.url);
});
layoutManagerActionStore.removeAction(actionUuid); layoutManagerActionStore.removeAction(actionUuid);
}; };
const forceTrigger = localUserStore.getForceCowebsiteTrigger(); if (
if (forceTrigger || websiteTriggerProperty === ON_ACTION_TRIGGER_BUTTON) { !localUserStore.getForceCowebsiteTrigger() &&
if (!websiteTriggerMessageProperty) { websiteTriggerProperty !== ON_ACTION_TRIGGER_BUTTON
websiteTriggerMessageProperty = "Press SPACE or touch here to open web site"; ) {
}
this.coWebsitesActionTriggerByLayer.set(layer, actionUuid);
layoutManagerActionStore.addAction({
uuid: actionUuid,
type: "message",
message: websiteTriggerMessageProperty,
callback: () => openWebsiteFunction(),
userInputManager: this.scene.userInputManager,
});
} else {
openWebsiteFunction(); openWebsiteFunction();
} }
}); });
@ -194,7 +179,7 @@ export class GameMapPropertiesListener {
return; return;
} }
if (coWebsiteOpen.state === OpenCoWebsiteState.LOADING) { if (coWebsiteOpen.state === OpenCoWebsiteState.ASLEEP) {
coWebsiteOpen.state = OpenCoWebsiteState.MUST_BE_CLOSE; coWebsiteOpen.state = OpenCoWebsiteState.MUST_BE_CLOSE;
} }
@ -203,26 +188,6 @@ export class GameMapPropertiesListener {
} }
this.coWebsitesOpenByLayer.delete(layer); this.coWebsitesOpenByLayer.delete(layer);
if (!websiteTriggerProperty) {
return;
}
const actionStore = get(layoutManagerActionStore);
const actionTriggerUuid = this.coWebsitesActionTriggerByLayer.get(layer);
if (!actionTriggerUuid) {
return;
}
const action =
actionStore && actionStore.length > 0
? actionStore.find((action) => action.uuid === actionTriggerUuid)
: undefined;
if (action) {
layoutManagerActionStore.removeAction(actionTriggerUuid);
}
}); });
}; };

View file

@ -250,7 +250,7 @@ export class GameScene extends DirtyScene {
} }
this.load.audio("audio-webrtc-in", "/resources/objects/webrtc-in.mp3"); this.load.audio("audio-webrtc-in", "/resources/objects/webrtc-in.mp3");
this.load.audio("audio-webrtc-out", "/resources/objects/webrtc-out.mp3"); this.load.audio("audio-webrtc-out", "/resources/objects/webrtc-out.mp3");
//this.load.audio('audio-report-message', '/resources/objects/report-message.mp3'); this.load.audio("audio-report-message", "/resources/objects/report-message.mp3");
this.sound.pauseOnBlur = false; this.sound.pauseOnBlur = false;
this.load.on(FILE_LOAD_ERROR, (file: { src: string }) => { this.load.on(FILE_LOAD_ERROR, (file: { src: string }) => {
@ -558,6 +558,12 @@ export class GameScene extends DirtyScene {
.catch((e) => console.error(e)); .catch((e) => console.error(e));
} }
this.pathfindingManager = new PathfindingManager(
this,
this.gameMap.getCollisionsGrid(),
this.gameMap.getTileDimensions()
);
//notify game manager can to create currentUser in map //notify game manager can to create currentUser in map
this.createCurrentPlayer(); this.createCurrentPlayer();
this.removeAllRemotePlayers(); //cleanup the list of remote players in case the scene was rebooted this.removeAllRemotePlayers(); //cleanup the list of remote players in case the scene was rebooted
@ -632,13 +638,9 @@ export class GameScene extends DirtyScene {
this.peerStoreUnsubscribe = peerStore.subscribe((peers) => { this.peerStoreUnsubscribe = peerStore.subscribe((peers) => {
const newPeerNumber = peers.size; const newPeerNumber = peers.size;
if (newPeerNumber > oldPeerNumber) { if (newPeerNumber > oldPeerNumber) {
this.sound.play("audio-webrtc-in", { this.playSound("audio-webrtc-in");
volume: 0.2,
});
} else if (newPeerNumber < oldPeerNumber) { } else if (newPeerNumber < oldPeerNumber) {
this.sound.play("audio-webrtc-out", { this.playSound("audio-webrtc-out");
volume: 0.2,
});
} }
oldPeerNumber = newPeerNumber; oldPeerNumber = newPeerNumber;
}); });
@ -1259,21 +1261,21 @@ ${escapedMessage}
throw new Error("Unknown query source"); throw new Error("Unknown query source");
} }
const coWebsite = await coWebsiteManager.loadCoWebsite( const coWebsite = coWebsiteManager.addCoWebsite(
openCoWebsite.url, openCoWebsite.url,
iframeListener.getBaseUrlFromSource(source), iframeListener.getBaseUrlFromSource(source),
openCoWebsite.allowApi, openCoWebsite.allowApi,
openCoWebsite.allowPolicy, openCoWebsite.allowPolicy,
openCoWebsite.position openCoWebsite.position,
openCoWebsite.closable ?? true
); );
if (!coWebsite) { if (openCoWebsite.lazy !== undefined && !openCoWebsite.lazy) {
throw new Error("Error on opening co-website"); await coWebsiteManager.loadCoWebsite(coWebsite);
} }
return { return {
id: coWebsite.iframe.id, id: coWebsite.iframe.id,
position: coWebsite.position,
}; };
}); });
@ -1283,7 +1285,6 @@ ${escapedMessage}
return coWebsites.map((coWebsite: CoWebsite) => { return coWebsites.map((coWebsite: CoWebsite) => {
return { return {
id: coWebsite.iframe.id, id: coWebsite.iframe.id,
position: coWebsite.position,
}; };
}); });
}); });
@ -1559,6 +1560,12 @@ ${escapedMessage}
} }
} }
public playSound(sound: string) {
this.sound.play(sound, {
volume: 0.2,
});
}
public cleanupClosingScene(): void { public cleanupClosingScene(): void {
// stop playing audio, close any open website, stop any open Jitsi // stop playing audio, close any open website, stop any open Jitsi
coWebsiteManager.closeCoWebsites().catch((e) => console.error(e)); coWebsiteManager.closeCoWebsites().catch((e) => console.error(e));
@ -1596,7 +1603,10 @@ ${escapedMessage}
this.sharedVariablesManager?.close(); this.sharedVariablesManager?.close();
this.embeddedWebsiteManager?.close(); this.embeddedWebsiteManager?.close();
mediaManager.hideGameOverlay(); //When we leave game, the camera is stop to be reopen after.
// I think that we could keep camera status and the scene can manage camera setup
//TODO find wy chrome don't manage correctly a multiple ask mediaDevices
//mediaManager.hideMyCamera();
for (const iframeEvents of this.iframeSubscriptionList) { for (const iframeEvents of this.iframeSubscriptionList) {
iframeEvents.unsubscribe(); iframeEvents.unsubscribe();
@ -1729,6 +1739,22 @@ ${escapedMessage}
this.connection?.emitEmoteEvent(emoteKey); this.connection?.emitEmoteEvent(emoteKey);
analyticsClient.launchEmote(emoteKey); analyticsClient.launchEmote(emoteKey);
}); });
const moveToParam = urlManager.getHashParameter("moveTo");
if (moveToParam) {
try {
const endPos = this.gameMap.getRandomPositionFromLayer(moveToParam);
this.pathfindingManager
.findPath(this.gameMap.getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y), endPos)
.then((path) => {
if (path && path.length > 0) {
this.CurrentPlayer.setPathToFollow(path).catch((reason) => console.warn(reason));
}
})
.catch((reason) => console.warn(reason));
} catch (err) {
console.warn(`Cannot proceed with moveTo command:\n\t-> ${err}`);
}
}
} catch (err) { } catch (err) {
if (err instanceof TextureError) { if (err instanceof TextureError) {
gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene()); gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene());
@ -2070,7 +2096,18 @@ ${escapedMessage}
biggestAvailableAreaStore.recompute(); biggestAvailableAreaStore.recompute();
} }
private startJitsi(roomName: string, jwt?: string): void { public enableMediaBehaviors() {
const silent = this.gameMap.getCurrentProperties().get(GameMapProperties.SILENT);
this.connection?.setSilent(!!silent);
mediaManager.showMyCamera();
}
public disableMediaBehaviors() {
this.connection?.setSilent(true);
mediaManager.hideMyCamera();
}
public startJitsi(roomName: string, jwt?: string): void {
const allProps = this.gameMap.getCurrentProperties(); const allProps = this.gameMap.getCurrentProperties();
const jitsiConfig = this.safeParseJSONstring( const jitsiConfig = this.safeParseJSONstring(
allProps.get(GameMapProperties.JITSI_CONFIG) as string | undefined, allProps.get(GameMapProperties.JITSI_CONFIG) as string | undefined,
@ -2081,28 +2118,21 @@ ${escapedMessage}
GameMapProperties.JITSI_INTERFACE_CONFIG GameMapProperties.JITSI_INTERFACE_CONFIG
); );
const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined; const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined;
const jitsiWidth = allProps.get(GameMapProperties.JITSI_WIDTH) as number | undefined;
jitsiFactory jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl).catch(() => {
.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl, jitsiWidth) console.error("Cannot start a Jitsi co-website");
.catch((e) => console.error(e));
this.connection?.setSilent(true);
mediaManager.hideGameOverlay();
analyticsClient.enteredJitsi(roomName, this.room.id);
//permit to stop jitsi when user close iframe
mediaManager.addTriggerCloseJitsiFrameButton("close-jitsi", () => {
this.stopJitsi();
}); });
this.disableMediaBehaviors();
analyticsClient.enteredJitsi(roomName, this.room.id);
} }
private stopJitsi(): void { public stopJitsi(): void {
const silent = this.gameMap.getCurrentProperties().get(GameMapProperties.SILENT); const coWebsite = coWebsiteManager.searchJitsi();
this.connection?.setSilent(!!silent); if (coWebsite) {
jitsiFactory.stop(); coWebsiteManager.closeCoWebsite(coWebsite).catch((e) => {
mediaManager.showGameOverlay(); console.error("Error during Jitsi co-website closing", e);
});
mediaManager.removeTriggerCloseJitsiFrameButton("close-jitsi"); }
} }
//todo: put this into an 'orchestrator' scene (EntryScene?) //todo: put this into an 'orchestrator' scene (EntryScene?)

View file

@ -41,7 +41,7 @@ export class PlayerMovement {
oldX: this.startPosition.x, oldX: this.startPosition.x,
oldY: this.startPosition.y, oldY: this.startPosition.y,
direction: this.endPosition.direction, direction: this.endPosition.direction,
moving: this.endPosition.moving, moving: this.isOutdated(tick) ? false : this.endPosition.moving,
}; };
} }
} }

View file

@ -11,10 +11,10 @@ import { areCharacterLayersValid } from "../../Connexion/LocalUser";
import { SelectCharacterSceneName } from "./SelectCharacterScene"; import { SelectCharacterSceneName } from "./SelectCharacterScene";
import { activeRowStore, customCharacterSceneVisibleStore } from "../../Stores/CustomCharacterStore"; import { activeRowStore, customCharacterSceneVisibleStore } from "../../Stores/CustomCharacterStore";
import { waScaleManager } from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import { isMobile } from "../../Enum/EnvironmentVariable";
import { CustomizedCharacter } from "../Entity/CustomizedCharacter"; import { CustomizedCharacter } from "../Entity/CustomizedCharacter";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { analyticsClient } from "../../Administration/AnalyticsClient"; import { analyticsClient } from "../../Administration/AnalyticsClient";
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
export const CustomizeSceneName = "CustomizeScene"; export const CustomizeSceneName = "CustomizeScene";
@ -67,12 +67,12 @@ export class CustomizeScene extends AbstractCharacterScene {
customCharacterSceneVisibleStore.set(true); customCharacterSceneVisibleStore.set(true);
this.events.addListener("wake", () => { this.events.addListener("wake", () => {
waScaleManager.saveZoom(); waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 3 : 1; waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 3 : 1;
customCharacterSceneVisibleStore.set(true); customCharacterSceneVisibleStore.set(true);
}); });
waScaleManager.saveZoom(); waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 3 : 1; waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 3 : 1;
this.Rectangle = this.add.rectangle( this.Rectangle = this.add.rectangle(
this.cameras.main.worldView.x + this.cameras.main.width / 2, this.cameras.main.worldView.x + this.cameras.main.width / 2,

View file

@ -8,8 +8,6 @@ import LL from "../../i18n/i18n-svelte";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { localeDetector } from "../../i18n/locales"; import { localeDetector } from "../../i18n/locales";
const $LL = get(LL);
export const EntrySceneName = "EntryScene"; export const EntrySceneName = "EntryScene";
/** /**
@ -43,6 +41,7 @@ export class EntryScene extends Scene {
this.scene.start(nextSceneName); this.scene.start(nextSceneName);
}) })
.catch((err) => { .catch((err) => {
const $LL = get(LL);
if (err.response && err.response.status == 404) { if (err.response && err.response.status == 404) {
ErrorScene.showError( ErrorScene.showError(
new WAError( new WAError(

View file

@ -12,8 +12,8 @@ import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { PinchManager } from "../UserInput/PinchManager"; import { PinchManager } from "../UserInput/PinchManager";
import { selectCharacterSceneVisibleStore } from "../../Stores/SelectCharacterStore"; import { selectCharacterSceneVisibleStore } from "../../Stores/SelectCharacterStore";
import { waScaleManager } from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import { isMobile } from "../../Enum/EnvironmentVariable";
import { analyticsClient } from "../../Administration/AnalyticsClient"; import { analyticsClient } from "../../Administration/AnalyticsClient";
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
//todo: put this constants in a dedicated file //todo: put this constants in a dedicated file
export const SelectCharacterSceneName = "SelectCharacterScene"; export const SelectCharacterSceneName = "SelectCharacterScene";
@ -60,7 +60,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
selectCharacterSceneVisibleStore.set(true); selectCharacterSceneVisibleStore.set(true);
this.events.addListener("wake", () => { this.events.addListener("wake", () => {
waScaleManager.saveZoom(); waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 2 : 1; waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 2 : 1;
selectCharacterSceneVisibleStore.set(true); selectCharacterSceneVisibleStore.set(true);
}); });
@ -69,7 +69,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
} }
waScaleManager.saveZoom(); waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 2 : 1; waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 2 : 1;
const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16; const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16;
this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xffffff); this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xffffff);

View file

@ -9,7 +9,7 @@ import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { PinchManager } from "../UserInput/PinchManager"; import { PinchManager } from "../UserInput/PinchManager";
import { selectCompanionSceneVisibleStore } from "../../Stores/SelectCompanionStore"; import { selectCompanionSceneVisibleStore } from "../../Stores/SelectCompanionStore";
import { waScaleManager } from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import { isMobile } from "../../Enum/EnvironmentVariable"; import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
export const SelectCompanionSceneName = "SelectCompanionScene"; export const SelectCompanionSceneName = "SelectCompanionScene";
@ -44,7 +44,7 @@ export class SelectCompanionScene extends ResizableScene {
selectCompanionSceneVisibleStore.set(true); selectCompanionSceneVisibleStore.set(true);
waScaleManager.saveZoom(); waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 2 : 1; waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 2 : 1;
if (touchScreenManager.supportTouchScreen) { if (touchScreenManager.supportTouchScreen) {
new PinchManager(this); new PinchManager(this);

View file

@ -37,19 +37,17 @@ export class WaScaleManager {
height: height * devicePixelRatio, height: height * devicePixelRatio,
}); });
if (gameSize.width == 0) { if (realSize.width !== 0 && gameSize.width !== 0 && devicePixelRatio !== 0) {
return; this.actualZoom = realSize.width / gameSize.width / devicePixelRatio;
} }
this.actualZoom = realSize.width / gameSize.width / devicePixelRatio; this.scaleManager.setZoom(this.actualZoom);
this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio);
this.scaleManager.resize(gameSize.width, gameSize.height); this.scaleManager.resize(gameSize.width, gameSize.height);
// Override bug in canvas resizing in Phaser. Let's resize the canvas ourselves // Override bug in canvas resizing in Phaser. Let's resize the canvas ourselves
const style = this.scaleManager.canvas.style; const style = this.scaleManager.canvas.style;
style.width = Math.ceil(realSize.width / devicePixelRatio) + "px"; style.width = Math.ceil(realSize.width !== 0 ? realSize.width / devicePixelRatio : 0) + "px";
style.height = Math.ceil(realSize.height / devicePixelRatio) + "px"; style.height = Math.ceil(realSize.height !== 0 ? realSize.height / devicePixelRatio : 0) + "px";
// Resize the game element at the same size at the canvas // Resize the game element at the same size at the canvas
const gameStyle = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("game").style; const gameStyle = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("game").style;

View file

@ -2,14 +2,14 @@ import { get, writable } from "svelte/store";
import type { Box } from "../WebRtc/LayoutManager"; import type { Box } from "../WebRtc/LayoutManager";
import { HtmlUtils } from "../WebRtc/HtmlUtils"; import { HtmlUtils } from "../WebRtc/HtmlUtils";
import { LayoutMode } from "../WebRtc/LayoutManager"; import { LayoutMode } from "../WebRtc/LayoutManager";
import { layoutModeStore } from "./StreamableCollectionStore"; import { embedScreenLayout } from "./EmbedScreensStore";
/** /**
* Tries to find the biggest available box of remaining space (this is a space where we can center the character) * Tries to find the biggest available box of remaining space (this is a space where we can center the character)
*/ */
function findBiggestAvailableArea(): Box { function findBiggestAvailableArea(): Box {
const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>("#game canvas"); const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>("#game canvas");
if (get(layoutModeStore) === LayoutMode.VideoChat) { if (get(embedScreenLayout) === LayoutMode.VideoChat) {
const children = document.querySelectorAll<HTMLDivElement>("div.chat-mode > div"); const children = document.querySelectorAll<HTMLDivElement>("div.chat-mode > div");
const htmlChildren = Array.from(children.values()); const htmlChildren = Array.from(children.values());

View file

@ -0,0 +1,51 @@
import { derived, get, writable } from "svelte/store";
import type { CoWebsite } from "../WebRtc/CoWebsiteManager";
function createCoWebsiteStore() {
const { subscribe, set, update } = writable(Array<CoWebsite>());
set(Array<CoWebsite>());
return {
subscribe,
add: (coWebsite: CoWebsite, position?: number) => {
coWebsite.state.subscribe((value) => {
update((currentArray) => currentArray);
});
if (position || position === 0) {
update((currentArray) => {
if (position === 0) {
return [coWebsite, ...currentArray];
} else if (currentArray.length > position) {
const test = [...currentArray.splice(position, 0, coWebsite)];
return [...currentArray.splice(position, 0, coWebsite)];
}
return [...currentArray, coWebsite];
});
return;
}
update((currentArray) => [...currentArray, coWebsite]);
},
remove: (coWebsite: CoWebsite) => {
update((currentArray) => [
...currentArray.filter((currentCoWebsite) => currentCoWebsite.iframe.id !== coWebsite.iframe.id),
]);
},
empty: () => {
set(Array<CoWebsite>());
},
};
}
export const coWebsites = createCoWebsiteStore();
export const coWebsitesNotAsleep = derived([coWebsites], ([$coWebsites]) =>
$coWebsites.filter((coWebsite) => get(coWebsite.state) !== "asleep")
);
export const mainCoWebsite = derived([coWebsites], ([$coWebsites]) =>
$coWebsites.find((coWebsite) => get(coWebsite.state) !== "asleep")
);

View file

@ -0,0 +1,51 @@
import { derived, get, writable } from "svelte/store";
import type { CoWebsite } from "../WebRtc/CoWebsiteManager";
import { LayoutMode } from "../WebRtc/LayoutManager";
import { coWebsites } from "./CoWebsiteStore";
import { Streamable, streamableCollectionStore } from "./StreamableCollectionStore";
export type EmbedScreen =
| {
type: "streamable";
embed: Streamable;
}
| {
type: "cowebsite";
embed: CoWebsite;
};
function createHighlightedEmbedScreenStore() {
const { subscribe, set, update } = writable<EmbedScreen | null>(null);
return {
subscribe,
highlight: (embedScreen: EmbedScreen) => {
set(embedScreen);
},
removeHighlight: () => {
set(null);
},
toggleHighlight: (embedScreen: EmbedScreen) => {
update((currentEmbedScreen) =>
!currentEmbedScreen ||
embedScreen.type !== currentEmbedScreen.type ||
(embedScreen.type === "cowebsite" &&
currentEmbedScreen.type === "cowebsite" &&
embedScreen.embed.iframe.id !== currentEmbedScreen.embed.iframe.id) ||
(embedScreen.type === "streamable" &&
currentEmbedScreen.type === "streamable" &&
embedScreen.embed.uniqueId !== currentEmbedScreen.embed.uniqueId)
? embedScreen
: null
);
},
};
}
export const highlightedEmbedScreen = createHighlightedEmbedScreenStore();
export const embedScreenLayout = writable<LayoutMode>(LayoutMode.Presentation);
export const hasEmbedScreen = derived(
[streamableCollectionStore],
($values) => get(streamableCollectionStore).size + get(coWebsites).length > 0
);

View file

@ -1,17 +0,0 @@
import { writable } from "svelte/store";
/**
* A store that contains whether the game overlay is shown or not.
* Typically, the overlay is hidden when entering Jitsi meet.
*/
function createGameOverlayVisibilityStore() {
const { subscribe, set, update } = writable(false);
return {
subscribe,
showGameOverlay: () => set(true),
hideGameOverlay: () => set(false),
};
}
export const gameOverlayVisibilityStore = createGameOverlayVisibilityStore();

View file

@ -51,6 +51,6 @@ function createLayoutManagerAction() {
export const layoutManagerActionStore = createLayoutManagerAction(); export const layoutManagerActionStore = createLayoutManagerAction();
export const layoutManagerVisibilityStore = derived(layoutManagerActionStore, ($layoutManagerActionStore) => { export const layoutManagerActionVisibilityStore = derived(layoutManagerActionStore, ($layoutManagerActionStore) => {
return !!$layoutManagerActionStore.length; return !!$layoutManagerActionStore.length;
}); });

View file

@ -6,7 +6,7 @@ import { BrowserTooOldError } from "./Errors/BrowserTooOldError";
import { errorStore } from "./ErrorStore"; import { errorStore } from "./ErrorStore";
import { getNavigatorType, isIOS, NavigatorType } from "../WebRtc/DeviceUtils"; import { getNavigatorType, isIOS, NavigatorType } from "../WebRtc/DeviceUtils";
import { WebviewOnOldIOS } from "./Errors/WebviewOnOldIOS"; import { WebviewOnOldIOS } from "./Errors/WebviewOnOldIOS";
import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility"; import { myCameraVisibilityStore } from "./MyCameraStoreVisibility";
import { peerStore } from "./PeerStore"; import { peerStore } from "./PeerStore";
import { privacyShutdownStore } from "./PrivacyShutdownStore"; import { privacyShutdownStore } from "./PrivacyShutdownStore";
import { MediaStreamConstraintsError } from "./Errors/MediaStreamConstraintsError"; import { MediaStreamConstraintsError } from "./Errors/MediaStreamConstraintsError";
@ -233,7 +233,7 @@ export const mediaStreamConstraintsStore = derived(
[ [
requestedCameraState, requestedCameraState,
requestedMicrophoneState, requestedMicrophoneState,
gameOverlayVisibilityStore, myCameraVisibilityStore,
enableCameraSceneVisibilityStore, enableCameraSceneVisibilityStore,
videoConstraintStore, videoConstraintStore,
audioConstraintStore, audioConstraintStore,
@ -245,7 +245,7 @@ export const mediaStreamConstraintsStore = derived(
[ [
$requestedCameraState, $requestedCameraState,
$requestedMicrophoneState, $requestedMicrophoneState,
$gameOverlayVisibilityStore, $myCameraVisibilityStore,
$enableCameraSceneVisibilityStore, $enableCameraSceneVisibilityStore,
$videoConstraintStore, $videoConstraintStore,
$audioConstraintStore, $audioConstraintStore,
@ -283,7 +283,7 @@ export const mediaStreamConstraintsStore = derived(
} }
// Disable webcam and microphone when in a Jitsi // Disable webcam and microphone when in a Jitsi
if ($gameOverlayVisibilityStore === false) { if ($myCameraVisibilityStore === false) {
currentVideoConstraint = false; currentVideoConstraint = false;
currentAudioConstraint = false; currentAudioConstraint = false;
} }

View file

@ -0,0 +1,8 @@
import { writable } from "svelte/store";
/**
* A store that contains whether my camera & actions is shown or not.
* Typically, the overlay is hidden when entering Jitsi meet.
*/
export const myCameraVisibilityStore = writable(false);

View file

@ -1,7 +1,7 @@
import { derived, Readable, readable, writable } from "svelte/store"; import { derived, Readable, readable, writable } from "svelte/store";
import { peerStore } from "./PeerStore"; import { peerStore } from "./PeerStore";
import type { LocalStreamStoreValue } from "./MediaStore"; import type { LocalStreamStoreValue } from "./MediaStore";
import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility"; import { myCameraVisibilityStore } from "./MyCameraStoreVisibility";
declare const navigator: any; // eslint-disable-line @typescript-eslint/no-explicit-any declare const navigator: any; // eslint-disable-line @typescript-eslint/no-explicit-any
@ -41,8 +41,8 @@ let previousComputedAudioConstraint: boolean | MediaTrackConstraints = false;
* A store containing the media constraints we want to apply. * A store containing the media constraints we want to apply.
*/ */
export const screenSharingConstraintsStore = derived( export const screenSharingConstraintsStore = derived(
[requestedScreenSharingState, gameOverlayVisibilityStore, peerStore], [requestedScreenSharingState, myCameraVisibilityStore, peerStore],
([$requestedScreenSharingState, $gameOverlayVisibilityStore, $peerStore], set) => { ([$requestedScreenSharingState, $myCameraVisibilityStore, $peerStore], set) => {
let currentVideoConstraint: boolean | MediaTrackConstraints = true; let currentVideoConstraint: boolean | MediaTrackConstraints = true;
let currentAudioConstraint: boolean | MediaTrackConstraints = false; let currentAudioConstraint: boolean | MediaTrackConstraints = false;
@ -53,7 +53,7 @@ export const screenSharingConstraintsStore = derived(
} }
// Disable screen sharing when in a Jitsi // Disable screen sharing when in a Jitsi
if (!$gameOverlayVisibilityStore) { if (!$myCameraVisibilityStore) {
currentVideoConstraint = false; currentVideoConstraint = false;
currentAudioConstraint = false; currentAudioConstraint = false;
} }

View file

@ -1,13 +1,10 @@
import { derived, get, Readable, writable } from "svelte/store"; import { derived, get, Readable } from "svelte/store";
import { ScreenSharingLocalMedia, screenSharingLocalMedia } from "./ScreenSharingStore"; import { ScreenSharingLocalMedia, screenSharingLocalMedia } from "./ScreenSharingStore";
import { peerStore, screenSharingStreamStore } from "./PeerStore"; import { peerStore, screenSharingStreamStore } from "./PeerStore";
import type { RemotePeer } from "../WebRtc/SimplePeer"; import type { RemotePeer } from "../WebRtc/SimplePeer";
import { LayoutMode } from "../WebRtc/LayoutManager";
export type Streamable = RemotePeer | ScreenSharingLocalMedia; export type Streamable = RemotePeer | ScreenSharingLocalMedia;
export const layoutModeStore = writable<LayoutMode>(LayoutMode.Presentation);
/** /**
* A store that contains everything that can produce a stream (so the peers + the local screen sharing stream) * A store that contains everything that can produce a stream (so the peers + the local screen sharing stream)
*/ */

View file

@ -96,6 +96,7 @@ export class MapStore<K, V> extends Map<K, V> implements Readable<Map<K, V>> {
const unsubscribe = storeByKey.subscribe((newMapValue) => { const unsubscribe = storeByKey.subscribe((newMapValue) => {
if (unsubscribeDeepStore) { if (unsubscribeDeepStore) {
unsubscribeDeepStore(); unsubscribeDeepStore();
unsubscribeDeepStore = undefined;
} }
if (newMapValue === undefined) { if (newMapValue === undefined) {
set(undefined); set(undefined);
@ -115,6 +116,7 @@ export class MapStore<K, V> extends Map<K, V> implements Readable<Map<K, V>> {
unsubscribe(); unsubscribe();
if (unsubscribeDeepStore) { if (unsubscribeDeepStore) {
unsubscribeDeepStore(); unsubscribeDeepStore();
unsubscribeDeepStore = undefined;
} }
}; };
}); });

View file

@ -45,8 +45,28 @@ class UrlManager {
} }
public getStartLayerNameFromUrl(): string | null { public getStartLayerNameFromUrl(): string | null {
const hash = window.location.hash; const parameters = this.getHashParameters();
return hash.length > 1 ? hash.substring(1) : null; for (const key in parameters) {
if (parameters[key] === undefined) {
return key;
}
}
return null;
}
public getHashParameter(name: string): string | undefined {
return this.getHashParameters()[name];
}
private getHashParameters(): Record<string, string> {
return window.location.hash
.substring(1)
.split("&")
.reduce((res: Record<string, string>, item: string) => {
const parts = item.split("=");
res[parts[0]] = parts[1];
return res;
}, {});
} }
pushStartLayerNameToUrl(startLayerName: string): void { pushStartLayerNameToUrl(startLayerName: string): void {

View file

@ -0,0 +1,112 @@
type InternalBreakpoint = {
beforeBreakpoint: InternalBreakpoint | undefined;
nextBreakpoint: InternalBreakpoint | undefined;
pixels: number;
};
function generateBreakpointsMap(): Map<string, InternalBreakpoint> {
// If is changed don't forget to also change it on SASS.
const breakpoints: { [key: string]: number } = {
xs: 0,
sm: 576,
md: 768,
lg: 992,
xl: 1200,
xxl: 1400,
};
let beforeBreakpoint: InternalBreakpoint | undefined;
let beforeBreakpointTag: string | undefined;
const mapRender = new Map<string, InternalBreakpoint>();
for (const breakpoint in breakpoints) {
const newBreakpoint = {
beforeBreakpoint: beforeBreakpoint,
nextBreakpoint: undefined,
pixels: breakpoints[breakpoint],
};
if (beforeBreakpointTag && beforeBreakpoint) {
beforeBreakpoint.nextBreakpoint = newBreakpoint;
mapRender.set(beforeBreakpointTag, beforeBreakpoint);
}
mapRender.set(breakpoint, {
beforeBreakpoint: beforeBreakpoint,
nextBreakpoint: undefined,
pixels: breakpoints[breakpoint],
});
beforeBreakpointTag = breakpoint;
beforeBreakpoint = newBreakpoint;
}
return mapRender;
}
const breakpoints = generateBreakpointsMap();
export type Breakpoint = "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
export function isMediaBreakpointUp(breakpoint: Breakpoint): boolean {
if (breakpoint === "xxl") {
return true;
}
const breakpointObject = breakpoints.get(breakpoint);
if (!breakpointObject) {
throw new Error(`Unknown breakpoint: ${breakpoint}`);
}
if (!breakpointObject.nextBreakpoint) {
return false;
}
return breakpointObject.nextBreakpoint.pixels - 1 >= window.innerWidth;
}
export function isMediaBreakpointDown(breakpoint: Breakpoint): boolean {
if (breakpoint === "xs") {
return true;
}
const breakpointObject = breakpoints.get(breakpoint);
if (!breakpointObject) {
throw new Error(`Unknown breakpoint: ${breakpoint}`);
}
return breakpointObject.pixels <= window.innerWidth;
}
export function isMediaBreakpointOnly(breakpoint: Breakpoint): boolean {
const breakpointObject = breakpoints.get(breakpoint);
if (!breakpointObject) {
throw new Error(`Unknown breakpoint: ${breakpoint}`);
}
return (
breakpointObject.pixels <= window.innerWidth &&
(!breakpointObject.nextBreakpoint || breakpointObject.nextBreakpoint.pixels - 1 >= window.innerWidth)
);
}
export function isMediaBreakpointBetween(startBreakpoint: Breakpoint, endBreakpoint: Breakpoint): boolean {
const startBreakpointObject = breakpoints.get(startBreakpoint);
const endBreakpointObject = breakpoints.get(endBreakpoint);
if (!startBreakpointObject) {
throw new Error(`Unknown start breakpoint: ${startBreakpointObject}`);
}
if (!endBreakpointObject) {
throw new Error(`Unknown end breakpoint: ${endBreakpointObject}`);
}
return (
startBreakpointObject.pixels <= innerWidth &&
(!endBreakpointObject.nextBreakpoint || endBreakpointObject.nextBreakpoint.pixels - 1 >= window.innerWidth)
);
}

View file

@ -31,4 +31,8 @@ export class MathUtils {
const distance = Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2); const distance = Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2);
return squared ? Math.sqrt(distance) : distance; return squared ? Math.sqrt(distance) : distance;
} }
public static randomFromArray<T>(array: T[]): T {
return array[Math.floor(Math.random() * array.length)];
}
} }

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,11 @@ export class HtmlUtils {
throw new Error("Cannot find HTML element with id '" + id + "'"); throw new Error("Cannot find HTML element with id '" + id + "'");
} }
public static getElementById<T extends HTMLElement>(id: string): T | undefined {
const elem = document.getElementById(id);
return HtmlUtils.isHtmlElement<T>(elem) ? elem : undefined;
}
public static querySelectorOrFail<T extends HTMLElement>(selector: string): T { public static querySelectorOrFail<T extends HTMLElement>(selector: string): T {
const elem = document.querySelector<T>(selector); const elem = document.querySelector<T>(selector);
if (HtmlUtils.isHtmlElement<T>(elem)) { if (HtmlUtils.isHtmlElement<T>(elem)) {
@ -35,6 +40,7 @@ export class HtmlUtils {
const urlRegex = /(https?:\/\/[^\s]+)/g; const urlRegex = /(https?:\/\/[^\s]+)/g;
text = HtmlUtils.escapeHtml(text); text = HtmlUtils.escapeHtml(text);
return text.replace(urlRegex, (url: string) => { return text.replace(urlRegex, (url: string) => {
url = HtmlUtils.htmlDecode(url);
const link = document.createElement("a"); const link = document.createElement("a");
link.href = url; link.href = url;
link.target = "_blank"; link.target = "_blank";
@ -45,6 +51,15 @@ export class HtmlUtils {
}); });
} }
private static htmlDecode(input: string): string {
const doc = new DOMParser().parseFromString(input, "text/html");
const text = doc.documentElement.textContent;
if (text === null) {
throw new Error("Unexpected non parseable string");
}
return text;
}
public static isClickedInside(event: MouseEvent, target: HTMLElement): boolean { public static isClickedInside(event: MouseEvent, target: HTMLElement): boolean {
return !!event.composedPath().find((et) => et === target); return !!event.composedPath().find((et) => et === target);
} }

View file

@ -7,6 +7,7 @@ interface jitsiConfigInterface {
startWithAudioMuted: boolean; startWithAudioMuted: boolean;
startWithVideoMuted: boolean; startWithVideoMuted: boolean;
prejoinPageEnabled: boolean; prejoinPageEnabled: boolean;
disableDeepLinking: boolean;
} }
interface JitsiOptions { interface JitsiOptions {
@ -40,6 +41,7 @@ const getDefaultConfig = (): jitsiConfigInterface => {
startWithAudioMuted: !get(requestedMicrophoneState), startWithAudioMuted: !get(requestedMicrophoneState),
startWithVideoMuted: !get(requestedCameraState), startWithVideoMuted: !get(requestedCameraState),
prejoinPageEnabled: false, prejoinPageEnabled: false,
disableDeepLinking: false,
}; };
}; };
@ -132,64 +134,112 @@ class JitsiFactory {
return slugify(instance.replace("/", "-") + "-" + roomName); return slugify(instance.replace("/", "-") + "-" + roomName);
} }
public start( public async start(
roomName: string, roomName: string,
playerName: string, playerName: string,
jwt?: string, jwt?: string,
config?: object, config?: object,
interfaceConfig?: object, interfaceConfig?: object,
jitsiUrl?: string, jitsiUrl?: string
jitsiWidth?: number ) {
): Promise<CoWebsite> { const coWebsite = coWebsiteManager.searchJitsi();
return coWebsiteManager.addCoWebsite(
async (cowebsiteDiv) => {
// Jitsi meet external API maintains some data in local storage
// which is sent via the appData URL parameter when joining a
// conference. Problem is that this data grows indefinitely. Thus
// after some time the URLs get so huge that loading the iframe
// becomes slow and eventually breaks completely. Thus lets just
// clear jitsi local storage before starting a new conference.
window.localStorage.removeItem("jitsiLocalStorage");
const domain = jitsiUrl || JITSI_URL; if (coWebsite) {
if (domain === undefined) { await coWebsiteManager.closeCoWebsite(coWebsite);
throw new Error("Missing JITSI_URL environment variable or jitsiUrl parameter in the map."); }
}
await this.loadJitsiScript(domain);
const options: JitsiOptions = { // Jitsi meet external API maintains some data in local storage
roomName: roomName, // which is sent via the appData URL parameter when joining a
jwt: jwt, // conference. Problem is that this data grows indefinitely. Thus
width: "100%", // after some time the URLs get so huge that loading the iframe
height: "100%", // becomes slow and eventually breaks completely. Thus lets just
parentNode: cowebsiteDiv, // clear jitsi local storage before starting a new conference.
configOverwrite: mergeConfig(config), window.localStorage.removeItem("jitsiLocalStorage");
interfaceConfigOverwrite: { ...defaultInterfaceConfig, ...interfaceConfig },
};
if (!options.jwt) {
delete options.jwt;
}
return new Promise((resolve, reject) => { const domain = jitsiUrl || JITSI_URL;
const doResolve = (): void => { if (domain === undefined) {
const iframe = cowebsiteDiv.querySelector<HTMLIFrameElement>('[id*="jitsi" i]'); throw new Error("Missing JITSI_URL environment variable or jitsiUrl parameter in the map.");
if (iframe === null) { }
throw new Error("Could not find Jitsi Iframe"); await this.loadJitsiScript(domain);
}
resolve(iframe);
};
options.onload = () => doResolve(); //we want for the iframe to be loaded before triggering animations.
setTimeout(() => doResolve(), 2000); //failsafe in case the iframe is deleted before loading or too long to load
this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options);
this.jitsiApi.executeCommand("displayName", playerName);
this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback); const options: JitsiOptions = {
this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback); roomName: roomName,
jwt: jwt,
width: "100%",
height: "100%",
parentNode: coWebsiteManager.getCoWebsiteBuffer(),
configOverwrite: mergeConfig(config),
interfaceConfigOverwrite: { ...defaultInterfaceConfig, ...interfaceConfig },
};
if (!options.jwt) {
delete options.jwt;
}
const doResolve = (): void => {
const iframe = coWebsiteManager.getCoWebsiteBuffer().querySelector<HTMLIFrameElement>('[id*="jitsi" i]');
if (iframe && this.jitsiApi) {
const coWebsite = coWebsiteManager.addCoWebsiteFromIframe(iframe, false, undefined, 0, false, true);
this.jitsiApi.addListener("videoConferenceLeft", () => {
this.closeOrUnload(coWebsite);
}); });
},
jitsiWidth, this.jitsiApi.addListener("readyToClose", () => {
0 this.closeOrUnload(coWebsite);
); });
}
coWebsiteManager.resizeAllIframes();
};
this.jitsiApi = undefined;
options.onload = () => doResolve(); //we want for the iframe to be loaded before triggering animations.
setTimeout(() => doResolve(), 2000); //failsafe in case the iframe is deleted before loading or too long to load
this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options);
this.jitsiApi.executeCommand("displayName", playerName);
this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback);
}
private closeOrUnload = function (coWebsite: CoWebsite) {
if (coWebsite.closable) {
coWebsiteManager.closeCoWebsite(coWebsite).catch(() => {
console.error("Error during closing a Jitsi Meet");
});
} else {
coWebsiteManager.unloadCoWebsite(coWebsite).catch(() => {
console.error("Error during unloading a Jitsi Meet");
});
}
};
public restart() {
if (!this.jitsiApi) {
return;
}
this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback);
const coWebsite = coWebsiteManager.searchJitsi();
console.log("jitsi api ", this.jitsiApi);
console.log("iframe cowebsite", coWebsite?.iframe);
if (!coWebsite) {
this.destroy();
return;
}
this.jitsiApi.addListener("videoConferenceLeft", () => {
this.closeOrUnload(coWebsite);
});
this.jitsiApi.addListener("readyToClose", () => {
this.closeOrUnload(coWebsite);
});
} }
public stop() { public stop() {
@ -197,14 +247,16 @@ class JitsiFactory {
return; return;
} }
const jitsiCoWebsite = coWebsiteManager.searchJitsi();
if (jitsiCoWebsite) {
coWebsiteManager.closeJitsi().catch((e) => console.error(e));
}
this.jitsiApi.removeListener("audioMuteStatusChanged", this.audioCallback); this.jitsiApi.removeListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.removeListener("videoMuteStatusChanged", this.videoCallback); this.jitsiApi.removeListener("videoMuteStatusChanged", this.videoCallback);
}
public destroy() {
if (!this.jitsiApi) {
return;
}
this.stop();
this.jitsiApi?.dispose(); this.jitsiApi?.dispose();
} }

View file

@ -7,8 +7,7 @@ import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStor
export type StartScreenSharingCallback = (media: MediaStream) => void; export type StartScreenSharingCallback = (media: MediaStream) => void;
export type StopScreenSharingCallback = (media: MediaStream) => void; export type StopScreenSharingCallback = (media: MediaStream) => void;
import { cowebsiteCloseButtonId } from "./CoWebsiteManager"; import { myCameraVisibilityStore } from "../Stores/MyCameraStoreVisibility";
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
import { layoutManagerActionStore } from "../Stores/LayoutManagerStore"; import { layoutManagerActionStore } from "../Stores/LayoutManagerStore";
import { MediaStreamConstraintsError } from "../Stores/Errors/MediaStreamConstraintsError"; import { MediaStreamConstraintsError } from "../Stores/Errors/MediaStreamConstraintsError";
import { localUserStore } from "../Connexion/LocalUserStore"; import { localUserStore } from "../Connexion/LocalUserStore";
@ -20,8 +19,6 @@ export class MediaManager {
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>(); startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks: Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>(); stopScreenSharingCallBacks: Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
private triggerCloseJistiFrame: Map<String, Function> = new Map<String, Function>();
private userInputManager?: UserInputManager; private userInputManager?: UserInputManager;
constructor() { constructor() {
@ -73,36 +70,12 @@ export class MediaManager {
}); });
} }
public showGameOverlay(): void { public showMyCamera(): void {
const gameOverlay = HtmlUtils.getElementByIdOrFail("game-overlay"); myCameraVisibilityStore.set(true);
gameOverlay.classList.add("active");
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
const functionTrigger = () => {
this.triggerCloseJitsiFrameButton();
};
buttonCloseFrame.removeEventListener("click", () => {
buttonCloseFrame.blur();
functionTrigger();
});
gameOverlayVisibilityStore.showGameOverlay();
} }
public hideGameOverlay(): void { public hideMyCamera(): void {
const gameOverlay = HtmlUtils.getElementByIdOrFail("game-overlay"); myCameraVisibilityStore.set(false);
gameOverlay.classList.remove("active");
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
const functionTrigger = () => {
this.triggerCloseJitsiFrameButton();
};
buttonCloseFrame.addEventListener("click", () => {
buttonCloseFrame.blur();
functionTrigger();
});
gameOverlayVisibilityStore.hideGameOverlay();
} }
private getScreenSharingId(userId: string): string { private getScreenSharingId(userId: string): string {
@ -179,20 +152,6 @@ export class MediaManager {
return connectingSpinnerDiv; return connectingSpinnerDiv;
} }
public addTriggerCloseJitsiFrameButton(id: String, Function: Function) {
this.triggerCloseJistiFrame.set(id, Function);
}
public removeTriggerCloseJitsiFrameButton(id: String) {
this.triggerCloseJistiFrame.delete(id);
}
private triggerCloseJitsiFrameButton(): void {
for (const callback of this.triggerCloseJistiFrame.values()) {
callback();
}
}
public setUserInputManager(userInputManager: UserInputManager) { public setUserInputManager(userInputManager: UserInputManager) {
this.userInputManager = userInputManager; this.userInputManager = userInputManager;
} }

View file

@ -2,10 +2,10 @@ import type * as SimplePeerNamespace from "simple-peer";
import type { RoomConnection } from "../Connexion/RoomConnection"; import type { RoomConnection } from "../Connexion/RoomConnection";
import { MESSAGE_TYPE_CONSTRAINT, PeerStatus } from "./VideoPeer"; import { MESSAGE_TYPE_CONSTRAINT, PeerStatus } from "./VideoPeer";
import type { UserSimplePeerInterface } from "./SimplePeer"; import type { UserSimplePeerInterface } from "./SimplePeer";
import { Readable, readable } from "svelte/store"; import { Readable, readable, writable, Writable } from "svelte/store";
import { videoFocusStore } from "../Stores/VideoFocusStore";
import { getIceServersConfig } from "../Components/Video/utils"; import { getIceServersConfig } from "../Components/Video/utils";
import { isMobile } from "../Enum/EnvironmentVariable"; import { highlightedEmbedScreen } from "../Stores/EmbedScreensStore";
import { isMediaBreakpointUp } from "../Utils/BreakpointsUtils";
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer"); const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
@ -22,7 +22,7 @@ export class ScreenSharingPeer extends Peer {
public readonly userId: number; public readonly userId: number;
public readonly uniqueId: string; public readonly uniqueId: string;
public readonly streamStore: Readable<MediaStream | null>; public readonly streamStore: Readable<MediaStream | null>;
public readonly statusStore: Readable<PeerStatus>; public readonly statusStore: Writable<PeerStatus>;
constructor( constructor(
user: UserSimplePeerInterface, user: UserSimplePeerInterface,
@ -43,7 +43,10 @@ export class ScreenSharingPeer extends Peer {
this.streamStore = readable<MediaStream | null>(null, (set) => { this.streamStore = readable<MediaStream | null>(null, (set) => {
const onStream = (stream: MediaStream | null) => { const onStream = (stream: MediaStream | null) => {
videoFocusStore.focus(this); highlightedEmbedScreen.highlight({
type: "streamable",
embed: this,
});
set(stream); set(stream);
}; };
const onData = (chunk: Buffer) => { const onData = (chunk: Buffer) => {
@ -67,7 +70,7 @@ export class ScreenSharingPeer extends Peer {
}; };
}); });
this.statusStore = readable<PeerStatus>("connecting", (set) => { this.statusStore = writable<PeerStatus>("connecting", (set) => {
const onConnect = () => { const onConnect = () => {
set("connected"); set("connected");
}; };
@ -138,6 +141,12 @@ export class ScreenSharingPeer extends Peer {
if (!stream) { if (!stream) {
this.isReceivingStream = false; this.isReceivingStream = false;
} else { } else {
//Check if the peer connection is already connected status. In this case, the status store must be set to 'connected'.
//In the case or player A send stream and player B send a stream, it's same peer connection, also the status must be changed to connect.
//TODO add event listening when the stream is ready for displaying and change the status
if (this._connected) {
this.statusStore.set("connected");
}
this.isReceivingStream = true; this.isReceivingStream = true;
} }
} }
@ -177,7 +186,13 @@ export class ScreenSharingPeer extends Peer {
public stopPushingScreenSharingToRemoteUser(stream: MediaStream) { public stopPushingScreenSharingToRemoteUser(stream: MediaStream) {
this.removeStream(stream); this.removeStream(stream);
this.write( this.write(
new Buffer(JSON.stringify({ type: MESSAGE_TYPE_CONSTRAINT, streamEnded: true, isMobile: isMobile() })) new Buffer(
JSON.stringify({
type: MESSAGE_TYPE_CONSTRAINT,
streamEnded: true,
isMobile: isMediaBreakpointUp("md"),
})
)
); );
} }
} }

View file

@ -86,7 +86,7 @@ export class SimplePeer {
} }
); );
mediaManager.showGameOverlay(); mediaManager.showMyCamera();
//receive message start //receive message start
this.Connection.webRtcStartMessageStream.subscribe((message: UserSimplePeerInterface) => { this.Connection.webRtcStartMessageStream.subscribe((message: UserSimplePeerInterface) => {
@ -259,7 +259,7 @@ export class SimplePeer {
console.warn( console.warn(
"closeScreenSharingConnection => Tried to close connection for user " + "closeScreenSharingConnection => Tried to close connection for user " +
userId + userId +
" but could not find user" " but could not find user or no peer connection started"
); );
return; return;
} }

View file

@ -9,7 +9,7 @@ import { localStreamStore, obtainedMediaConstraintStore, ObtainedMediaStreamCons
import { playersStore } from "../Stores/PlayersStore"; import { playersStore } from "../Stores/PlayersStore";
import { chatMessagesStore, newChatMessageSubject } from "../Stores/ChatStore"; import { chatMessagesStore, newChatMessageSubject } from "../Stores/ChatStore";
import { getIceServersConfig } from "../Components/Video/utils"; import { getIceServersConfig } from "../Components/Video/utils";
import { isMobile } from "../Enum/EnvironmentVariable"; import { isMediaBreakpointUp } from "../Utils/BreakpointsUtils";
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer"); const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
@ -202,7 +202,7 @@ export class VideoPeer extends Peer {
JSON.stringify({ JSON.stringify({
type: MESSAGE_TYPE_CONSTRAINT, type: MESSAGE_TYPE_CONSTRAINT,
...constraints, ...constraints,
isMobile: isMobile(), isMobile: isMediaBreakpointUp("md"),
}) })
) )
); );

View file

@ -0,0 +1,10 @@
import type { Translation } from "../i18n-types";
const audio: NonNullable<Translation["audio"]> = {
manager: {
reduce: "Während Unterhaltungen verringern",
},
message: "Sprachnachricht",
};
export default audio;

View file

@ -0,0 +1,22 @@
import type { Translation } from "../i18n-types";
const camera: NonNullable<Translation["camera"]> = {
enable: {
title: "Bitte schalte deine Kamera und dein Mikrofon ein.",
start: "Los gehts!",
},
help: {
title: "Zugriff auf Kamera / Mikrofon erforderlich",
permissionDenied: "Zugriff verweigert",
content: "Der Zugriff auf Kamera und Mikrofon muss im Browser freigegeben werden.",
firefoxContent:
'Bitte klicke auf "Diese Entscheidungen speichern" Schaltfläche um erneute Nachfragen nach der Freigabe in Firefox zu verhindern.',
refresh: "Aktualisieren",
continue: "Ohne Kamera fortfahren",
},
my: {
silentZone: "Stiller Bereich",
},
};
export default camera;

View file

@ -0,0 +1,12 @@
import type { Translation } from "../i18n-types";
const chat: NonNullable<Translation["chat"]> = {
intro: "Hier ist dein Nachrichtenverlauf:",
enter: "Verfasse deine Nachricht...",
menu: {
visitCard: "Visitenkarte",
addFriend: "Freund*In hinzufügen",
},
};
export default chat;

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