Co-website management move to Svelte

This commit is contained in:
Alexis Faizeau 2022-01-05 10:30:27 +01:00
parent 0bf1acfefb
commit 4f068c72be
33 changed files with 1409 additions and 759 deletions

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

@ -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

@ -166,7 +166,7 @@
margin-right: auto; margin-right: auto;
left: 0; left: 0;
right: 0; right: 0;
z-index: 200; 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

@ -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,100 @@
<script lang="typescript">
import { onMount } from "svelte";
import { ICON_URL } from "../../Enum/EnvironmentVariable";
import { 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 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;
});
async function toggleHighlightEmbedScreen() {
if (vertical) {
coWebsiteManager.goToMain(coWebsite);
} else if ($mainCoWebsite) {
highlightedEmbedScreen.toggleHighlight({
type: "cowebsite",
embed: coWebsite,
});
}
if ($state === "asleep") {
await coWebsiteManager.loadCoWebsite(coWebsite);
}
coWebsiteManager.resizeAllIframes();
}
function noDrag() {
return false;
}
</script>
<div
id={"cowebsite-thumbnail-" + index}
class="cowebsite-thumbnail nes-container is-rounded nes-pointer"
class:asleep={$state === "asleep"}
class:loading={$state === "loading"}
class:ready={$state === "ready"}
class:vertical
on:click={toggleHighlightEmbedScreen}
>
<img class="cowebsite-icon noselect nes-pointer" bind:this={icon} on:dragstart|preventDefault={noDrag} alt="" />
</div>
<style lang="scss">
.cowebsite-thumbnail {
padding: 0;
background-color: rgba(#000000, 0.6);
margin: 1%;
margin-top: auto;
margin-bottom: auto;
.cowebsite-icon {
width: 50px;
height: 50px;
object-fit: cover;
}
&.vertical {
margin: 7px;
.cowebsite-icon {
width: 40px;
height: 40px;
}
}
&.asleep {
filter: grayscale(100%);
--webkit-filter: grayscale(100%);
}
&.loading {
animation: 2500ms ease-in-out 0s infinite alternate backgroundLoading;
}
@keyframes backgroundLoading {
0% {
background-color: rgba(#000000, 0.6);
}
100% {
background-color: #25598e;
}
}
}
</style>

View file

@ -0,0 +1,34 @@
<script lang="typescript">
import { coWebsiteThumbails } from "../../Stores/CoWebsiteStore";
import CoWebsiteThumbnail from "./CoWebsiteThumbnailSlot.svelte";
export let vertical = false;
</script>
{#if $coWebsiteThumbails.length > 0}
<div id="cowebsite-thumbnail-container" class:vertical>
{#each [...$coWebsiteThumbails.values()] as coWebsite, index (coWebsite.iframe.id)}
<CoWebsiteThumbnail {index} {coWebsite} {vertical} />
{/each}
</div>
{/if}
<style lang="scss">
#cowebsite-thumbnail-container {
pointer-events: all;
height: 12%;
display: flex;
overflow-x: auto;
overflow-y: hidden;
&.vertical {
height: auto !important;
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,169 @@
<script lang="ts">
import { highlightedEmbedScreen } from "../../../Stores/EmbedScreensStore";
import CamerasContainer from "../CamerasContainer.svelte";
import CoWebsitesContainer from "../CoWebsitesContainer.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");
});
}
}
}
function minimiseCoWebsite() {
if ($highlightedEmbedScreen?.type === "cowebsite") {
highlightedEmbedScreen.removeHighlight();
coWebsiteManager.resizeAllIframes();
}
}
function expandCoWebsite() {
if ($highlightedEmbedScreen?.type === "cowebsite") {
coWebsiteManager.goToMain($highlightedEmbedScreen.embed);
}
}
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-primary expand" on:click={expandCoWebsite}
>></button
>
<button
type="button"
class="nes-btn is-secondary minimise"
on:click={minimiseCoWebsite}>=</button
>
<button type="button" class="nes-btn is-error close" on:click={closeCoWebsite}
>&times;</button
>
</div>
</div>
{/key}
{/if}
{/if}
</div>
{#if displayCoWebsiteContainer}
<CoWebsitesContainer />
{/if}
</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: 82%;
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

@ -87,7 +87,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
position: absolute; position: absolute;
z-index: 101; z-index: 300;
} }
.emote-menu { .emote-menu {

View file

@ -121,7 +121,7 @@
right: 0; right: 0;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
z-index: 150; z-index: 400;
} }
div.interact-menu { div.interact-menu {

View file

@ -60,7 +60,7 @@
margin-top: 4%; margin-top: 4%;
max-height: 80vh; max-height: 80vh;
max-width: 80vw; max-width: 80vw;
z-index: 250; z-index: 600;
overflow: auto; overflow: auto;
text-align: center; text-align: center;

View file

@ -1,7 +1,7 @@
<script lang="typescript"> <script lang="typescript">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { audioManagerVisibilityStore } from "../Stores/AudioManagerStore"; import { audioManagerVisibilityStore } from "../Stores/AudioManagerStore";
import { hasEmbedScreen } from "../Stores/EmbedScreensStore"; import { embedScreenLayout, hasEmbedScreen } from "../Stores/EmbedScreensStore";
import { emoteMenuStore } from "../Stores/EmoteStore"; import { emoteMenuStore } from "../Stores/EmoteStore";
import { myCameraVisibilityStore } from "../Stores/MyCameraStoreVisibility"; import { myCameraVisibilityStore } from "../Stores/MyCameraStoreVisibility";
import { requestVisitCardsStore } from "../Stores/GameStore"; import { requestVisitCardsStore } from "../Stores/GameStore";
@ -30,11 +30,12 @@
import BanMessageContainer from "./TypeMessage/BanMessageContainer.svelte"; import BanMessageContainer from "./TypeMessage/BanMessageContainer.svelte";
import { textMessageStore } from "../Stores/TypeMessageStore/TextMessageStore"; import { textMessageStore } from "../Stores/TypeMessageStore/TextMessageStore";
import TextMessageContainer from "./TypeMessage/TextMessageContainer.svelte"; import TextMessageContainer from "./TypeMessage/TextMessageContainer.svelte";
import { soundPlayingStore } from "../Stores/SoundPlayingStore"; import { soundPlayingStore } from "../Stores/SoundPlayingStore";
import AudioPlaying from "./UI/AudioPlaying.svelte"; import AudioPlaying from "./UI/AudioPlaying.svelte";
import { showLimitRoomModalStore, showShareLinkMapModalStore } from "../Stores/ModalStore"; import { showLimitRoomModalStore, showShareLinkMapModalStore } from "../Stores/ModalStore";
import LimitRoomModal from "./Modal/LimitRoomModal.svelte"; import LimitRoomModal from "./Modal/LimitRoomModal.svelte";
import ShareLinkMapModal from "./Modal/ShareLinkMapModal.svelte"; import ShareLinkMapModal from "./Modal/ShareLinkMapModal.svelte";
import { LayoutMode } from "../WebRtc/LayoutManager";
let mainLayout: HTMLDivElement; let mainLayout: HTMLDivElement;
@ -55,7 +56,7 @@ import ShareLinkMapModal from "./Modal/ShareLinkMapModal.svelte";
<MenuIcon /> <MenuIcon />
{/if} {/if}
{#if displayCoWebsiteContainer} {#if $embedScreenLayout === LayoutMode.VideoChat || displayCoWebsiteContainer}
<CoWebsitesContainer vertical={true} /> <CoWebsitesContainer vertical={true} />
{/if} {/if}
</aside> </aside>
@ -75,14 +76,14 @@ import ShareLinkMapModal from "./Modal/ShareLinkMapModal.svelte";
<AudioPlaying url={$soundPlayingStore} /> <AudioPlaying url={$soundPlayingStore} />
{/if} {/if}
{#if $showReportScreenStore !== userReportEmpty}
<ReportMenu />
{/if}
{#if $warningContainerStore} {#if $warningContainerStore}
<WarningContainer /> <WarningContainer />
{/if} {/if}
{#if $showReportScreenStore !== userReportEmpty}
<ReportMenu />
{/if}
{#if $helpCameraSettingsVisibleStore} {#if $helpCameraSettingsVisibleStore}
<HelpCameraSettingsPopup /> <HelpCameraSettingsPopup />
{/if} {/if}
@ -133,7 +134,7 @@ import ShareLinkMapModal from "./Modal/ShareLinkMapModal.svelte";
#main-layout { #main-layout {
display: grid; display: grid;
grid-template-columns: 7% 93%; grid-template-columns: 120px calc(100% - 120px);
grid-template-rows: 80% 20%; grid-template-rows: 80% 20%;
&-left-aside { &-left-aside {

View file

@ -74,6 +74,7 @@
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss"; @import "../../../style/breakpoints.scss";
.menuIcon { .menuIcon {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -89,9 +90,11 @@
margin: 5%; margin: 5%;
} }
} }
.menuIcon img:hover { .menuIcon img:hover {
transform: scale(1.2); transform: scale(1.2);
} }
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
.menuIcon { .menuIcon {
margin-top: 10%; margin-top: 10%;

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

@ -108,7 +108,7 @@
pointer-events: auto; pointer-events: auto;
background-color: #333333; background-color: #333333;
color: whitesmoke; color: whitesmoke;
z-index: 300; z-index: 650;
position: absolute; position: absolute;
height: 70vh; height: 70vh;
width: 50vw; width: 50vw;

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

@ -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

@ -8,16 +8,23 @@
export let streamable: Streamable; export let streamable: Streamable;
export let isHightlighted = false; export let isHightlighted = false;
export let clickable = false; export let isClickable = false;
export let mozaicFullWidth = false;
export let mozaicQuarter = false;
</script> </script>
<div class="media-container nes-container is-rounded {isHightlighted ? 'hightlighted' : ''}" class:clickable> <div
class="media-container nes-container is-rounded {isHightlighted ? 'hightlighted' : ''}"
class:clickable={isClickable}
class:mozaic-full-width={mozaicFullWidth}
class:mozaic-quarter={mozaicQuarter}
>
{#if streamable instanceof VideoPeer} {#if streamable instanceof VideoPeer}
<VideoMediaBox peer={streamable} {clickable} /> <VideoMediaBox peer={streamable} clickable={isClickable} />
{:else if streamable instanceof ScreenSharingPeer} {:else if streamable instanceof ScreenSharingPeer}
<ScreenSharingMediaBox peer={streamable} {clickable} /> <ScreenSharingMediaBox peer={streamable} clickable={isClickable} />
{:else} {:else}
<LocalStreamMediaBox peer={streamable} {clickable} cssClass="" /> <LocalStreamMediaBox peer={streamable} clickable={isClickable} cssClass="" />
{/if} {/if}
</div> </div>
@ -57,6 +64,32 @@
} }
} }
&.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;
}
}
&.clickable { &.clickable {
cursor: url("../../../style/images/cursor_pointer.png"), pointer; cursor: url("../../../style/images/cursor_pointer.png"), pointer;
} }

View file

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

View file

@ -42,7 +42,7 @@
font-family: Lato; font-family: Lato;
min-width: 300px; min-width: 300px;
opacity: 0.9; opacity: 0.9;
z-index: 270; z-index: 700;
h2 { h2 {
padding: 5px; padding: 5px;
} }

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

@ -2145,8 +2145,8 @@ ${escapedMessage}
public stopJitsi(): void { public stopJitsi(): void {
const coWebsite = coWebsiteManager.searchJitsi(); const coWebsite = coWebsiteManager.searchJitsi();
if (coWebsite) { if (coWebsite) {
coWebsiteManager.closeCoWebsite(coWebsite).catch(() => { coWebsiteManager.closeCoWebsite(coWebsite).catch((e) => {
console.error("Error during Jitsi co-website closing"); console.error("Error during Jitsi co-website closing", e);
}); });
} }
} }

View file

@ -0,0 +1,66 @@
import { derived, get, writable } from "svelte/store";
import type { CoWebsite } from "../WebRtc/CoWebsiteManager";
import { highlightedEmbedScreen } from "./EmbedScreensStore";
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")
);
export const coWebsiteThumbails = derived(
[coWebsites, highlightedEmbedScreen, mainCoWebsite],
([$coWebsites, highlightedEmbedScreen, $mainCoWebsite]) =>
$coWebsites.filter((coWebsite, index) => {
return (
(!$mainCoWebsite || $mainCoWebsite.iframe.id !== coWebsite.iframe.id) &&
(!highlightedEmbedScreen ||
highlightedEmbedScreen.type !== "cowebsite" ||
(highlightedEmbedScreen.type === "cowebsite" &&
highlightedEmbedScreen.embed.iframe.id !== coWebsite.iframe.id))
);
})
);

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
);

File diff suppressed because it is too large Load diff

View file

@ -132,64 +132,64 @@ 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%",
jitsiWidth, height: "100%",
0 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) {
coWebsiteManager.addCoWebsiteFromIframe(iframe, false, undefined, 0, false, true);
}
coWebsiteManager.resizeAllIframes();
};
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);
} }
public stop() { public stop() {
@ -197,12 +197,6 @@ 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);
this.jitsiApi?.dispose(); this.jitsiApi?.dispose();

View file

@ -1,220 +1,3 @@
/* A potentially shared website could appear in an iframe in the cowebsite space. */ @import "cowebsite/global";
@import "cowebsite/short-screens";
#cowebsite { @import "cowebsite/wide-screens";
position: fixed;
z-index: 200;
transition: transform 0.5s;
background-color: whitesmoke;
display: none;
&.loading {
background-color: gray;
}
main {
iframe {
width: 100%;
height: 100%;
max-width: 100vw;
max-height: 100vh;
}
}
aside {
background: gray;
align-items: center;
display: flex;
flex-direction: column;
justify-content: space-between;
#cowebsite-aside-holder {
background: gray;
height: 20px;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
img {
width: 80%;
pointer-events: none;
}
}
#cowebsite-aside-buttons {
display: flex;
flex-direction: column;
margin-bottom: auto;
flex: 1;
justify-content: start;
}
.top-right-btn{
transform: scale(0.5);
cursor: url('./images/cursor_pointer.png'), pointer;
}
#cowebsite-sub-icons {
display: flex;
margin-top: auto;
visibility: hidden;
justify-content: end;
flex: 1;
}
}
&-container {
position: absolute;
display: none;
height: 100%;
width: 100%;
&-main {
padding: 2% 5%;
height: 50%;
}
&-sub {
position: absolute !important;
display: inline-flex;
justify-content: center;
align-items: center;
bottom: 23%;
height: 20% !important;
width: 100%;
}
}
&-slot-0 {
z-index: 70 !important;
background-color: whitesmoke;
}
@for $i from 1 through 4 {
&-slot-#{$i} {
transition: transform 0.5s;
position: relative;
height: 100%;
display: none;
background-color: #333333;
@if $i == 1 {
width: 100%;
} @else {
width: 33%;
margin: 5px;
}
.overlay {
width: 100%;
height: 100%;
z-index: 50;
position: absolute;
display: flex;
flex-direction: column;
.actions-move {
display: none;
flex-direction: row;
justify-content: center;
align-items: center;
position: absolute;
height: 100%;
width: 100%;
gap: 10%;
}
&:hover {
background-color: rgba($color: #333333, $alpha: 0.6);
.actions-move {
display: flex;
}
}
}
.actions {
pointer-events: all !important;
margin: 3% 2%;
display: flex;
flex-direction: row;
justify-content: end;
position: relative;
z-index: 50;
button {
width: 32px;
height: 32px;
margin: 8px;
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
&-buffer {
iframe {
z-index: 45 !important;
pointer-events: none !important;
overflow: hidden;
border: 0;
position: absolute;
}
.main {
pointer-events: all !important;
z-index: 205 !important;
}
.sub-main {
pointer-events: all !important;
}
.thumbnail {
transform: scale(0.5, 0.5);
}
}
.pixel {
visibility: hidden;
height: 1px;
width: 1px;
}
}
@media (min-aspect-ratio: 1/1) {
#cowebsite {
right: 0;
top: 0;
width: 50%;
height: 100vh;
display: none;
&.loading {
transform: translateX(90%);
}
&.hidden {
transform: translateX(100%);
}
main {
width: 100%;
}
aside {
width: 30px;
img {
transform: rotate(90deg);
}
}
&-aside-holder {
cursor: ew-resize;
}
}
}

View file

@ -0,0 +1,119 @@
#cowebsite {
position: fixed;
z-index: 820;
transition: transform 0.5s;
background-color: rgba(10, 9, 9, 0.8);
display: none;
main {
iframe {
width: 100%;
height: 100%;
max-width: 100vw;
max-height: 100vh;
}
}
aside {
background: gray;
align-items: center;
display: flex;
flex-direction: column;
justify-content: space-between;
#cowebsite-aside-holder {
background: gray;
height: 20px;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
img {
width: 80%;
pointer-events: none;
}
}
#cowebsite-aside-buttons {
display: flex;
flex-direction: column;
margin-bottom: auto;
flex: 1;
justify-content: start;
}
.top-right-btn {
transform: scale(0.5);
cursor: url("./images/cursor_pointer.png"), pointer;
}
#cowebsite-other-actions {
display: flex;
margin-top: auto;
visibility: hidden;
justify-content: end;
flex: 1;
}
}
&-loader {
width: 20%;
#smoke {
@for $i from 1 through 3 {
#trail-#{$i} {
@for $y from 1 through 3 {
#trail-#{$i}-state-#{$y} {
visibility: hidden;
}
}
}
}
}
}
&-slot-main {
z-index: 70 !important;
background-color: rgba(10, 9, 9, 0);
display: flex;
justify-content: center;
align-items: center;
}
&-buffer {
iframe {
z-index: 45 !important;
pointer-events: none !important;
overflow: hidden;
border: 0;
position: absolute;
&.pixel {
height: 1px !important;
width: 1px !important;
}
}
.main {
pointer-events: all !important;
z-index: 821 !important;
}
.highlighted {
pointer-events: all !important;
padding: 4px;
}
.thumbnail {
transform: scale(0.5, 0.5);
}
}
.pixel {
visibility: hidden;
height: 1px;
width: 1px;
}
}

View file

@ -0,0 +1,84 @@
@include media-breakpoint-up(md) {
#main-container {
display: flex;
flex-direction: column-reverse;
}
#cowebsite {
left: 0;
top: 0;
width: 100%;
height: 50%;
display: flex;
flex-direction: column-reverse;
visibility: collapse;
transform: translateY(-100%);
&.loading {
visibility: visible;
transform: translateY(0%);
}
&.opened {
visibility: visible;
transform: translateY(0%);
}
&.closing {
visibility: visible;
}
&-loader {
height: 20%;
}
main {
height: 100%;
}
aside {
height: 50px;
flex-direction: row-reverse;
align-items: center;
display: flex;
justify-content: space-between;
#cowebsite-aside-holder {
height: 100%;
cursor: ns-resize;
img {
height: 100%;
}
}
#cowebsite-aside-buttons {
flex-direction: row-reverse;
margin-left: auto;
margin-bottom: 0;
justify-content: end;
}
#cowebsite-fullscreen {
padding-top: 0;
}
#cowebsite-other-actions {
display: inline-flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 0;
height: 100%;
visibility: visible;
}
.top-right-btn {
img {
width: 15px;
}
}
}
}
}

View file

@ -0,0 +1,41 @@
@include media-breakpoint-down(lg) {
#cowebsite {
right: 0;
top: 0;
width: 50%;
height: 100vh;
display: flex;
visibility: collapse;
transform: translateX(100%);
&.loading {
visibility: visible;
transform: translateX(0%);
}
&.opened {
visibility: visible;
transform: translateX(0%);
}
&.closing {
visibility: visible;
}
main {
width: 100%;
}
aside {
width: 30px;
img {
transform: rotate(90deg);
}
}
&-aside-holder {
cursor: ew-resize;
}
}
}