Merge pull request #1812 from thecodingmachine/develop

Deploy 2022-02-01
This commit is contained in:
David Négrier 2022-02-01 19:01:26 +01:00 committed by GitHub
commit 95b471f809
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
228 changed files with 9510 additions and 4188 deletions

View file

@ -10,7 +10,6 @@ on:
pull_request: pull_request:
jobs: jobs:
continuous-integration-front: continuous-integration-front:
name: "Continuous Integration Front" name: "Continuous Integration Front"
@ -46,6 +45,10 @@ jobs:
run: ./templater.sh run: ./templater.sh
working-directory: "front" working-directory: "front"
- name: "Generate i18n files"
run: yarn run typesafe-i18n
working-directory: "front"
- name: "Build" - name: "Build"
run: yarn run build run: yarn run build
env: env:

View file

@ -32,7 +32,7 @@ jobs:
mode: start mode: start
github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
ec2-image-id: ami-094dbcc53250a2480 ec2-image-id: ami-094dbcc53250a2480
ec2-instance-type: t3.xlarge ec2-instance-type: m5.2xlarge
subnet-id: subnet-0ac40025f559df1bc subnet-id: subnet-0ac40025f559df1bc
security-group-id: sg-0e36e96e3b8ed2d64 security-group-id: sg-0e36e96e3b8ed2d64
#iam-role-name: my-role-name # optional, requires additional permissions #iam-role-name: my-role-name # optional, requires additional permissions

View file

@ -43,6 +43,10 @@ jobs:
run: ./templater.sh run: ./templater.sh
working-directory: "front" working-directory: "front"
- name: "Generate i18n files"
run: yarn run typesafe-i18n
working-directory: "front"
- name: "Build" - name: "Build"
run: yarn run build-typings run: yarn run build-typings
env: env:

View file

@ -1,13 +1,13 @@
# Contributing to WorkAdventure # Contributing to WorkAdventure
Are you looking to help on WorkAdventure? Awesome, feel welcome and read the following sections in order to know how to Are you looking to help on WorkAdventure? Awesome, feel welcome and read the following sections in order to know how to
ask questions and how to work on something. ask questions and how to work on something.
## Contributions we are seeking ## Contributions we are seeking
We love to receive contributions from our community — you! We love to receive contributions from our community — you!
There are many ways to contribute, from writing tutorials or blog posts, improving the documentation, There are many ways to contribute, from writing tutorials or blog posts, improving the documentation,
submitting bug reports and feature requests or writing code which can be incorporated into WorkAdventure itself. submitting bug reports and feature requests or writing code which can be incorporated into WorkAdventure itself.
## Contributing external resources ## Contributing external resources
@ -16,7 +16,7 @@ You can share your work on maps / articles / videos related to WorkAdventure on
## Developer documentation ## Developer documentation
Documentation targeted at developers can be found in the [`/docs/dev`](docs/dev/) Documentation targeted at developers can be found in the [`/docs/dev`](docs/dev/)
## Using the issue tracker ## Using the issue tracker
@ -34,11 +34,11 @@ Finally, you can come and talk to the WorkAdventure core team... on WorkAdventur
## Pull requests ## Pull requests
Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope
and avoid containing unrelated commits. and avoid containing unrelated commits.
Please ask first before embarking on any significant pull request (e.g. implementing features, refactoring code), Please ask first before embarking on any significant pull request (e.g. implementing features, refactoring code),
otherwise you risk spending a lot of time working on something that the project's developers might not want to merge otherwise you risk spending a lot of time working on something that the project's developers might not want to merge
into the project. into the project.
You can ask us on [Discord](https://discord.gg/YGtngdh9gt) or in the [GitHub issues](https://github.com/thecodingmachine/workadventure/issues). You can ask us on [Discord](https://discord.gg/YGtngdh9gt) or in the [GitHub issues](https://github.com/thecodingmachine/workadventure/issues).
@ -54,7 +54,7 @@ $ yarn install
$ yarn run prepare $ yarn run prepare
``` ```
If you don't have the precommit hook installed (or if you committed code before installing the precommit hook), you will need If you don't have the precommit hook installed (or if you committed code before installing the precommit hook), you will need
to run code linting manually: to run code linting manually:
```console ```console
@ -72,7 +72,7 @@ Nevertheless, if your code can be unit tested, please provide a unit test (we us
If you are providing a new feature, you should setup a test map in the `maps/tests` directory. The test map should contain If you are providing a new feature, you should setup a test map in the `maps/tests` directory. The test map should contain
some description text describing how to test the feature. some description text describing how to test the feature.
* if the features is meant to be manually tested, you should modify the `maps/tests/index.html` file to add a reference * if the features is meant to be manually tested, you should modify the `maps/tests/index.html` file to add a reference
to your newly created test map to your newly created test map
* if the features can be automatically tested, please provide a testcafe test * if the features can be automatically tested, please provide a testcafe test
@ -90,8 +90,8 @@ $ npm run test
``` ```
Note: If your tests fail on a Javascript error in "sockjs", this is due to the Note: If your tests fail on a Javascript error in "sockjs", this is due to the
Webpack live reload. The Webpack live reload feature is conflicting with testcafe. This is why we recommend starting Webpack live reload. The Webpack live reload feature is conflicting with testcafe. This is why we recommend starting
WorkAdventure with the `LIVE_RELOAD=0` environment variable. WorkAdventure with the `LIVE_RELOAD=0` environment variable.
End-to-end tests can take a while to run. To run only one test, use: End-to-end tests can take a while to run. To run only one test, use:
@ -107,3 +107,7 @@ $ LIVE_RELOAD=0 docker-compose up -d
# Wait 2-3 minutes for the environment to start, then: # Wait 2-3 minutes for the environment to start, then:
$ PROJECT_DIR=$(pwd) docker-compose -f docker-compose.testcafe.yml up $ PROJECT_DIR=$(pwd) docker-compose -f docker-compose.testcafe.yml up
``` ```
### A bad wording or a missing language
If you notice a translation error or missing language you can help us by following the [how to translate](docs/dev/how-to-translate.md) documentation.

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

@ -1,11 +1,10 @@
import "jasmine"; import "jasmine";
import {PositionNotifier} from "../src/Model/PositionNotifier"; import { PositionNotifier } from "../src/Model/PositionNotifier";
import {User, UserSocket} from "../src/Model/User"; import { User, UserSocket } from "../src/Model/User";
import {Zone} from "_Model/Zone"; import { Zone } from "_Model/Zone";
import {Movable} from "_Model/Movable"; import { Movable } from "_Model/Movable";
import {PositionInterface} from "_Model/PositionInterface"; import { PositionInterface } from "_Model/PositionInterface";
import {ZoneSocket} from "../src/RoomManager"; import { ZoneSocket } from "../src/RoomManager";
describe("PositionNotifier", () => { describe("PositionNotifier", () => {
it("should receive notifications when player moves", () => { it("should receive notifications when player moves", () => {
@ -13,28 +12,59 @@ describe("PositionNotifier", () => {
let moveTriggered = false; let moveTriggered = false;
let leaveTriggered = false; let leaveTriggered = false;
const positionNotifier = new PositionNotifier(300, 300, (thing: Movable) => { const positionNotifier = new PositionNotifier(
enterTriggered = true; 300,
}, (thing: Movable, position: PositionInterface) => { 300,
moveTriggered = true; (thing: Movable) => {
}, (thing: Movable) => { enterTriggered = true;
leaveTriggered = true; },
}, () => {}, (thing: Movable, position: PositionInterface) => {
() => {}); moveTriggered = true;
},
(thing: Movable) => {
leaveTriggered = true;
},
() => {},
() => {}
);
const user1 = new User(1, 'test', '10.0.0.2', { const user1 = new User(
x: 500, 1,
y: 500, "test",
moving: false, "10.0.0.2",
direction: 'down' {
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []); x: 500,
y: 500,
moving: false,
direction: "down",
},
false,
positionNotifier,
{} as UserSocket,
[],
null,
"foo",
[]
);
const user2 = new User(2, 'test', '10.0.0.2', { const user2 = new User(
x: -9999, 2,
y: -9999, "test",
moving: false, "10.0.0.2",
direction: 'down' {
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []); x: -9999,
y: -9999,
moving: false,
direction: "down",
},
false,
positionNotifier,
{} as UserSocket,
[],
null,
"foo",
[]
);
positionNotifier.addZoneListener({} as ZoneSocket, 0, 0); positionNotifier.addZoneListener({} as ZoneSocket, 0, 0);
positionNotifier.addZoneListener({} as ZoneSocket, 0, 1); positionNotifier.addZoneListener({} as ZoneSocket, 0, 1);
@ -47,21 +77,21 @@ describe("PositionNotifier", () => {
bottom: 500 bottom: 500
});*/ });*/
user2.setPosition({x: 500, y: 500, direction: 'down', moving: false}); user2.setPosition({ x: 500, y: 500, direction: "down", moving: false });
expect(enterTriggered).toBe(true); expect(enterTriggered).toBe(true);
expect(moveTriggered).toBe(false); expect(moveTriggered).toBe(false);
enterTriggered = false; enterTriggered = false;
// Move inside the zone // Move inside the zone
user2.setPosition({x:501, y:500, direction: 'down', moving: false}); user2.setPosition({ x: 501, y: 500, direction: "down", moving: false });
expect(enterTriggered).toBe(false); expect(enterTriggered).toBe(false);
expect(moveTriggered).toBe(true); expect(moveTriggered).toBe(true);
moveTriggered = false; moveTriggered = false;
// Move out of the zone in a zone that we don't track // Move out of the zone in a zone that we don't track
user2.setPosition({x: 901, y: 500, direction: 'down', moving: false}); user2.setPosition({ x: 901, y: 500, direction: "down", moving: false });
expect(enterTriggered).toBe(false); expect(enterTriggered).toBe(false);
expect(moveTriggered).toBe(false); expect(moveTriggered).toBe(false);
@ -69,7 +99,7 @@ describe("PositionNotifier", () => {
leaveTriggered = false; leaveTriggered = false;
// Move back in // Move back in
user2.setPosition({x: 500, y: 500, direction: 'down', moving: false}); user2.setPosition({ x: 500, y: 500, direction: "down", moving: false });
expect(enterTriggered).toBe(true); expect(enterTriggered).toBe(true);
expect(moveTriggered).toBe(false); expect(moveTriggered).toBe(false);
expect(leaveTriggered).toBe(false); expect(leaveTriggered).toBe(false);
@ -89,28 +119,59 @@ describe("PositionNotifier", () => {
let moveTriggered = false; let moveTriggered = false;
let leaveTriggered = false; let leaveTriggered = false;
const positionNotifier = new PositionNotifier(300, 300, (thing: Movable, fromZone: Zone|null ) => { const positionNotifier = new PositionNotifier(
enterTriggered = true; 300,
}, (thing: Movable, position: PositionInterface) => { 300,
moveTriggered = true; (thing: Movable, fromZone: Zone | null) => {
}, (thing: Movable) => { enterTriggered = true;
leaveTriggered = true; },
}, () => {}, (thing: Movable, position: PositionInterface) => {
() => {}); moveTriggered = true;
},
(thing: Movable) => {
leaveTriggered = true;
},
() => {},
() => {}
);
const user1 = new User(1, 'test', '10.0.0.2', { const user1 = new User(
x: 500, 1,
y: 500, "test",
moving: false, "10.0.0.2",
direction: 'down' {
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []); x: 500,
y: 500,
moving: false,
direction: "down",
},
false,
positionNotifier,
{} as UserSocket,
[],
null,
"foo",
[]
);
const user2 = new User(2, 'test', '10.0.0.2', { const user2 = new User(
x: 0, 2,
y: 0, "test",
moving: false, "10.0.0.2",
direction: 'down' {
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []); x: 0,
y: 0,
moving: false,
direction: "down",
},
false,
positionNotifier,
{} as UserSocket,
[],
null,
"foo",
[]
);
const listener = {} as ZoneSocket; const listener = {} as ZoneSocket;
positionNotifier.addZoneListener(listener, 0, 0); positionNotifier.addZoneListener(listener, 0, 0);
@ -126,14 +187,12 @@ describe("PositionNotifier", () => {
positionNotifier.enter(user1); positionNotifier.enter(user1);
positionNotifier.enter(user2); positionNotifier.enter(user2);
//expect(newUsers.length).toBe(2); //expect(newUsers.length).toBe(2);
expect(enterTriggered).toBe(true); expect(enterTriggered).toBe(true);
enterTriggered = false; enterTriggered = false;
//positionNotifier.updatePosition(user2, {x:500, y:500}, {x:0, y: 0}) //positionNotifier.updatePosition(user2, {x:500, y:500}, {x:0, y: 0})
user2.setPosition({x: 500, y: 500, direction: 'down', moving: false}); user2.setPosition({ x: 500, y: 500, direction: "down", moving: false });
expect(enterTriggered).toBe(true); expect(enterTriggered).toBe(true);
expect(moveTriggered).toBe(false); expect(moveTriggered).toBe(false);
@ -184,4 +243,4 @@ describe("PositionNotifier", () => {
enterTriggered = false; enterTriggered = false;
//expect(newUsers.length).toBe(2); //expect(newUsers.length).toBe(2);
}); });
}) });

View file

@ -935,9 +935,9 @@ flatted@^3.1.0:
integrity sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw== integrity sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==
follow-redirects@^1.14.0: follow-redirects@^1.14.0:
version "1.14.6" version "1.14.7"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.6.tgz#8cfb281bbc035b3c067d6cd975b0f6ade6e855cd" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685"
integrity sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A== integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==
fs-minipass@^2.0.0: fs-minipass@^2.0.0:
version "2.1.0" version "2.1.0"
@ -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

@ -73,7 +73,7 @@ services:
DEBUG: "socket:*" DEBUG: "socket:*"
STARTUP_COMMAND_1: yarn install STARTUP_COMMAND_1: yarn install
# wait for files generated by "messages" container to exists # wait for files generated by "messages" container to exists
STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done STARTUP_COMMAND_2: sleep 5; while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
SECRET_KEY: yourSecretKey SECRET_KEY: yourSecretKey
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
@ -132,7 +132,7 @@ services:
DEBUG: "*" DEBUG: "*"
STARTUP_COMMAND_1: yarn install STARTUP_COMMAND_1: yarn install
# wait for files generated by "messages" container to exists # wait for files generated by "messages" container to exists
STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done STARTUP_COMMAND_2: sleep 5; while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
SECRET_KEY: yourSecretKey SECRET_KEY: yourSecretKey
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
ALLOW_ARTILLERY: "true" ALLOW_ARTILLERY: "true"

View file

@ -0,0 +1,76 @@
# How to translate WorkAdventure
We use the [typesafe-i18n](https://github.com/ivanhofer/typesafe-i18n) package to handle the translation.
## Add a new language
It is very easy to add a new language!
First, in the `front/src/i18n` folder create a new folder with the language code as name (the language code according to [RFC 5646](https://datatracker.ietf.org/doc/html/rfc5646)).
In the previously created folder, add a file named index.ts with the following content containing your language information (french from France in this example):
```ts
import type { Translation } from "../i18n-types";
const fr_FR: Translation = {
...en_US,
language: "Français",
country: "France",
};
export default fr_FR;
```
## Add a new key
### Add a simple key
The keys are searched by a path through the properties of the sub-objects and it is therefore advisable to write your translation as a JavaScript object.
Please use kamelcase to name your keys!
Example:
```ts
{
messages: {
coffeMachine: {
start: "Coffe machine has been started!";
}
}
}
```
In the code you can translate using `$LL`:
```ts
import LL from "../../i18n/i18n-svelte";
console.log($LL.messages.coffeMachine.start());
```
### Add a key with parameters
You can also use parameters to make the translation dynamic.
Use the tag { [parameter name] } to apply your parameters in the translations
Example:
```ts
{
messages: {
coffeMachine: {
playerStart: "{ playerName } started the coffee machine!";
}
}
}
```
In the code you can use it like this:
```ts
$LL.messages.coffeMachine.playerStart.start({
playerName: "John",
});
```

View file

@ -1,6 +1,32 @@
{.section-title.accent.text-primary} {.section-title.accent.text-primary}
# API Camera functions Reference # API Camera functions Reference
### Start following player
```javascript
WA.camera.followPlayer(smooth: boolean): void
```
Set camera to follow the player. Set `smooth` to true for smooth transition.
### Set spot for camera to look at
```javascript
WA.camera.set(
x: number,
y: number,
width?: number,
height?: number,
lock: boolean = false,
smooth: boolean = false,
): void
```
Set camera to look at given spot.
Setting `width` and `height` will adjust zoom.
Set `lock` to true to lock camera in this position.
Set `smooth` to true for smooth transition.
### Listen to camera updates ### Listen to camera updates
``` ```
@ -21,4 +47,4 @@ Example :
```javascript ```javascript
const subscription = WA.camera.onCameraUpdate().subscribe((worldView) => console.log(worldView)); const subscription = WA.camera.onCameraUpdate().subscribe((worldView) => console.log(worldView));
//later... //later...
subscription.unsubscribe(); subscription.unsubscribe();

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

@ -36,6 +36,23 @@ WA.onInit().then(() => {
}) })
``` ```
### Get the player language
```
WA.player.language: string;
```
The current language of player is available from the `WA.player.language` property.
{.alert.alert-info}
You need to wait for the end of the initialization before accessing `WA.player.language`
```typescript
WA.onInit().then(() => {
console.log('Player language: ', WA.player.language);
})
```
### Get the tags of the player ### Get the tags of the player
``` ```
@ -58,6 +75,27 @@ WA.onInit().then(() => {
}) })
``` ```
### Get the position of the player
```
WA.player.getPosition(): Promise<Position>
```
The player's current position is available using the `WA.player.getPosition()` function.
`Position` has the following attributes :
* **x (number) :** The coordinate x of the current player's position.
* **y (number) :** The coordinate y of the current player's position.
{.alert.alert-info}
You need to wait for the end of the initialization before calling `WA.player.getPosition()`
```typescript
WA.onInit().then(() => {
console.log('Position: ', WA.player.getPosition());
})
```
### Get the user-room token of the player ### Get the user-room token of the player
``` ```
@ -152,6 +190,37 @@ Example:
WA.player.state.toto //will retrieve the variable WA.player.state.toto //will retrieve the variable
``` ```
### Move player to position
```typescript
WA.player.moveTo(x: number, y: number, speed?: number): Promise<{ x: number, y: number, cancelled: boolean }>;
```
Player will try to find shortest path to the destination point and proceed to move there.
```typescript
// Let's move player to x: 250 y: 250 with speed of 10
WA.player.moveTo(250, 250, 10);
```
You can also chain movement like this:
```typescript
// Player will move to the next point after reaching first one
await WA.player.moveTo(250, 250, 10);
await WA.player.moveTo(500, 0, 10);
```
Or like this:
```typescript
// Player will move to the next point after reaching first one or stop if the movement was cancelled
WA.player.moveTo(250, 250, 10).then((result) => {
if (!result.cancelled) {
WA.player.moveTo(500, 0, 10);
}
});
```
It is possible to get the information about current player's position on stop and if the movement was interrupted
```typescript
// Result will store x and y of Player at the moment of movement's end and information if the movement was interrupted
const result = await WA.player.moveTo(250, 250, 10);
// result: { x: number, y: number, cancelled: boolean }
```
### Set the outline color of the player ### Set the outline color of the player
``` ```
WA.player.setOutlineColor(red: number, green: number, blue: number): Promise<void>; WA.player.setOutlineColor(red: number, green: number, blue: number): Promise<void>;

View file

@ -8,6 +8,7 @@ If you use group layers in your map, to reference a layer in a group you will ne
together. together.
Example : Example :
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<img src="images/groupLayer.png" class="figure-img img-fluid rounded" alt="" /> <img src="images/groupLayer.png" class="figure-img img-fluid rounded" alt="" />
@ -16,10 +17,10 @@ Example :
The name of the layers of this map are : The name of the layers of this map are :
* `entries/start` - `entries/start`
* `bottom/ground/under` - `bottom/ground/under`
* `bottom/build/carpet` - `bottom/build/carpet`
* `wall` - `wall`
### Detecting when the user enters/leaves a layer ### Detecting when the user enters/leaves a layer
@ -30,17 +31,18 @@ WA.room.onLeaveLayer(name: string): Subscription
Listens to the position of the current user. The event is triggered when the user enters or leaves a given layer. Listens to the position of the current user. The event is triggered when the user enters or leaves a given layer.
* **name**: the name of the layer who as defined in Tiled. - **name**: the name of the layer who as defined in Tiled.
Example: Example:
```javascript ```ts
WA.room.onEnterLayer('myLayer').subscribe(() => { const myLayerSubscriber = WA.room.onEnterLayer("myLayer").subscribe(() => {
WA.chat.sendChatMessage("Hello!", 'Mr Robot'); WA.chat.sendChatMessage("Hello!", "Mr Robot");
}); });
WA.room.onLeaveLayer('myLayer').subscribe(() => { WA.room.onLeaveLayer("myLayer").subscribe(() => {
WA.chat.sendChatMessage("Goodbye!", 'Mr Robot'); WA.chat.sendChatMessage("Goodbye!", "Mr Robot");
myLayerSubscriber.unsubscribe();
}); });
``` ```
@ -56,10 +58,10 @@ layer in that group layer.
Example : Example :
```javascript ```ts
WA.room.showLayer('bottom'); WA.room.showLayer("bottom");
//... //...
WA.room.hideLayer('bottom'); WA.room.hideLayer("bottom");
``` ```
### Set/Create properties in a layer ### Set/Create properties in a layer
@ -76,8 +78,8 @@ To unset a property from a layer, use `setProperty` with `propertyValue` set to
Example : Example :
```javascript ```ts
WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); WA.room.setProperty("wikiLayer", "openWebsite", "https://www.wikipedia.org/");
``` ```
### Get the room id ### Get the room id
@ -92,9 +94,9 @@ The ID of the current room is available from the `WA.room.id` property.
```typescript ```typescript
WA.onInit().then(() => { WA.onInit().then(() => {
console.log('Room id: ', WA.room.id); console.log("Room id: ", WA.room.id);
// Will output something like: 'https://play.workadventu.re/@/myorg/myworld/myroom', or 'https://play.workadventu.re/_/global/mymap.org/map.json" // Will output something like: 'https://play.workadventu.re/@/myorg/myworld/myroom', or 'https://play.workadventu.re/_/global/mymap.org/map.json"
}) });
``` ```
### Get the map URL ### Get the map URL
@ -109,9 +111,9 @@ The URL of the map is available from the `WA.room.mapURL` property.
```typescript ```typescript
WA.onInit().then(() => { WA.onInit().then(() => {
console.log('Map URL: ', WA.room.mapURL); console.log("Map URL: ", WA.room.mapURL);
// Will output something like: 'https://mymap.org/map.json" // Will output something like: 'https://mymap.org/map.json"
}) });
``` ```
### Getting map data ### Getting map data
@ -122,7 +124,7 @@ WA.room.getTiledMap(): Promise<ITiledMap>
Returns a promise that resolves to the JSON map file. Returns a promise that resolves to the JSON map file.
```javascript ```ts
const map = await WA.room.getTiledMap(); const map = await WA.room.getTiledMap();
console.log("Map generated with Tiled version ", map.tiledversion); console.log("Map generated with Tiled version ", map.tiledversion);
``` ```
@ -140,6 +142,7 @@ WA.room.setTiles(tiles: TileDescriptor[]): void
Replace the tile at the `x` and `y` coordinates in the layer named `layer` by the tile with the id `tile`. Replace the tile at the `x` and `y` coordinates in the layer named `layer` by the tile with the id `tile`.
If `tile` is a string, it's not the id of the tile but the value of the property `name`. If `tile` is a string, it's not the id of the tile but the value of the property `name`.
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<img src="images/nameIndexProperty.png" class="figure-img img-fluid rounded" alt="" /> <img src="images/nameIndexProperty.png" class="figure-img img-fluid rounded" alt="" />
@ -148,10 +151,10 @@ If `tile` is a string, it's not the id of the tile but the value of the property
`TileDescriptor` has the following attributes : `TileDescriptor` has the following attributes :
* **x (number) :** The coordinate x of the tile that you want to replace. - **x (number) :** The coordinate x of the tile that you want to replace.
* **y (number) :** The coordinate y of the tile that you want to replace. - **y (number) :** The coordinate y of the tile that you want to replace.
* **tile (number | string) :** The id of the tile that will be placed in the map. - **tile (number | string) :** The id of the tile that will be placed in the map.
* **layer (string) :** The name of the layer where the tile will be placed. - **layer (string) :** The name of the layer where the tile will be placed.
**Important !** : If you use `tile` as a number, be sure to add the `firstgid` of the tileset of the tile that you want **Important !** : If you use `tile` as a number, be sure to add the `firstgid` of the tileset of the tile that you want
to the id of the tile in Tiled Editor. to the id of the tile in Tiled Editor.
@ -160,12 +163,12 @@ Note: If you want to unset a tile, use `setTiles` with `tile` set to `null`.
Example : Example :
```javascript ```ts
WA.room.setTiles([ WA.room.setTiles([
{ x: 6, y: 4, tile: 'blue', layer: 'setTiles' }, { x: 6, y: 4, tile: "blue", layer: "setTiles" },
{ x: 7, y: 4, tile: 109, layer: 'setTiles' }, { x: 7, y: 4, tile: 109, layer: "setTiles" },
{ x: 8, y: 4, tile: 109, layer: 'setTiles' }, { x: 8, y: 4, tile: 109, layer: "setTiles" },
{ x: 9, y: 4, tile: 'blue', layer: 'setTiles' } { x: 9, y: 4, tile: "blue", layer: "setTiles" },
]); ]);
``` ```
@ -179,10 +182,10 @@ Load a tileset in JSON format from an url and return the id of the first tile of
You can create a tileset file in Tile Editor. You can create a tileset file in Tile Editor.
```javascript ```ts
WA.room.loadTileset("Assets/Tileset.json").then((firstId) => { WA.room.loadTileset("Assets/Tileset.json").then((firstId) => {
WA.room.setTiles([{ x: 4, y: 4, tile: firstId, layer: 'bottom' }]); WA.room.setTiles([{ x: 4, y: 4, tile: firstId, layer: "bottom" }]);
}) });
``` ```
## Embedding websites in a map ## Embedding websites in a map
@ -199,10 +202,10 @@ WA.room.website.get(objectName: string): Promise<EmbeddedWebsite>
You can get an instance of an embedded website by using the `WA.room.website.get()` method. It returns a promise of You can get an instance of an embedded website by using the `WA.room.website.get()` method. It returns a promise of
an `EmbeddedWebsite` instance. an `EmbeddedWebsite` instance.
```javascript ```ts
// Get an existing website object where 'my_website' is the name of the object (on any layer object of the map) // Get an existing website object where 'my_website' is the name of the object (on any layer object of the map)
const website = await WA.room.website.get('my_website'); const website = await WA.room.website.get("my_website");
website.url = 'https://example.com'; website.url = "https://example.com";
website.visible = true; website.visible = true;
``` ```
@ -231,7 +234,7 @@ interface CreateEmbeddedWebsiteEvent {
You can create an instance of an embedded website by using the `WA.room.website.create()` method. It returns You can create an instance of an embedded website by using the `WA.room.website.create()` method. It returns
an `EmbeddedWebsite` instance. an `EmbeddedWebsite` instance.
```javascript ```ts
// Create a new website object // Create a new website object
const website = WA.room.website.create({ const website = WA.room.website.create({
name: "my_website", name: "my_website",
@ -269,10 +272,10 @@ class EmbeddedWebsite {
visible: boolean; visible: boolean;
allow: string; allow: string;
allowApi: boolean; allowApi: boolean;
x: number; // In "game" pixels, relative to the map or player coordinates, depending on origin x: number; // In "game" pixels, relative to the map or player coordinates, depending on origin
y: number; // In "game" pixels, relative to the map or player coordinates, depending on origin y: number; // In "game" pixels, relative to the map or player coordinates, depending on origin
width: number; // In "game" pixels width: number; // In "game" pixels
height: number; // In "game" pixels height: number; // In "game" pixels
origin: "player" | "map"; origin: "player" | "map";
scale: number; scale: number;
} }
@ -282,4 +285,3 @@ When you modify a property of an `EmbeddedWebsite` instance, the iframe is autom
{.alert.alert-warning} The websites you add/edit/delete via the scripting API are only shown locally. If you want them {.alert.alert-warning} The websites you add/edit/delete via the scripting API are only shown locally. If you want them
to be displayed for every player, you can use [variables](api-start.md) to share a common state between all users. to be displayed for every player, you can use [variables](api-start.md) to share a common state between all users.

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

@ -8,7 +8,7 @@ module.exports = {
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking" "plugin:@typescript-eslint/recommended-requiring-type-checking",
], ],
"globals": { "globals": {
"Atomics": "readonly", "Atomics": "readonly",
@ -23,7 +23,7 @@ module.exports = {
}, },
"plugins": [ "plugins": [
"@typescript-eslint", "@typescript-eslint",
"svelte3" "svelte3",
], ],
"overrides": [ "overrides": [
{ {
@ -33,6 +33,7 @@ module.exports = {
], ],
"rules": { "rules": {
"no-unused-vars": "off", "no-unused-vars": "off",
"eol-last": ["error", "always"],
"@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-explicit-any": "error",
"no-throw-literal": "error", "no-throw-literal": "error",
// TODO: remove those ignored rules and write a stronger code! // TODO: remove those ignored rules and write a stronger code!

1
front/.gitignore vendored
View file

@ -6,6 +6,7 @@
/dist/main.*.css.map /dist/main.*.css.map
/dist/tests/ /dist/tests/
/yarn-error.log /yarn-error.log
/package-lock.json
/dist/webpack.config.js /dist/webpack.config.js
/dist/webpack.config.js.map /dist/webpack.config.js.map
/dist/src /dist/src

View file

@ -1,2 +1,5 @@
src/Messages/generated src/Messages/generated
src/Messages/JsonMessages src/Messages/JsonMessages
src/i18n/i18n-svelte.ts
src/i18n/i18n-types.ts
src/i18n/i18n-util.ts

View file

@ -0,0 +1,5 @@
{
"$schema": "https://unpkg.com/typesafe-i18n@2.59.0/schema/typesafe-i18n.json",
"baseLocale": "en-US",
"adapter": "svelte"
}

View file

@ -1,23 +1,35 @@
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 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

@ -0,0 +1 @@
*.json

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",
@ -46,8 +47,10 @@
"@types/simple-peer": "^9.11.1", "@types/simple-peer": "^9.11.1",
"@types/socket.io-client": "^1.4.32", "@types/socket.io-client": "^1.4.32",
"axios": "^0.21.2", "axios": "^0.21.2",
"cancelable-promise": "^4.2.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"deep-copy-ts": "^0.5.0", "deep-copy-ts": "^0.5.0",
"easystarjs": "^0.4.4",
"generic-type-guard": "^3.2.0", "generic-type-guard": "^3.2.0",
"google-protobuf": "^3.13.0", "google-protobuf": "^3.13.0",
"phaser": "^3.54.0", "phaser": "^3.54.0",
@ -63,10 +66,12 @@
"socket.io-client": "^2.3.0", "socket.io-client": "^2.3.0",
"standardized-audio-context": "^25.2.4", "standardized-audio-context": "^25.2.4",
"ts-proto": "^1.96.0", "ts-proto": "^1.96.0",
"uuidv4": "^6.2.10" "typesafe-i18n": "^2.59.0",
"uuidv4": "^6.2.10",
"zod": "^3.11.6"
}, },
"scripts": { "scripts": {
"start": "run-p templater serve svelte-check-watch", "start": "run-p templater serve svelte-check-watch typesafe-i18n",
"templater": "cross-env ./templater.sh", "templater": "cross-env ./templater.sh",
"serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open", "serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open",
"build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack", "build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack",
@ -78,7 +83,8 @@
"svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\" --watch", "svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\" --watch",
"svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\"", "svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\"",
"pretty": "yarn prettier --write 'src/**/*.{ts,svelte}'", "pretty": "yarn prettier --write 'src/**/*.{ts,svelte}'",
"pretty-check": "yarn prettier --check 'src/**/*.{ts,svelte}'" "pretty-check": "yarn prettier --check 'src/**/*.{ts,svelte}'",
"typesafe-i18n": "typesafe-i18n --no-watch"
}, },
"lint-staged": { "lint-staged": {
"*.svelte": [ "*.svelte": [

View file

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isCameraFollowPlayerEvent = new tg.IsInterface()
.withProperties({
smooth: tg.isBoolean,
})
.get();
/**
* A message sent from the iFrame to the game to make the camera follow player.
*/
export type CameraFollowPlayerEvent = tg.GuardedType<typeof isCameraFollowPlayerEvent>;

View file

@ -0,0 +1,16 @@
import * as tg from "generic-type-guard";
export const isCameraSetEvent = new tg.IsInterface()
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
width: tg.isOptional(tg.isNumber),
height: tg.isOptional(tg.isNumber),
lock: tg.isBoolean,
smooth: tg.isBoolean,
})
.get();
/**
* A message sent from the iFrame to the game to change the camera position.
*/
export type CameraSetEvent = tg.GuardedType<typeof isCameraSetEvent>;

View file

@ -5,12 +5,13 @@ export const isGameStateEvent = new tg.IsInterface()
roomId: tg.isString, roomId: tg.isString,
mapUrl: tg.isString, mapUrl: tg.isString,
nickname: tg.isString, nickname: tg.isString,
language: tg.isUnion(tg.isString, tg.isUndefined),
uuid: tg.isUnion(tg.isString, tg.isUndefined), uuid: tg.isUnion(tg.isString, tg.isUndefined),
startLayerName: tg.isUnion(tg.isString, tg.isNull), startLayerName: tg.isUnion(tg.isString, tg.isNull),
tags: tg.isArray(tg.isString), tags: tg.isArray(tg.isString),
variables: tg.isObject, variables: tg.isObject,
userRoomToken: tg.isUnion(tg.isString, tg.isUndefined),
playerVariables: tg.isObject, playerVariables: tg.isObject,
userRoomToken: tg.isUnion(tg.isString, tg.isUndefined),
}) })
.get(); .get();
/** /**

View file

@ -28,10 +28,14 @@ import type { MessageReferenceEvent } from "./ui/TriggerActionMessageEvent";
import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent"; import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent";
import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEvent"; import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEvent";
import type { ChangeLayerEvent } from "./ChangeLayerEvent"; import type { ChangeLayerEvent } from "./ChangeLayerEvent";
import type { ChangeZoneEvent } from "./ChangeZoneEvent";
import { isColorEvent } from "./ColorEvent";
import { isPlayerPosition } from "./PlayerPosition"; import { isPlayerPosition } from "./PlayerPosition";
import type { WasCameraUpdatedEvent } from "./WasCameraUpdatedEvent"; import type { WasCameraUpdatedEvent } from "./WasCameraUpdatedEvent";
import type { ChangeZoneEvent } from "./ChangeZoneEvent";
import type { CameraSetEvent } from "./CameraSetEvent";
import type { CameraFollowPlayerEvent } from "./CameraFollowPlayerEvent";
import { isColorEvent } from "./ColorEvent";
import { isMovePlayerToEventConfig } from "./MovePlayerToEvent";
import { isMovePlayerToEventAnswer } from "./MovePlayerToEventAnswer";
export interface TypedMessageEvent<T> extends MessageEvent { export interface TypedMessageEvent<T> extends MessageEvent {
data: T; data: T;
@ -43,6 +47,8 @@ export interface TypedMessageEvent<T> extends MessageEvent {
export type IframeEventMap = { export type IframeEventMap = {
loadPage: LoadPageEvent; loadPage: LoadPageEvent;
chat: ChatEvent; chat: ChatEvent;
cameraFollowPlayer: CameraFollowPlayerEvent;
cameraSet: CameraSetEvent;
openPopup: OpenPopupEvent; openPopup: OpenPopupEvent;
closePopup: ClosePopupEvent; closePopup: ClosePopupEvent;
openTab: OpenTabEvent; openTab: OpenTabEvent;
@ -169,6 +175,10 @@ export const iframeQueryMapTypeGuards = {
query: tg.isUndefined, query: tg.isUndefined,
answer: isPlayerPosition, answer: isPlayerPosition,
}, },
movePlayerTo: {
query: isMovePlayerToEventConfig,
answer: isMovePlayerToEventAnswer,
},
}; };
type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never; type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never;

View file

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isMovePlayerToEventConfig = new tg.IsInterface()
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
speed: tg.isOptional(tg.isNumber),
})
.get();
export type MovePlayerToEvent = tg.GuardedType<typeof isMovePlayerToEventConfig>;

View file

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isMovePlayerToEventAnswer = new tg.IsInterface()
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
cancelled: tg.isBoolean,
})
.get();
export type MovePlayerToEventAnswer = tg.GuardedType<typeof isMovePlayerToEventAnswer>;

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,
@ -33,6 +32,8 @@ import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Store
import type { ChangeLayerEvent } from "./Events/ChangeLayerEvent"; import type { ChangeLayerEvent } from "./Events/ChangeLayerEvent";
import type { WasCameraUpdatedEvent } from "./Events/WasCameraUpdatedEvent"; import type { WasCameraUpdatedEvent } from "./Events/WasCameraUpdatedEvent";
import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent"; import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent";
import { CameraSetEvent, isCameraSetEvent } from "./Events/CameraSetEvent";
import { CameraFollowPlayerEvent, isCameraFollowPlayerEvent } from "./Events/CameraFollowPlayerEvent";
type AnswererCallback<T extends keyof IframeQueryMap> = ( type AnswererCallback<T extends keyof IframeQueryMap> = (
query: IframeQueryMap[T]["query"], query: IframeQueryMap[T]["query"],
@ -56,6 +57,12 @@ class IframeListener {
private readonly _disablePlayerControlStream: Subject<void> = new Subject(); private readonly _disablePlayerControlStream: Subject<void> = new Subject();
public readonly disablePlayerControlStream = this._disablePlayerControlStream.asObservable(); public readonly disablePlayerControlStream = this._disablePlayerControlStream.asObservable();
private readonly _cameraSetStream: Subject<CameraSetEvent> = new Subject();
public readonly cameraSetStream = this._cameraSetStream.asObservable();
private readonly _cameraFollowPlayerStream: Subject<CameraFollowPlayerEvent> = new Subject();
public readonly cameraFollowPlayerStream = this._cameraFollowPlayerStream.asObservable();
private readonly _enablePlayerControlStream: Subject<void> = new Subject(); private readonly _enablePlayerControlStream: Subject<void> = new Subject();
public readonly enablePlayerControlStream = this._enablePlayerControlStream.asObservable(); public readonly enablePlayerControlStream = this._enablePlayerControlStream.asObservable();
@ -202,6 +209,10 @@ class IframeListener {
this._hideLayerStream.next(payload.data); this._hideLayerStream.next(payload.data);
} else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) { } else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) {
this._setPropertyStream.next(payload.data); this._setPropertyStream.next(payload.data);
} else if (payload.type === "cameraSet" && isCameraSetEvent(payload.data)) {
this._cameraSetStream.next(payload.data);
} else if (payload.type === "cameraFollowPlayer" && isCameraFollowPlayerEvent(payload.data)) {
this._cameraFollowPlayerStream.next(payload.data);
} else if (payload.type === "chat" && isChatEvent(payload.data)) { } else if (payload.type === "chat" && isChatEvent(payload.data)) {
scriptUtils.sendAnonymousChat(payload.data); scriptUtils.sendAnonymousChat(payload.data);
} else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) { } else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) {

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

@ -17,6 +17,27 @@ export class WorkAdventureCameraCommands extends IframeApiContribution<WorkAdven
}), }),
]; ];
public set(
x: number,
y: number,
width?: number,
height?: number,
lock: boolean = false,
smooth: boolean = false
): void {
sendToWorkadventure({
type: "cameraSet",
data: { x, y, width, height, lock, smooth },
});
}
public followPlayer(smooth: boolean = false): void {
sendToWorkadventure({
type: "cameraFollowPlayer",
data: { smooth },
});
}
onCameraUpdate(): Subject<WasCameraUpdatedEvent> { onCameraUpdate(): Subject<WasCameraUpdatedEvent> {
sendToWorkadventure({ sendToWorkadventure({
type: "onCameraUpdate", type: "onCameraUpdate",

View file

@ -1,4 +1,3 @@
import type { ChatEvent } from "../Events/ChatEvent";
import { isUserInputChatEvent, UserInputChatEvent } from "../Events/UserInputChatEvent"; import { isUserInputChatEvent, UserInputChatEvent } from "../Events/UserInputChatEvent";
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution"; import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks"; import { apiCallback } from "./registeredCallbacks";

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

@ -13,6 +13,12 @@ export const setPlayerName = (name: string) => {
playerName = name; playerName = name;
}; };
let playerLanguage: string | undefined;
export const setPlayerLanguage = (language: string | undefined) => {
playerLanguage = language;
};
let tags: string[] | undefined; let tags: string[] | undefined;
export const setTags = (_tags: string[]) => { export const setTags = (_tags: string[]) => {
@ -61,6 +67,15 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
return playerName; return playerName;
} }
get language(): string {
if (playerLanguage === undefined) {
throw new Error(
"Player language not initialized yet. You should call WA.player.language within a WA.onInit callback."
);
}
return playerLanguage;
}
get tags(): string[] { get tags(): string[] {
if (tags === undefined) { if (tags === undefined) {
throw new Error("Tags not initialized yet. You should call WA.player.tags within a WA.onInit callback."); throw new Error("Tags not initialized yet. You should call WA.player.tags within a WA.onInit callback.");
@ -84,6 +99,13 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
}); });
} }
public async moveTo(x: number, y: number, speed?: number): Promise<{ x: number; y: number; cancelled: boolean }> {
return await queryWorkadventure({
type: "movePlayerTo",
data: { x, y, speed },
});
}
get userRoomToken(): string | undefined { get userRoomToken(): string | undefined {
if (userRoomToken === undefined) { if (userRoomToken === undefined) {
throw new Error( throw new Error(

View file

@ -1,166 +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";
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 $errorStore.length > 0}
<div>
<ErrorDialog />
</div>
{/if}
{#if $chatVisibilityStore} {#if $chatVisibilityStore}
<Chat /> <Chat />
{/if} {/if}
{#if $warningContainerStore} {/if}
<WarningContainer />
{/if}
</div>

View file

@ -5,6 +5,7 @@
import { get } from "svelte/store"; import { get } from "svelte/store";
import type { Unsubscriber } from "svelte/store"; import type { Unsubscriber } from "svelte/store";
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
import LL from "../../i18n/i18n-svelte";
let HTMLAudioPlayer: HTMLAudioElement; let HTMLAudioPlayer: HTMLAudioElement;
let audioPlayerVolumeIcon: HTMLElement; let audioPlayerVolumeIcon: HTMLElement;
@ -144,7 +145,7 @@
</div> </div>
<div class="audio-manager-reduce-conversation"> <div class="audio-manager-reduce-conversation">
<label> <label>
reduce in conversations {$LL.audio.manager.reduce()}
<input type="checkbox" bind:checked={decreaseWhileTalking} on:change={setDecrease} /> <input type="checkbox" bind:checked={decreaseWhileTalking} on:change={setDecrease} />
</label> </label>
<section class="audio-manager-file"> <section class="audio-manager-file">
@ -156,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

@ -5,6 +5,7 @@
import ChatElement from "./ChatElement.svelte"; import ChatElement from "./ChatElement.svelte";
import { afterUpdate, beforeUpdate, onMount } from "svelte"; import { afterUpdate, beforeUpdate, onMount } from "svelte";
import { HtmlUtils } from "../../WebRtc/HtmlUtils"; import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import LL from "../../i18n/i18n-svelte";
let listDom: HTMLElement; let listDom: HTMLElement;
let chatWindowElement: HTMLElement; let chatWindowElement: HTMLElement;
@ -42,10 +43,10 @@
<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">Here is your chat history:</p></li> <li><p class="system-text">{$LL.chat.intro()}</p></li>
{#each $chatMessagesStore as message, i} {#each $chatMessagesStore as message, i}
<li><ChatElement {message} line={i} /></li> <li><ChatElement {message} line={i} /></li>
{/each} {/each}
@ -77,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

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import LL from "../../i18n/i18n-svelte";
import { chatMessagesStore, chatInputFocusStore } from "../../Stores/ChatStore"; import { chatMessagesStore, chatInputFocusStore } from "../../Stores/ChatStore";
export const handleForm = { export const handleForm = {
@ -27,7 +28,7 @@
<input <input
type="text" type="text"
bind:value={newMessageText} bind:value={newMessageText}
placeholder="Enter your message..." placeholder={$LL.chat.enter()}
on:focus={onFocus} on:focus={onFocus}
on:blur={onBlur} on:blur={onBlur}
bind:this={inputElement} bind:this={inputElement}

View file

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import LL from "../../i18n/i18n-svelte";
import type { PlayerInterface } from "../../Phaser/Game/PlayerInterface"; import type { PlayerInterface } from "../../Phaser/Game/PlayerInterface";
import { requestVisitCardsStore } from "../../Stores/GameStore"; import { requestVisitCardsStore } from "../../Stores/GameStore";
@ -12,8 +13,12 @@
</script> </script>
<ul class="selectMenu" style="border-top: {player.color || 'whitesmoke'} 5px solid"> <ul class="selectMenu" style="border-top: {player.color || 'whitesmoke'} 5px solid">
<li><button class="text-btn" disabled={!player.visitCardUrl} on:click={openVisitCard}>Visit card</button></li> <li>
<li><button class="text-btn" disabled>Add friend</button></li> <button class="text-btn" disabled={!player.visitCardUrl} on:click={openVisitCard}
>{$LL.chat.menu.visitCard()}</button
>
</li>
<li><button class="text-btn" disabled>{$LL.chat.menu.addFriend}</button></li>
</ul> </ul>
<style lang="scss"> <style lang="scss">

View file

@ -2,6 +2,7 @@
import type { Game } from "../../Phaser/Game/Game"; import type { Game } from "../../Phaser/Game/Game";
import { CustomizeScene, CustomizeSceneName } from "../../Phaser/Login/CustomizeScene"; import { CustomizeScene, CustomizeSceneName } from "../../Phaser/Login/CustomizeScene";
import { activeRowStore } from "../../Stores/CustomCharacterStore"; import { activeRowStore } from "../../Stores/CustomCharacterStore";
import LL from "../../i18n/i18n-svelte";
export let game: Game; export let game: Game;
@ -34,7 +35,7 @@
<form class="customCharacterScene"> <form class="customCharacterScene">
<section class="text-center"> <section class="text-center">
<h2>Customize your WOKA</h2> <h2>{$LL.woka.customWoka.title()}</h2>
</section> </section>
<section class="action action-move"> <section class="action action-move">
<button <button
@ -53,32 +54,37 @@
<section class="action"> <section class="action">
{#if $activeRowStore === 0} {#if $activeRowStore === 0}
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={previousScene} <button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={previousScene}
>Return</button >{$LL.woka.customWoka.navigation.return()}</button
> >
{/if} {/if}
{#if $activeRowStore !== 0} {#if $activeRowStore !== 0}
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={selectUp} <button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={selectUp}
>Back <img src="resources/objects/arrow_up_black.png" alt="" /></button >{$LL.woka.customWoka.navigation.back()}
<img src="resources/objects/arrow_up_black.png" alt="" /></button
> >
{/if} {/if}
{#if $activeRowStore === 5} {#if $activeRowStore === 5}
<button <button
type="submit" type="submit"
class="customCharacterSceneFormSubmit nes-btn is-primary" class="customCharacterSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={finish}>Finish</button on:click|preventDefault={finish}>{$LL.woka.customWoka.navigation.finish()}</button
> >
{/if} {/if}
{#if $activeRowStore !== 5} {#if $activeRowStore !== 5}
<button <button
type="submit" type="submit"
class="customCharacterSceneFormSubmit nes-btn is-primary" class="customCharacterSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={selectDown}>Next <img src="resources/objects/arrow_down.png" alt="" /></button on:click|preventDefault={selectDown}
>{$LL.woka.customWoka.navigation.next()}
<img src="resources/objects/arrow_down.png" alt="" /></button
> >
{/if} {/if}
</section> </section>
</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;
@ -125,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,313 @@
<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 = coWebsite.altMessage ?? 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;
}
&:not(.vertical) {
animation: bounce 0.35s ease 6 alternate;
}
&.vertical {
margin: 7px;
&::before {
width: 48px;
height: 48px;
}
.cowebsite-icon {
width: 40px;
height: 40px;
}
animation: shake 0.35s ease-in-out;
}
&.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);
}
}
@keyframes bounce {
from {
transform: translateY(0);
}
to {
transform: translateY(-15px);
}
}
@keyframes shake {
0% {
transform: translateX(0);
}
20% {
transform: translateX(-10px);
}
40% {
transform: translateX(10px);
}
60% {
transform: translateX(-10px);
}
80% {
transform: translateX(10px);
}
100% {
transform: translateX(0);
}
}
.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

@ -13,6 +13,7 @@
import cinemaCloseImg from "../images/cinema-close.svg"; import cinemaCloseImg from "../images/cinema-close.svg";
import cinemaImg from "../images/cinema.svg"; import cinemaImg from "../images/cinema.svg";
import microphoneImg from "../images/microphone.svg"; import microphoneImg from "../images/microphone.svg";
import LL from "../../i18n/i18n-svelte";
export let game: Game; export let game: Game;
let selectedCamera: string | undefined = undefined; let selectedCamera: string | undefined = undefined;
@ -76,7 +77,7 @@
<form class="enableCameraScene" on:submit|preventDefault={submit}> <form class="enableCameraScene" on:submit|preventDefault={submit}>
<section class="text-center"> <section class="text-center">
<h2>Turn on your camera and microphone</h2> <h2>{$LL.camera.enable.title()}</h2>
</section> </section>
{#if $localStreamStore.type === "success" && $localStreamStore.stream} {#if $localStreamStore.type === "success" && $localStreamStore.stream}
<video class="myCamVideoSetup" use:srcObject={$localStreamStore.stream} autoplay muted playsinline /> <video class="myCamVideoSetup" use:srcObject={$localStreamStore.stream} autoplay muted playsinline />
@ -121,11 +122,13 @@
{/if} {/if}
</section> </section>
<section class="action"> <section class="action">
<button type="submit" class="nes-btn is-primary letsgo">Let's go!</button> <button type="submit" class="nes-btn is-primary letsgo">{$LL.camera.enable.start()}</button>
</section> </section>
</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;
@ -213,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,20 +1,13 @@
<!--
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";
const gameScene = gameManager.getCurrentGameScene(); const gameScene = gameManager.getCurrentGameScene();
function name(userId: number): string | undefined { function name(userId: number): string {
return gameScene.MapPlayersByKey.get(userId)?.PlayerValue; const user = gameScene.MapPlayersByKey.get(userId);
} return user ? user.PlayerValue : "";
function sendFollowRequest() {
gameScene.CurrentPlayer.sendFollowRequest();
} }
function acceptFollowRequest() { function acceptFollowRequest() {
@ -42,11 +35,15 @@ vim: ft=typescript
{#if $followStateStore === "requesting" && $followRoleStore === "follower"} {#if $followStateStore === "requesting" && $followRoleStore === "follower"}
<div class="interact-menu nes-container is-rounded"> <div class="interact-menu nes-container is-rounded">
<section class="interact-menu-title"> <section class="interact-menu-title">
<h2>Do you want to follow {name($followUsersStore[0])}?</h2> <h2>{$LL.follow.interactMenu.title.follow({ leader: name($followUsersStore[0]) })}</h2>
</section> </section>
<section class="interact-menu-action"> <section class="interact-menu-action">
<button type="button" class="nes-btn is-success" on:click|preventDefault={acceptFollowRequest}>Yes</button> <button type="button" class="nes-btn is-success" on:click|preventDefault={acceptFollowRequest}
<button type="button" class="nes-btn is-error" on:click|preventDefault={reset}>No</button> >{$LL.follow.interactMenu.yes()}</button
>
<button type="button" class="nes-btn is-error" on:click|preventDefault={reset}
>{$LL.follow.interactMenu.no()}</button
>
</section> </section>
</div> </div>
{/if} {/if}
@ -54,88 +51,77 @@ vim: ft=typescript
{#if $followStateStore === "ending"} {#if $followStateStore === "ending"}
<div class="interact-menu nes-container is-rounded"> <div class="interact-menu nes-container is-rounded">
<section class="interact-menu-title"> <section class="interact-menu-title">
<h2>Interaction</h2> <h2>{$LL.follow.interactMenu.title.interact()}</h2>
</section> </section>
{#if $followRoleStore === "follower"} {#if $followRoleStore === "follower"}
<section class="interact-menu-question"> <section class="interact-menu-question">
<p>Do you want to stop following {name($followUsersStore[0])}?</p> <p>{$LL.follow.interactMenu.stop.follower({ leader: name($followUsersStore[0]) })}</p>
</section> </section>
{:else if $followRoleStore === "leader"} {:else if $followRoleStore === "leader"}
<section class="interact-menu-question"> <section class="interact-menu-question">
<p>Do you want to stop leading the way?</p> <p>{$LL.follow.interactMenu.stop.leader()}</p>
</section> </section>
{/if} {/if}
<section class="interact-menu-action"> <section class="interact-menu-action">
<button type="button" class="nes-btn is-success" on:click|preventDefault={reset}>Yes</button> <button type="button" class="nes-btn is-success" on:click|preventDefault={reset}
<button type="button" class="nes-btn is-error" on:click|preventDefault={abortEnding}>No</button> >{$LL.follow.interactMenu.yes()}</button
>
<button type="button" class="nes-btn is-error" on:click|preventDefault={abortEnding}
>{$LL.follow.interactMenu.no()}</button
>
</section> </section>
</div> </div>
{/if} {/if}
{#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>Following {name($followUsersStore[0])}</p> <p>{$LL.follow.interactStatus.following({ leader: name($followUsersStore[0]) })}</p>
{:else if $followUsersStore.length === 0} {:else if $followUsersStore.length === 0}
<p>Waiting for followers' confirmation</p> <p>{$LL.follow.interactStatus.waitingFollowers()}</p>
{:else if $followUsersStore.length === 1} {:else if $followUsersStore.length === 1}
<p>{name($followUsersStore[0])} is following you</p> <p>{$LL.follow.interactStatus.followed.one({ follower: name($followUsersStore[0]) })}</p>
{:else if $followUsersStore.length === 2} {:else if $followUsersStore.length === 2}
<p>{name($followUsersStore[0])} and {name($followUsersStore[1])} are following you</p> <p>
{$LL.follow.interactStatus.followed.two({
firstFollower: name($followUsersStore[0]),
secondFollower: name($followUsersStore[1]),
})}
</p>
{:else} {:else}
<p> <p>
{$followUsersStore.slice(0, -1).map(name).join(", ")} and {name( {$LL.follow.interactStatus.followed.many({
$followUsersStore[$followUsersStore.length - 1] followers: $followUsersStore.slice(0, -1).map(name).join(", "),
)} are following you lastFollower: name($followUsersStore[$followUsersStore.length - 1]),
})}
</p> </p>
{/if} {/if}
</section> </section>
</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 {
@ -143,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;
@ -174,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

@ -4,6 +4,7 @@
import firefoxImg from "./images/help-setting-camera-permission-firefox.png"; import firefoxImg from "./images/help-setting-camera-permission-firefox.png";
import chromeImg from "./images/help-setting-camera-permission-chrome.png"; import chromeImg from "./images/help-setting-camera-permission-chrome.png";
import { getNavigatorType, isAndroid as isAndroidFct, NavigatorType } from "../../WebRtc/DeviceUtils"; import { getNavigatorType, isAndroid as isAndroidFct, NavigatorType } from "../../WebRtc/DeviceUtils";
import LL from "../../i18n/i18n-svelte";
let isAndroid = isAndroidFct(); let isAndroid = isAndroidFct();
let isFirefox = getNavigatorType() === NavigatorType.firefox; let isFirefox = getNavigatorType() === NavigatorType.firefox;
@ -21,17 +22,16 @@
<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>Camera / Microphone access needed</h2> <h2>{$LL.camera.help.title()}</h2>
<p class="err">Permission denied</p> <p class="err">{$LL.camera.help.permissionDenied()}</p>
<p>You must allow camera and microphone access in your browser.</p> <p>{$LL.camera.help.content()}</p>
<p> <p>
{#if isFirefox} {#if isFirefox}
<p class="err"> <p class="err">
Please click the "Remember this decision" checkbox, if you don't want Firefox to keep asking you the {$LL.camera.help.firefoxContent()}
authorization.
</p> </p>
<img src={firefoxImg} alt="" /> <img src={firefoxImg} alt="" />
{:else if isChrome && !isAndroid} {:else if isChrome && !isAndroid}
@ -40,9 +40,11 @@
</p> </p>
</section> </section>
<section> <section>
<button class="helpCameraSettingsFormRefresh nes-btn" on:click|preventDefault={refresh}>Refresh</button> <button class="helpCameraSettingsFormRefresh nes-btn" on:click|preventDefault={refresh}
>{$LL.camera.help.refresh()}</button
>
<button type="submit" class="helpCameraSettingsFormContinue nes-btn is-primary" on:click|preventDefault={close} <button type="submit" class="helpCameraSettingsFormContinue nes-btn is-primary" on:click|preventDefault={close}
>Continue without webcam</button >{$LL.camera.help.continue()}</button
> >
</section> </section>
</form> </form>
@ -53,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

@ -4,6 +4,7 @@
import { DISPLAY_TERMS_OF_USE, MAX_USERNAME_LENGTH } from "../../Enum/EnvironmentVariable"; import { DISPLAY_TERMS_OF_USE, MAX_USERNAME_LENGTH } from "../../Enum/EnvironmentVariable";
import logoImg from "../images/logo.png"; import logoImg from "../images/logo.png";
import { gameManager } from "../../Phaser/Game/GameManager"; import { gameManager } from "../../Phaser/Game/GameManager";
import LL from "../../i18n/i18n-svelte";
export let game: Game; export let game: Game;
@ -27,7 +28,7 @@
<img src={logoImg} alt="WorkAdventure logo" /> <img src={logoImg} alt="WorkAdventure logo" />
</section> </section>
<section class="text-center"> <section class="text-center">
<h2>Enter your name</h2> <h2>{$LL.login.input.name.placeholder()}</h2>
</section> </section>
<!-- svelte-ignore a11y-autofocus --> <!-- svelte-ignore a11y-autofocus -->
<input <input
@ -44,22 +45,20 @@
/> />
<section class="error-section"> <section class="error-section">
{#if name.trim() === "" && startValidating} {#if name.trim() === "" && startValidating}
<p class="err">The name is empty</p> <p class="err">{$LL.login.input.name.empty()}</p>
{/if} {/if}
</section> </section>
{#if DISPLAY_TERMS_OF_USE} {#if DISPLAY_TERMS_OF_USE}
<section class="terms-and-conditions"> <section class="terms-and-conditions">
<a style="display: none;" href="traduction">Need for traduction</a>
<p> <p>
By continuing, you are agreeing our <a href="https://workadventu.re/terms-of-use" target="_blank" {$LL.login.terms()}
>terms of use</a
>, <a href="https://workadventu.re/privacy-policy" target="_blank">privacy policy</a> and
<a href="https://workadventu.re/cookie-policy" target="_blank">cookie policy</a>.
</p> </p>
</section> </section>
{/if} {/if}
<section class="action"> <section class="action">
<button type="submit" class="nes-btn is-primary loginSceneFormSubmit">Continue</button> <button type="submit" class="nes-btn is-primary loginSceneFormSubmit">{$LL.login.continue()}</button>
</section> </section>
</form> </form>

View file

@ -0,0 +1,170 @@
<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";
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 $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

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { gameManager } from "../../Phaser/Game/GameManager"; import { gameManager } from "../../Phaser/Game/GameManager";
import { onMount } from "svelte"; import { onMount } from "svelte";
import LL from "../../i18n/i18n-svelte";
let gameScene = gameManager.getCurrentGameScene(); let gameScene = gameManager.getCurrentGameScene();
@ -11,7 +12,7 @@
let mapName: string = ""; let mapName: string = "";
let mapLink: string = ""; let mapLink: string = "";
let mapDescription: string = ""; let mapDescription: string = "";
let mapCopyright: string = "The map creator did not declare a copyright for the map."; let mapCopyright: string = $LL.menu.about.copyrights.map.empty();
let tilesetCopyright: string[] = []; let tilesetCopyright: string[] = [];
let audioCopyright: string[] = []; let audioCopyright: string[] = [];
@ -62,47 +63,45 @@
</script> </script>
<div class="about-room-main"> <div class="about-room-main">
<h2>Information on the map</h2> <h2>{$LL.menu.about.mapInfo()}</h2>
<section class="container-overflow"> <section class="container-overflow">
<h3>{mapName}</h3> <h3>{mapName}</h3>
<p class="string-HTML">{mapDescription}</p> <p class="string-HTML">{mapDescription}</p>
{#if mapLink} {#if mapLink}
<p class="string-HTML">&gt; <a href={mapLink} target="_blank">link to this map</a> &lt;</p> <p class="string-HTML">
&gt; <a href={mapLink} target="_blank">{$LL.menu.about.mapLink()}</a> &lt;
</p>
{/if} {/if}
<h3 class="nes-pointer hoverable" on:click={() => (expandedMapCopyright = !expandedMapCopyright)}> <h3 class="nes-pointer hoverable" on:click={() => (expandedMapCopyright = !expandedMapCopyright)}>
Copyrights of the map {$LL.menu.about.copyrights.map.title()}
</h3> </h3>
<p class="string-HTML" hidden={!expandedMapCopyright}>{mapCopyright}</p> <p class="string-HTML" hidden={!expandedMapCopyright}>{mapCopyright}</p>
<h3 class="nes-pointer hoverable" on:click={() => (expandedTilesetCopyright = !expandedTilesetCopyright)}> <h3 class="nes-pointer hoverable" on:click={() => (expandedTilesetCopyright = !expandedTilesetCopyright)}>
Copyrights of the tilesets {$LL.menu.about.copyrights.tileset.title()}
</h3> </h3>
<section hidden={!expandedTilesetCopyright}> <section hidden={!expandedTilesetCopyright}>
{#each tilesetCopyright as copyright} {#each tilesetCopyright as copyright}
<p class="string-HTML">{copyright}</p> <p class="string-HTML">{copyright}</p>
{:else} {:else}
<p> <p>{$LL.menu.about.copyrights.tileset.empty()}</p>
The map creator did not declare a copyright for the tilesets. This doesn't mean that those tilesets
have no license.
</p>
{/each} {/each}
</section> </section>
<h3 class="nes-pointer hoverable" on:click={() => (expandedAudioCopyright = !expandedAudioCopyright)}> <h3 class="nes-pointer hoverable" on:click={() => (expandedAudioCopyright = !expandedAudioCopyright)}>
Copyrights of audio files {$LL.menu.about.copyrights.audio.title()}
</h3> </h3>
<section hidden={!expandedAudioCopyright}> <section hidden={!expandedAudioCopyright}>
{#each audioCopyright as copyright} {#each audioCopyright as copyright}
<p class="string-HTML">{copyright}</p> <p class="string-HTML">{copyright}</p>
{:else} {:else}
<p> <p>{$LL.menu.about.copyrights.audio.empty()}</p>
The map creator did not declare a copyright for audio files. This doesn't mean that those tilesets
have no license.
</p>
{/each} {/each}
</section> </section>
</section> </section>
</div> </div>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
.string-HTML { .string-HTML {
white-space: pre-line; white-space: pre-line;
} }
@ -129,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

@ -4,6 +4,7 @@
import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService"; import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService";
import uploadFile from "../images/music-file.svg"; import uploadFile from "../images/music-file.svg";
import type { PlayGlobalMessageInterface } from "../../Connexion/ConnexionModels"; import type { PlayGlobalMessageInterface } from "../../Connexion/ConnexionModels";
import LL from "../../i18n/i18n-svelte";
interface EventTargetFiles extends EventTarget { interface EventTargetFiles extends EventTarget {
files: Array<File>; files: Array<File>;
@ -76,7 +77,7 @@
<img <img
class="nes-pointer" class="nes-pointer"
src={uploadFile} src={uploadFile}
alt="Upload a file" alt={$LL.menu.globalAudio.uploadInfo()}
on:click|preventDefault={() => { on:click|preventDefault={() => {
fileInput.click(); fileInput.click();
}} }}
@ -85,7 +86,7 @@
<p>{fileName} : {fileSize}</p> <p>{fileName} : {fileSize}</p>
{/if} {/if}
{#if errorFile} {#if errorFile}
<p class="err">No file selected. You need to upload a file before sending it.</p> <p class="err">{$LL.menu.globalAudio.error()}</p>
{/if} {/if}
<input <input
type="file" type="file"

View file

@ -1,4 +1,7 @@
<script lang="ts"> <script lang="ts">
import LL from "../../i18n/i18n-svelte";
import { contactPageStore } from "../../Stores/MenuStore";
function goToGettingStarted() { function goToGettingStarted() {
const sparkHost = "https://workadventu.re/getting-started"; const sparkHost = "https://workadventu.re/getting-started";
window.open(sparkHost, "_blank"); window.open(sparkHost, "_blank");
@ -8,25 +11,24 @@
const sparkHost = "https://workadventu.re/map-building/"; const sparkHost = "https://workadventu.re/map-building/";
window.open(sparkHost, "_blank"); window.open(sparkHost, "_blank");
} }
import { contactPageStore } from "../../Stores/MenuStore";
</script> </script>
<div class="create-map-main"> <div class="create-map-main">
<section class="container-overflow"> <section class="container-overflow">
<section> <section>
<h3>Getting started</h3> <h3>{$LL.menu.contact.gettingStarted.title()}</h3>
<p> <p>{$LL.menu.contact.gettingStarted.description()}</p>
WorkAdventure allows you to create an online space to communicate spontaneously with others. And it all <button type="button" class="nes-btn is-primary" on:click={goToGettingStarted}
starts with creating your own space. Choose from a large selection of prefabricated maps by our team. >{$LL.menu.contact.gettingStarted.title()}</button
</p> >
<button type="button" class="nes-btn is-primary" on:click={goToGettingStarted}>Getting started</button>
</section> </section>
<section> <section>
<h3>Create your map</h3> <h3>{$LL.menu.contact.createMap.title()}</h3>
<p>You can also create your own custom map by following the step of the documentation.</p> <p>{$LL.menu.contact.createMap.description()}</p>
<button type="button" class="nes-btn" on:click={goToBuildingMap}>Create your map</button> <button type="button" class="nes-btn" on:click={goToBuildingMap}
>{$LL.menu.contact.createMap.title()}</button
>
</section> </section>
<iframe <iframe

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import TextGlobalMessage from "./TextGlobalMessage.svelte"; import TextGlobalMessage from "./TextGlobalMessage.svelte";
import AudioGlobalMessage from "./AudioGlobalMessage.svelte"; import AudioGlobalMessage from "./AudioGlobalMessage.svelte";
import LL from "../../i18n/i18n-svelte";
let handleSendText: { sendTextMessage(broadcast: boolean): void }; let handleSendText: { sendTextMessage(broadcast: boolean): void };
let handleSendAudio: { sendAudioMessage(broadcast: boolean): Promise<void> }; let handleSendAudio: { sendAudioMessage(broadcast: boolean): Promise<void> };
@ -35,14 +36,14 @@
<button <button
type="button" type="button"
class="nes-btn {inputSendTextActive ? 'is-disabled' : ''}" class="nes-btn {inputSendTextActive ? 'is-disabled' : ''}"
on:click|preventDefault={activateInputText}>Text</button on:click|preventDefault={activateInputText}>{$LL.menu.globalMessage.text()}</button
> >
</section> </section>
<section> <section>
<button <button
type="button" type="button"
class="nes-btn {uploadAudioActive ? 'is-disabled' : ''}" class="nes-btn {uploadAudioActive ? 'is-disabled' : ''}"
on:click|preventDefault={activateUploadAudio}>Audio</button on:click|preventDefault={activateUploadAudio}>{$LL.menu.globalMessage.audio()}</button
> >
</section> </section>
</div> </div>
@ -57,15 +58,17 @@
<div class="global-message-footer"> <div class="global-message-footer">
<label> <label>
<input type="checkbox" class="nes-checkbox is-dark nes-pointer" bind:checked={broadcastToWorld} /> <input type="checkbox" class="nes-checkbox is-dark nes-pointer" bind:checked={broadcastToWorld} />
<span>Broadcast to all rooms of the world</span> <span>{$LL.menu.globalMessage.warning()}</span>
</label> </label>
<section> <section>
<button class="nes-btn is-primary" on:click|preventDefault={send}>Send</button> <button class="nes-btn is-primary" on:click|preventDefault={send}>{$LL.menu.globalMessage.send()}</button>
</section> </section>
</div> </div>
</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;
@ -108,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

@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import LL from "../../i18n/i18n-svelte";
function copyLink() { function copyLink() {
const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement; const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement;
input.focus(); input.focus();
@ -21,19 +23,21 @@
<div class="guest-main"> <div class="guest-main">
<section class="container-overflow"> <section class="container-overflow">
<section class="share-url not-mobile"> <section class="share-url not-mobile">
<h3>Share the link of the room!</h3> <h3>{$LL.menu.invite.description()}</h3>
<input type="text" readonly id="input-share-link" value={location.toString()} /> <input type="text" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={copyLink}>Copy</button> <button type="button" class="nes-btn is-primary" on:click={copyLink}>{$LL.menu.invite.copy()}</button>
</section> </section>
<section class="is-mobile"> <section class="is-mobile">
<h3>Share the link of the room!</h3> <h3>{$LL.menu.invite.description()}</h3>
<input type="hidden" readonly id="input-share-link" value={location.toString()} /> <input type="hidden" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={shareLink}>Share</button> <button type="button" class="nes-btn is-primary" on:click={shareLink}>{$LL.menu.invite.share()}</button>
</section> </section>
</section> </section>
</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);
@ -55,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

@ -14,26 +14,27 @@
SubMenusInterface, SubMenusInterface,
subMenusStore, subMenusStore,
} from "../../Stores/MenuStore"; } from "../../Stores/MenuStore";
import type { MenuItem } from "../../Stores/MenuStore";
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
import { get } from "svelte/store";
import type { Unsubscriber } from "svelte/store"; import type { Unsubscriber } from "svelte/store";
import { sendMenuClickedEvent } from "../../Api/iframe/Ui/MenuItem"; import { sendMenuClickedEvent } from "../../Api/iframe/Ui/MenuItem";
import LL from "../../i18n/i18n-svelte";
let activeSubMenu: string = SubMenusInterface.profile; let activeSubMenu: MenuItem = $subMenusStore[0];
let activeComponent: typeof ProfileSubMenu | typeof CustomSubMenu = ProfileSubMenu; let activeComponent: typeof ProfileSubMenu | typeof CustomSubMenu = ProfileSubMenu;
let props: { url: string; allowApi: boolean }; let props: { url: string; allowApi: boolean };
let unsubscriberSubMenuStore: Unsubscriber; let unsubscriberSubMenuStore: Unsubscriber;
onMount(() => { onMount(() => {
unsubscriberSubMenuStore = subMenusStore.subscribe(() => { unsubscriberSubMenuStore = subMenusStore.subscribe(() => {
if (!get(subMenusStore).includes(activeSubMenu)) { if (!$subMenusStore.includes(activeSubMenu)) {
switchMenu(SubMenusInterface.profile); switchMenu($subMenusStore[0]);
} }
}); });
checkSubMenuToShow(); checkSubMenuToShow();
switchMenu(SubMenusInterface.profile); switchMenu($subMenusStore[0]);
}); });
onDestroy(() => { onDestroy(() => {
@ -42,10 +43,10 @@
} }
}); });
function switchMenu(menu: string) { function switchMenu(menu: MenuItem) {
if (get(subMenusStore).find((subMenu) => subMenu === menu)) { if (menu.type === "translated") {
activeSubMenu = menu; activeSubMenu = menu;
switch (menu) { switch (menu.key) {
case SubMenusInterface.settings: case SubMenusInterface.settings:
activeComponent = SettingsSubMenu; activeComponent = SettingsSubMenu;
break; break;
@ -64,36 +65,46 @@
case SubMenusInterface.contact: case SubMenusInterface.contact:
activeComponent = ContactSubMenu; activeComponent = ContactSubMenu;
break; break;
default: {
const customMenu = customMenuIframe.get(menu);
if (customMenu !== undefined) {
props = { url: customMenu.url, allowApi: customMenu.allowApi };
activeComponent = CustomSubMenu;
} else {
sendMenuClickedEvent(menu);
menuVisiblilityStore.set(false);
}
break;
}
} }
} else throw new Error("There is no menu called " + menu); } else {
const customMenu = customMenuIframe.get(menu.label);
if (customMenu !== undefined) {
props = { url: customMenu.url, allowApi: customMenu.allowApi };
activeComponent = CustomSubMenu;
} else {
sendMenuClickedEvent(menu.label);
menuVisiblilityStore.set(false);
}
}
} }
function closeMenu() { function closeMenu() {
menuVisiblilityStore.set(false); menuVisiblilityStore.set(false);
} }
function onKeyDown(e: KeyboardEvent) { function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") { if (e.key === "Escape") {
closeMenu(); closeMenu();
} }
} }
function translateMenuName(menu: MenuItem) {
if (menu.type === "scripting") {
return menu.label;
}
// Bypass the proxy of typesafe for getting the menu name : https://github.com/ivanhofer/typesafe-i18n/issues/156
const getMenuName = $LL.menu.sub[menu.key];
return getMenuName();
}
</script> </script>
<svelte:window on:keydown={onKeyDown} /> <svelte:window on:keydown={onKeyDown} />
<div class="menu-container-main"> <div class="menu-container-main">
<div class="menu-nav-sidebar nes-container is-rounded" transition:fly={{ x: -1000, duration: 500 }}> <div class="menu-nav-sidebar nes-container is-rounded" transition:fly={{ x: -1000, duration: 500 }}>
<h2>Menu</h2> <h2>{$LL.menu.title()}</h2>
<nav> <nav>
{#each $subMenusStore as submenu} {#each $subMenusStore as submenu}
<button <button
@ -101,19 +112,21 @@
class="nes-btn {activeSubMenu === submenu ? 'is-disabled' : ''}" class="nes-btn {activeSubMenu === submenu ? 'is-disabled' : ''}"
on:click|preventDefault={() => switchMenu(submenu)} on:click|preventDefault={() => switchMenu(submenu)}
> >
{submenu} {translateMenuName(submenu)}
</button> </button>
{/each} {/each}
</nav> </nav>
</div> </div>
<div class="menu-submenu-container nes-container is-rounded" transition:fly={{ y: -1000, duration: 500 }}> <div class="menu-submenu-container nes-container is-rounded" transition:fly={{ y: -1000, duration: 500 }}>
<button type="button" class="nes-btn is-error close" on:click={closeMenu}>&times</button> <button type="button" class="nes-btn is-error close" on:click={closeMenu}>&times</button>
<h2>{activeSubMenu}</h2> <h2>{translateMenuName(activeSubMenu)}</h2>
<svelte:component this={activeComponent} {...props} /> <svelte:component this={activeComponent} {...props} />
</div> </div>
</div> </div>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss";
.nes-container { .nes-container {
padding: 5px; padding: 5px;
} }
@ -125,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));
@ -162,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

@ -9,10 +9,12 @@
import { get } from "svelte/store"; import { get } from "svelte/store";
import { ADMIN_URL } from "../../Enum/EnvironmentVariable"; import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
import { showShareLinkMapModalStore } from "../../Stores/ModalStore"; import { showShareLinkMapModalStore } from "../../Stores/ModalStore";
import LL from "../../i18n/i18n-svelte";
function showMenu() { function showMenu() {
menuVisiblilityStore.set(!get(menuVisiblilityStore)); menuVisiblilityStore.set(!get(menuVisiblilityStore));
} }
function showChat() { function showChat() {
chatVisibilityStore.set(true); chatVisibilityStore.set(true);
} }
@ -20,62 +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 src={logoInvite} alt="open menu" class="nes-pointer" on:click|preventDefault={showInvite} /> <img
<img src={logoRegister} alt="open menu" class="nes-pointer" on:click|preventDefault={register} /> src={logoInvite}
alt={$LL.menu.icon.open.invite()}
class="nes-pointer"
draggable="false"
on:dragstart|preventDefault={noDrag}
on:click|preventDefault={showInvite}
/>
<img
src={logoRegister}
alt={$LL.menu.icon.open.register()}
class="nes-pointer"
draggable="false"
on:dragstart|preventDefault={noDrag}
on:click|preventDefault={register}
/>
{:else} {:else}
<img src={logoWA} alt="open menu" class="nes-pointer" on:click|preventDefault={showMenu} /> <img
<img src={logoTalk} alt="open menu" 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

@ -17,6 +17,7 @@
import btnProfileSubMenuCompanion from "../images/btn-menu-profile-companion.svg"; import btnProfileSubMenuCompanion from "../images/btn-menu-profile-companion.svg";
import Woka from "../Woka/Woka.svelte"; import Woka from "../Woka/Woka.svelte";
import Companion from "../Companion/Companion.svelte"; import Companion from "../Companion/Companion.svelte";
import LL from "../../i18n/i18n-svelte";
function disableMenuStores() { function disableMenuStores() {
menuVisiblilityStore.set(false); menuVisiblilityStore.set(false);
@ -62,20 +63,20 @@
<div class="submenu"> <div class="submenu">
<section> <section>
<button type="button" class="nes-btn" on:click|preventDefault={openEditNameScene}> <button type="button" class="nes-btn" on:click|preventDefault={openEditNameScene}>
<img src={btnProfileSubMenuIdentity} alt="Edit your name" /> <img src={btnProfileSubMenuIdentity} alt={$LL.menu.profile.edit.name()} />
<span class="btn-hover">Edit your name</span> <span class="btn-hover">{$LL.menu.profile.edit.name()}</span>
</button> </button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditSkinScene}> <button type="button" class="nes-btn" on:click|preventDefault={openEditSkinScene}>
<Woka userId={-1} placeholderSrc="" width="26px" height="26px" /> <Woka userId={-1} placeholderSrc="" width="26px" height="26px" />
<span class="btn-hover">Edit your WOKA</span> <span class="btn-hover">{$LL.menu.profile.edit.woka()}</span>
</button> </button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}> <button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}>
<Companion userId={-1} placeholderSrc={btnProfileSubMenuCompanion} width="26px" height="26px" /> <Companion userId={-1} placeholderSrc={btnProfileSubMenuCompanion} width="26px" height="26px" />
<span class="btn-hover">Edit your companion</span> <span class="btn-hover">{$LL.menu.profile.edit.companion()}</span>
</button> </button>
<button type="button" class="nes-btn" on:click|preventDefault={openEnableCameraScene}> <button type="button" class="nes-btn" on:click|preventDefault={openEnableCameraScene}>
<img src={btnProfileSubMenuCamera} alt="Edit your camera" /> <img src={btnProfileSubMenuCamera} alt={$LL.menu.profile.edit.camera()} />
<span class="btn-hover">Edit your camera</span> <span class="btn-hover">{$LL.menu.profile.edit.camera()}</span>
</button> </button>
</section> </section>
</div> </div>
@ -88,17 +89,21 @@
{/if} {/if}
</section> </section>
<section> <section>
<button type="button" class="nes-btn" on:click|preventDefault={logOut}>Log out</button> <button type="button" class="nes-btn" on:click|preventDefault={logOut}
>{$LL.menu.profile.logout()}</button
>
</section> </section>
{:else} {:else}
<section> <section>
<a type="button" class="nes-btn" href="/login">Sign in</a> <a type="button" class="nes-btn" href="/login">{$LL.menu.profile.login()}</a>
</section> </section>
{/if} {/if}
</div> </div>
</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;
@ -158,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,8 +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 type { Locales } from "../../i18n/i18n-types";
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";
@ -11,14 +14,17 @@
let ignoreFollowRequests: boolean = localUserStore.getIgnoreFollowRequests(); let ignoreFollowRequests: boolean = localUserStore.getIgnoreFollowRequests();
let valueGame: number = localUserStore.getGameQualityValue(); let valueGame: number = localUserStore.getGameQualityValue();
let valueVideo: number = localUserStore.getVideoQualityValue(); let valueVideo: number = localUserStore.getVideoQualityValue();
let valueLocale: string = $locale;
let previewValueGame = valueGame; let previewValueGame = valueGame;
let previewValueVideo = valueVideo; let previewValueVideo = valueVideo;
let previewValueLocale = valueLocale;
function saveSetting() { function saveSetting() {
if (valueGame !== previewValueGame) { let change = false;
previewValueGame = valueGame;
localUserStore.setGameQualityValue(valueGame); if (valueLocale !== previewValueLocale) {
window.location.reload(); previewValueLocale = valueLocale;
setCurrentLocale(valueLocale as Locales);
} }
if (valueVideo !== previewValueVideo) { if (valueVideo !== previewValueVideo) {
@ -26,6 +32,16 @@
videoConstraintStore.setFrameRate(valueVideo); videoConstraintStore.setFrameRate(valueVideo);
} }
if (valueGame !== previewValueGame) {
previewValueGame = valueGame;
localUserStore.setGameQualityValue(valueGame);
change = true;
}
if (change) {
window.location.reload();
}
closeMenu(); closeMenu();
} }
@ -69,38 +85,80 @@
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}>
<section> <section>
<h3>Game quality</h3> <h3>{$LL.menu.settings.gameQuality.title()}</h3>
<div class="nes-select is-dark"> <div class="nes-select is-dark">
<select bind:value={valueGame}> <select bind:value={valueGame}>
<option value={120}>{isMobile() ? "High (120 fps)" : "High video quality (120 fps)"}</option> <option value={120}
<option value={60} >{isMobile
>{isMobile() ? "Medium (60 fps)" : "Medium video quality (60 fps, recommended)"}</option ? $LL.menu.settings.gameQuality.short.high()
: $LL.menu.settings.gameQuality.long.high()}</option
>
<option value={60}
>{isMobile
? $LL.menu.settings.gameQuality.short.medium()
: $LL.menu.settings.gameQuality.long.medium()}</option
>
<option value={40}
>{isMobile
? $LL.menu.settings.gameQuality.short.small()
: $LL.menu.settings.gameQuality.long.small()}</option
>
<option value={20}
>{isMobile
? $LL.menu.settings.gameQuality.short.minimum()
: $LL.menu.settings.gameQuality.long.minimum()}</option
> >
<option value={40}>{isMobile() ? "Minimum (40 fps)" : "Minimum video quality (40 fps)"}</option>
<option value={20}>{isMobile() ? "Small (20 fps)" : "Small video quality (20 fps)"}</option>
</select> </select>
</div> </div>
</section> </section>
<section> <section>
<h3>Video quality</h3> <h3>{$LL.menu.settings.videoQuality.title()}</h3>
<div class="nes-select is-dark"> <div class="nes-select is-dark">
<select bind:value={valueVideo}> <select bind:value={valueVideo}>
<option value={30}>{isMobile() ? "High (30 fps)" : "High video quality (30 fps)"}</option> <option value={30}
<option value={20} >{isMobile
>{isMobile() ? "Medium (20 fps)" : "Medium video quality (20 fps, recommended)"}</option ? $LL.menu.settings.videoQuality.short.high()
: $LL.menu.settings.videoQuality.long.high()}</option
> >
<option value={10}>{isMobile() ? "Minimum (10 fps)" : "Minimum video quality (10 fps)"}</option> <option value={20}
<option value={5}>{isMobile() ? "Small (5 fps)" : "Small video quality (5 fps)"}</option> >{isMobile
? $LL.menu.settings.videoQuality.short.medium()
: $LL.menu.settings.videoQuality.long.medium()}</option
>
<option value={10}
>{isMobile
? $LL.menu.settings.videoQuality.short.small()
: $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>
</div>
</section>
<section>
<h3>{$LL.menu.settings.language.title()}</h3>
<div class="nes-select is-dark">
<select class="languages-switcher" bind:value={valueLocale}>
{#each displayableLocales as locale (locale.id)}
<option value={locale.id}>{`${locale.language} (${locale.country})`}</option>
{/each}
</select> </select>
</div> </div>
</section> </section>
<section class="settings-section-save"> <section class="settings-section-save">
<p>(Saving these settings will restart the game)</p> <p>{$LL.menu.settings.save.warning()}</p>
<button type="button" class="nes-btn is-primary" on:click|preventDefault={saveSetting}>Save</button> <button type="button" class="nes-btn is-primary" on:click|preventDefault={saveSetting}
>{$LL.menu.settings.save.button()}</button
>
</section> </section>
<section class="settings-section-noSaveOption"> <section class="settings-section-noSaveOption">
<label> <label>
@ -110,7 +168,7 @@
bind:checked={fullscreen} bind:checked={fullscreen}
on:change={changeFullscreen} on:change={changeFullscreen}
/> />
<span>Fullscreen</span> <span>{$LL.menu.settings.fullscreen()}</span>
</label> </label>
<label> <label>
<input <input
@ -119,7 +177,7 @@
bind:checked={notification} bind:checked={notification}
on:change={changeNotification} on:change={changeNotification}
/> />
<span>Notifications</span> <span>{$LL.menu.settings.notifications()}</span>
</label> </label>
<label> <label>
<input <input
@ -128,7 +186,7 @@
bind:checked={forceCowebsiteTrigger} bind:checked={forceCowebsiteTrigger}
on:change={changeForceCowebsiteTrigger} on:change={changeForceCowebsiteTrigger}
/> />
<span>Always ask before opening websites and Jitsi Meet rooms</span> <span>{$LL.menu.settings.cowebsiteTrigger()}</span>
</label> </label>
<label> <label>
<input <input
@ -137,12 +195,14 @@
bind:checked={ignoreFollowRequests} bind:checked={ignoreFollowRequests}
on:change={changeIgnoreFollowRequests} on:change={changeIgnoreFollowRequests}
/> />
<span>Ignore requests to follow other users</span> <span>{$LL.menu.settings.ignoreFollowRequest()}</span>
</label> </label>
</section> </section>
</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;
@ -174,9 +234,13 @@
margin: 0 0 15px; margin: 0 0 15px;
} }
} }
.languages-switcher option {
text-transform: capitalize;
}
} }
@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

@ -5,6 +5,7 @@
import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService"; import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService";
import type { Quill } from "quill"; import type { Quill } from "quill";
import type { PlayGlobalMessageInterface } from "../../Connexion/ConnexionModels"; import type { PlayGlobalMessageInterface } from "../../Connexion/ConnexionModels";
import LL from "../../i18n/i18n-svelte";
//toolbar //toolbar
const toolbarOptions = [ const toolbarOptions = [
@ -58,7 +59,7 @@
const { default: Quill } = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any const { default: Quill } = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any
quill = new Quill(QUILL_EDITOR, { quill = new Quill(QUILL_EDITOR, {
placeholder: "Enter your message here...", placeholder: $LL.menu.globalMessage.enter(),
theme: "snow", theme: "snow",
modules: { modules: {
toolbar: toolbarOptions, toolbar: toolbarOptions,

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,8 +2,9 @@
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";
let stream: MediaStream | null; let stream: MediaStream | null;
@ -22,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}>Silent zone</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

@ -2,6 +2,7 @@
import { blackListManager } from "../../WebRtc/BlackListManager"; import { blackListManager } from "../../WebRtc/BlackListManager";
import { showReportScreenStore, userReportEmpty } from "../../Stores/ShowReportScreenStore"; import { showReportScreenStore, userReportEmpty } from "../../Stores/ShowReportScreenStore";
import { onMount } from "svelte"; import { onMount } from "svelte";
import LL from "../../i18n/i18n-svelte";
export let userUUID: string | undefined; export let userUUID: string | undefined;
export let userName: string; export let userName: string;
@ -29,10 +30,10 @@
</script> </script>
<div class="block-container"> <div class="block-container">
<h3>Block</h3> <h3>{$LL.report.block.title()}</h3>
<p>Block any communication from and to {userName}. This can be reverted.</p> <p>{$LL.report.block.content({ userName })}</p>
<button type="button" class="nes-btn is-error" on:click|preventDefault={blockUser}> <button type="button" class="nes-btn is-error" on:click|preventDefault={blockUser}>
{userIsBlocked ? "Unblock this user" : "Block this user"} {userIsBlocked ? $LL.report.block.unblock() : $LL.report.block.block()}
</button> </button>
</div> </div>

View file

@ -7,6 +7,7 @@
import { playersStore } from "../../Stores/PlayersStore"; import { playersStore } from "../../Stores/PlayersStore";
import { connectionManager } from "../../Connexion/ConnectionManager"; import { connectionManager } from "../../Connexion/ConnectionManager";
import { get } from "svelte/store"; import { get } from "svelte/store";
import LL from "../../i18n/i18n-svelte";
let blockActive = true; let blockActive = true;
let reportActive = !blockActive; let reportActive = !blockActive;
@ -59,7 +60,7 @@
<div class="report-menu-main nes-container is-rounded"> <div class="report-menu-main nes-container is-rounded">
<section class="report-menu-title"> <section class="report-menu-title">
<h2>Moderate {userName}</h2> <h2>{$LL.report.moderate.title({ userName })}</h2>
<section class="justify-center"> <section class="justify-center">
<button type="button" class="nes-btn" on:click|preventDefault={close}>X</button> <button type="button" class="nes-btn" on:click|preventDefault={close}>X</button>
</section> </section>
@ -69,14 +70,14 @@
<button <button
type="button" type="button"
class="nes-btn {blockActive ? 'is-disabled' : ''}" class="nes-btn {blockActive ? 'is-disabled' : ''}"
on:click|preventDefault={activateBlock}>Block</button on:click|preventDefault={activateBlock}>{$LL.report.moderate.block()}</button
> >
</section> </section>
<section class="justify-center"> <section class="justify-center">
<button <button
type="button" type="button"
class="nes-btn {reportActive ? 'is-disabled' : ''}" class="nes-btn {reportActive ? 'is-disabled' : ''}"
on:click|preventDefault={activateReport}>Report</button on:click|preventDefault={activateReport}>{$LL.report.moderate.report()}</button
> >
</section> </section>
</section> </section>
@ -86,7 +87,7 @@
{:else if reportActive} {:else if reportActive}
<ReportSubMenu {userUUID} /> <ReportSubMenu {userUUID} />
{:else} {:else}
<p>ERROR : There is no action selected.</p> <p>{$LL.report.moderate.noSelect()}</p>
{/if} {/if}
</section> </section>
</div> </div>
@ -107,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;
@ -136,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

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { showReportScreenStore, userReportEmpty } from "../../Stores/ShowReportScreenStore"; import { showReportScreenStore, userReportEmpty } from "../../Stores/ShowReportScreenStore";
import { gameManager } from "../../Phaser/Game/GameManager"; import { gameManager } from "../../Phaser/Game/GameManager";
import LL from "../../i18n/i18n-svelte";
export let userUUID: string | undefined; export let userUUID: string | undefined;
let reportMessage: string; let reportMessage: string;
@ -22,18 +23,18 @@
</script> </script>
<div class="report-container-main"> <div class="report-container-main">
<h3>Report</h3> <h3>{$LL.report.title()}</h3>
<p>Send a report message to the administrators of this room. They may later ban this user.</p> <p>{$LL.report.content()}</p>
<form> <form>
<section> <section>
<label> <label>
<span>Your message: </span> <span>{$LL.report.message.title()}</span>
<textarea type="text" class="nes-textarea" bind:value={reportMessage} /> <textarea type="text" class="nes-textarea" bind:value={reportMessage} />
</label> </label>
<p hidden={hiddenError}>Report message cannot to be empty.</p> <p hidden={hiddenError}>{$LL.report.message.empty()}</p>
</section> </section>
<section> <section>
<button type="submit" class="nes-btn is-error" on:click={submitReport}>Report this user</button> <button type="submit" class="nes-btn is-error" on:click={submitReport}>{$LL.report.submit()}</button>
</section> </section>
</form> </form>
</div> </div>

View file

@ -1,4 +1,5 @@
<script lang="typescript"> <script lang="typescript">
import LL from "../../i18n/i18n-svelte";
import type { Game } from "../../Phaser/Game/Game"; import type { Game } from "../../Phaser/Game/Game";
import { SelectCompanionScene, SelectCompanionSceneName } from "../../Phaser/Login/SelectCompanionScene"; import { SelectCompanionScene, SelectCompanionSceneName } from "../../Phaser/Login/SelectCompanionScene";
@ -25,7 +26,7 @@
<form class="selectCompanionScene"> <form class="selectCompanionScene">
<section class="text-center"> <section class="text-center">
<h2>Select your companion</h2> <h2>{$LL.companion.select.title()}</h2>
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}> <button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}>
&lt; &lt;
</button> </button>
@ -35,17 +36,19 @@
</section> </section>
<section class="action"> <section class="action">
<button href="/" class="selectCompanionSceneFormBack nes-btn" on:click|preventDefault={noCompanion} <button href="/" class="selectCompanionSceneFormBack nes-btn" on:click|preventDefault={noCompanion}
>No companion</button >{$LL.companion.select.any()}</button
> >
<button <button
type="submit" type="submit"
class="selectCompanionSceneFormSubmit nes-btn is-primary" class="selectCompanionSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={selectCompanion}>Continue</button on:click|preventDefault={selectCompanion}>{$LL.companion.select.continue()}</button
> >
</section> </section>
</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;
@ -84,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,8 +1,10 @@
<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";
export let message: Message; export let message: Message;
@ -12,6 +14,8 @@
onMount(() => { onMount(() => {
timeToRead(); timeToRead();
const gameScene = gameManager.getCurrentGameScene();
gameScene.playSound("audio-report-message");
}); });
function timeToRead() { function timeToRead() {
@ -37,7 +41,8 @@
out:fade={{ duration: 200 }} out:fade={{ duration: 200 }}
> >
<h2 class="title-ban-message"> <h2 class="title-ban-message">
<img src="resources/logos/report.svg" alt="***" /> Important message <img src="resources/logos/report.svg" alt="***" />
{$LL.warning.importantMessage()}
<img src="resources/logos/report.svg" alt="***" /> <img src="resources/logos/report.svg" alt="***" />
</h2> </h2>
<div class="content-ban-message"> <div class="content-ban-message">
@ -51,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

@ -3,6 +3,7 @@
import megaphoneImg from "./images/megaphone.svg"; import megaphoneImg from "./images/megaphone.svg";
import { soundPlayingStore } from "../../Stores/SoundPlayingStore"; import { soundPlayingStore } from "../../Stores/SoundPlayingStore";
import { afterUpdate } from "svelte"; import { afterUpdate } from "svelte";
import LL from "../../i18n/i18n-svelte";
export let url: string; export let url: string;
let audio: HTMLAudioElement; let audio: HTMLAudioElement;
@ -18,7 +19,7 @@
<div class="audio-playing" transition:fly={{ x: 210, duration: 500 }}> <div class="audio-playing" transition:fly={{ x: 210, duration: 500 }}>
<img src={megaphoneImg} alt="Audio playing" /> <img src={megaphoneImg} alt="Audio playing" />
<p>Audio message</p> <p>{$LL.audio.message()}</p>
<audio bind:this={audio} src={url} on:ended={soundEnded}> <audio bind:this={audio} src={url} on:ended={soundEnded}>
<track kind="captions" /> <track kind="captions" />
</audio> </audio>
@ -36,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

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { errorStore, hasClosableMessagesInErrorStore } from "../../Stores/ErrorStore"; import { errorStore, hasClosableMessagesInErrorStore } from "../../Stores/ErrorStore";
import LL from "../../i18n/i18n-svelte";
function close(): boolean { function close(): boolean {
errorStore.clearClosableMessages(); errorStore.clearClosableMessages();
@ -8,7 +9,7 @@
</script> </script>
<div class="error-div nes-container is-dark is-rounded" open> <div class="error-div nes-container is-dark is-rounded" open>
<p class="nes-text is-error title">Error</p> <p class="nes-text is-error title">{$LL.error.error()}</p>
<div class="body"> <div class="body">
{#each $errorStore as error} {#each $errorStore as error}
<p>{error.message}</p> <p>{error.message}</p>
@ -24,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: peer 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: 200px;
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> <i class="container">
{:else} <span style="background-color: {getColorByString(name)};">{name}</span>
</i>
<!-- svelte-ignore a11y-media-has-caption --> <!-- svelte-ignore a11y-media-has-caption -->
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)} /> <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,37 @@
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}
on:click={() => (clickable ? highlightedEmbedScreen.toggleHighlight(embedScreen) : null)}
>
{#if $statusStore === "connecting"} {#if $statusStore === "connecting"}
<div class="connecting-spinner" /> <div class="connecting-spinner" />
{/if} {/if}
@ -29,43 +63,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

@ -2,6 +2,7 @@
import { fly } from "svelte/transition"; import { fly } from "svelte/transition";
import { requestVisitCardsStore } from "../../Stores/GameStore"; import { requestVisitCardsStore } from "../../Stores/GameStore";
import { onMount } from "svelte"; import { onMount } from "svelte";
import LL from "../../i18n/i18n-svelte";
export let visitCardUrl: string; export let visitCardUrl: string;
let w = "500px"; let w = "500px";
@ -40,7 +41,7 @@
/> />
{#if !hidden} {#if !hidden}
<div class="buttonContainer"> <div class="buttonContainer">
<button class="nes-btn is-popUpElement" on:click={closeCard}>Close</button> <button class="nes-btn is-popUpElement" on:click={closeCard}>{$LL.menu.visitCard.close()}</button>
</div> </div>
{/if} {/if}
</section> </section>
@ -56,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

@ -2,6 +2,7 @@
import { fly } from "svelte/transition"; import { fly } from "svelte/transition";
import { userIsAdminStore, limitMapStore } from "../../Stores/GameStore"; import { userIsAdminStore, limitMapStore } from "../../Stores/GameStore";
import { ADMIN_URL } from "../../Enum/EnvironmentVariable"; import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
import LL from "../../i18n/i18n-svelte";
const upgradeLink = ADMIN_URL + "/pricing"; const upgradeLink = ADMIN_URL + "/pricing";
const registerLink = ADMIN_URL + "/second-step-register"; const registerLink = ADMIN_URL + "/second-step-register";
@ -9,37 +10,45 @@
<main class="warningMain" transition:fly={{ y: -200, duration: 500 }}> <main class="warningMain" transition:fly={{ y: -200, duration: 500 }}>
{#if $userIsAdminStore} {#if $userIsAdminStore}
<h2>Warning!</h2> <h2>{$LL.warning.title()}</h2>
<p> <p>
This world is close to its limit!. You can upgrade its capacity <a href={upgradeLink} target="_blank" {$LL.warning.content({ upgradeLink })}
>here</a
>
</p> </p>
{:else if $limitMapStore} {:else if $limitMapStore}
<p> <p>
This map is available for 2 days. You can register your domain <a href={registerLink}>here</a>! This map is available for 2 days. You can register your domain <a href={registerLink}>here</a>!
</p> </p>
{:else} {:else}
<h2>Warning!</h2> <h2>{$LL.warning.title()}</h2>
<p>This world is close to its limit!</p> <p>{$LL.warning.limit()}</p>
{/if} {/if}
</main> </main>
<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

@ -1,6 +1,7 @@
<script lang="typescript"> <script lang="typescript">
import type { Game } from "../../Phaser/Game/Game"; import type { Game } from "../../Phaser/Game/Game";
import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene"; import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene";
import LL from "../../i18n/i18n-svelte";
export let game: Game; export let game: Game;
@ -25,7 +26,7 @@
<form class="selectCharacterScene"> <form class="selectCharacterScene">
<section class="text-center"> <section class="text-center">
<h2>Select your WOKA</h2> <h2>{$LL.woka.selectWoka.title()}</h2>
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}> <button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}>
&lt; &lt;
</button> </button>
@ -37,17 +38,19 @@
<button <button
type="submit" type="submit"
class="selectCharacterSceneFormSubmit nes-btn is-primary" class="selectCharacterSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={cameraScene}>Continue</button on:click|preventDefault={cameraScene}>{$LL.woka.selectWoka.continue()}</button
> >
<button <button
type="submit" type="submit"
class="selectCharacterSceneFormCustomYourOwnSubmit nes-btn" class="selectCharacterSceneFormCustomYourOwnSubmit nes-btn"
on:click|preventDefault={customizeScene}>Customize your WOKA</button on:click|preventDefault={customizeScene}>{$LL.woka.selectWoka.customize()}</button
> >
</section> </section>
</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;
@ -90,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

@ -1,6 +1,8 @@
import axios from "axios"; import axios from "axios";
import * as rax from "retry-axios"; import * as rax from "retry-axios";
import { errorStore } from "../Stores/ErrorStore"; import { errorStore } from "../Stores/ErrorStore";
import LL from "../i18n/i18n-svelte";
import { get } from "svelte/store";
/** /**
* This instance of Axios will retry in case of an issue and display an error message as a HTML overlay. * This instance of Axios will retry in case of an issue and display an error message as a HTML overlay.
@ -26,7 +28,7 @@ axiosWithRetry.defaults.raxConfig = {
console.log(err); console.log(err);
console.log(cfg); console.log(cfg);
console.log(`Retry attempt #${cfg?.currentRetryAttempt} on URL '${err.config.url}'`); console.log(`Retry attempt #${cfg?.currentRetryAttempt} on URL '${err.config.url}'`);
errorStore.addErrorMessage("Unable to connect to WorkAdventure. Are you connected to internet?", { errorStore.addErrorMessage(get(LL).error.connectionRetry.unableConnect(), {
closable: false, closable: false,
id: "axios_retry", id: "axios_retry",
}); });

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

@ -91,9 +91,7 @@ export class Room {
} }
baseUrl.pathname = "/_/" + instance + "/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname; baseUrl.pathname = "/_/" + instance + "/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname;
if (absoluteExitSceneUrl.hash) { baseUrl.hash = absoluteExitSceneUrl.hash;
baseUrl.hash = absoluteExitSceneUrl.hash;
}
return baseUrl; return baseUrl;
} }

View file

@ -1,32 +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 = getEnv("FALLBACK_LOCALE") || undefined;
export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600;
export { export {
DEBUG_MODE, DEBUG_MODE,
@ -44,4 +58,5 @@ export {
TURN_PASSWORD, TURN_PASSWORD,
JITSI_URL, JITSI_URL,
JITSI_PRIVATE_MODE, JITSI_PRIVATE_MODE,
FALLBACK_LOCALE,
}; };

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