Merge pull request #1669 from thecodingmachine/develop
Deploy 2021-12-23
4
.github/workflows/continuous_integration.yml
vendored
@ -39,7 +39,7 @@ jobs:
|
||||
working-directory: "messages"
|
||||
|
||||
- name: "Build proto messages"
|
||||
run: yarn run proto && yarn run copy-to-front
|
||||
run: yarn run proto && yarn run copy-to-front && yarn run json-copy-to-front
|
||||
working-directory: "messages"
|
||||
|
||||
- name: "Create index.html"
|
||||
@ -97,7 +97,7 @@ jobs:
|
||||
working-directory: "messages"
|
||||
|
||||
- name: "Build proto messages"
|
||||
run: yarn run proto && yarn run copy-to-pusher
|
||||
run: yarn run proto && yarn run copy-to-pusher && yarn run json-copy-to-pusher
|
||||
working-directory: "messages"
|
||||
|
||||
- name: "Build"
|
||||
|
15
.github/workflows/end_to_end_tests.yml
vendored
@ -20,6 +20,15 @@ jobs:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v2.0.0"
|
||||
|
||||
- name: "Setup NodeJS"
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: npm install
|
||||
working-directory: "tests"
|
||||
|
||||
- name: "Setup .env file"
|
||||
run: cp .env.template .env
|
||||
|
||||
@ -27,10 +36,10 @@ jobs:
|
||||
run: sudo chown 1000:1000 -R .
|
||||
|
||||
- name: "Start environment"
|
||||
run: docker-compose up -d
|
||||
run: LIVE_RELOAD=0 docker-compose up -d
|
||||
|
||||
- name: "Wait for environment to build (and downloading testcafe image)"
|
||||
run: (docker-compose -f docker-compose.testcafe.yml pull &) && docker-compose logs -f --tail=0 front | grep -q "Compiled successfully"
|
||||
run: (docker-compose -f docker-compose.testcafe.yml build &) && docker-compose logs -f --tail=0 front | grep -q "Compiled successfully"
|
||||
|
||||
# - name: "temp debug: display logs"
|
||||
# run: docker-compose logs
|
||||
@ -42,7 +51,7 @@ jobs:
|
||||
# run: docker-compose logs -f pusher | grep -q "WorkAdventure starting on port"
|
||||
|
||||
- name: "Run tests"
|
||||
run: docker-compose -f docker-compose.testcafe.yml up --exit-code-from testcafe
|
||||
run: PROJECT_DIR=$(pwd) docker-compose -f docker-compose.testcafe.yml up --exit-code-from testcafe
|
||||
|
||||
- name: Upload failed tests
|
||||
if: ${{ failure() }}
|
||||
|
2
.github/workflows/push-to-npm.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
||||
working-directory: "messages"
|
||||
|
||||
- name: "Build proto messages"
|
||||
run: yarn run proto && yarn run copy-to-front
|
||||
run: yarn run proto && yarn run copy-to-front && yarn run json-copy-to-front
|
||||
working-directory: "messages"
|
||||
|
||||
- name: "Create index.html"
|
||||
|
@ -1,6 +1,10 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
(
|
||||
cd messages || exit
|
||||
yarn run precommit
|
||||
)
|
||||
(
|
||||
cd front || exit
|
||||
yarn run precommit
|
||||
|
@ -10,6 +10,14 @@ We love to receive contributions from our community — you!
|
||||
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.
|
||||
|
||||
## Contributing external resources
|
||||
|
||||
You can share your work on maps / articles / videos related to WorkAdventure on our [awesome-workadventure](https://github.com/workadventure/awesome-workadventure) list.
|
||||
|
||||
## Developer documentation
|
||||
|
||||
Documentation targeted at developers can be found in the [`/docs/dev`](docs/dev/)
|
||||
|
||||
## Using the issue tracker
|
||||
|
||||
First things first: **Do NOT report security vulnerabilities in public issues!**.
|
||||
@ -59,9 +67,43 @@ $ docker-compose exec back yarn run pretty
|
||||
|
||||
WorkAdventure is based on a video game engine (Phaser), and video games are not the easiest programs to unit test.
|
||||
|
||||
Nevertheless, if your code can be unit tested, please provide a unit test (we use Jasmine).
|
||||
Nevertheless, if your code can be unit tested, please provide a unit test (we use Jasmine), or an end-to-end test (we use Testcafe).
|
||||
|
||||
If you are providing a new feature, you should setup a test map in the `maps/tests` directory. The test map should contain
|
||||
some description text describing how to test the feature. Finally, you should modify the `maps/tests/index.html` file
|
||||
to add a reference to your newly created test map.
|
||||
some description text describing how to test the feature.
|
||||
|
||||
* if the features is meant to be manually tested, you should modify the `maps/tests/index.html` file to add a reference
|
||||
to your newly created test map
|
||||
* if the features can be automatically tested, please provide a testcafe test
|
||||
|
||||
#### Running testcafe tests
|
||||
|
||||
End-to-end tests are available in the "/tests" directory.
|
||||
|
||||
To run these tests locally:
|
||||
|
||||
```console
|
||||
$ LIVE_RELOAD=0 docker-compose up -d
|
||||
$ cd tests
|
||||
$ npm install
|
||||
$ npm run test
|
||||
```
|
||||
|
||||
Note: If your tests fail on a Javascript error in "sockjs", this is due to the
|
||||
Webpack live reload. The Webpack live reload feature is conflicting with testcafe. This is why we recommend starting
|
||||
WorkAdventure with the `LIVE_RELOAD=0` environment variable.
|
||||
|
||||
End-to-end tests can take a while to run. To run only one test, use:
|
||||
|
||||
```console
|
||||
$ npm run test -- tests/[name of the test file].ts
|
||||
```
|
||||
|
||||
You can also run the tests inside a container (but you will not have visual feedbacks on your test, so we recommend using
|
||||
the local tests).
|
||||
|
||||
```console
|
||||
$ LIVE_RELOAD=0 docker-compose up -d
|
||||
# Wait 2-3 minutes for the environment to start, then:
|
||||
$ PROJECT_DIR=$(pwd) docker-compose -f docker-compose.testcafe.yml up
|
||||
```
|
||||
|
@ -14,6 +14,10 @@ In WorkAdventure you can move around your office and talk to your colleagues (us
|
||||
|
||||
See more features for your virtual office: https://workadventu.re/virtual-office
|
||||
|
||||
## Community resources
|
||||
|
||||
Check out resources developed by the WorkAdventure community at [awesome-workadventure](https://github.com/workadventure/awesome-workadventure)
|
||||
|
||||
## Setting up a development environment
|
||||
|
||||
Install Docker.
|
||||
|
@ -12,43 +12,52 @@ export class DebugController {
|
||||
|
||||
getDump() {
|
||||
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
|
||||
const query = parse(req.getQuery());
|
||||
(async () => {
|
||||
const query = parse(req.getQuery());
|
||||
|
||||
if (query.token !== ADMIN_API_TOKEN) {
|
||||
return res.writeStatus("401 Unauthorized").end("Invalid token sent!");
|
||||
}
|
||||
if (query.token !== ADMIN_API_TOKEN) {
|
||||
return res.writeStatus("401 Unauthorized").end("Invalid token sent!");
|
||||
}
|
||||
|
||||
return res
|
||||
.writeStatus("200 OK")
|
||||
.writeHeader("Content-Type", "application/json")
|
||||
.end(
|
||||
stringify(socketManager.getWorlds(), (key: unknown, value: unknown) => {
|
||||
if (key === "listeners") {
|
||||
return "Listeners";
|
||||
}
|
||||
if (key === "socket") {
|
||||
return "Socket";
|
||||
}
|
||||
if (key === "batchedMessages") {
|
||||
return "BatchedMessages";
|
||||
}
|
||||
if (value instanceof Map) {
|
||||
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
for (const [mapKey, mapValue] of value.entries()) {
|
||||
obj[mapKey] = mapValue;
|
||||
return res
|
||||
.writeStatus("200 OK")
|
||||
.writeHeader("Content-Type", "application/json")
|
||||
.end(
|
||||
stringify(
|
||||
await Promise.all(socketManager.getWorlds().values()),
|
||||
(key: unknown, value: unknown) => {
|
||||
if (key === "listeners") {
|
||||
return "Listeners";
|
||||
}
|
||||
if (key === "socket") {
|
||||
return "Socket";
|
||||
}
|
||||
if (key === "batchedMessages") {
|
||||
return "BatchedMessages";
|
||||
}
|
||||
if (value instanceof Map) {
|
||||
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
for (const [mapKey, mapValue] of value.entries()) {
|
||||
obj[mapKey] = mapValue;
|
||||
}
|
||||
return obj;
|
||||
} else if (value instanceof Set) {
|
||||
const obj: Array<unknown> = [];
|
||||
for (const [setKey, setValue] of value.entries()) {
|
||||
obj.push(setValue);
|
||||
}
|
||||
return obj;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
} else if (value instanceof Set) {
|
||||
const obj: Array<unknown> = [];
|
||||
for (const [setKey, setValue] of value.entries()) {
|
||||
obj.push(setValue);
|
||||
}
|
||||
return obj;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
})
|
||||
);
|
||||
)
|
||||
);
|
||||
})().catch((e) => {
|
||||
console.error(e);
|
||||
res.writeStatus("500");
|
||||
res.end("An error occurred");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,13 @@ import { PointInterface } from "./Websocket/PointInterface";
|
||||
import { Group } from "./Group";
|
||||
import { User, UserSocket } from "./User";
|
||||
import { PositionInterface } from "_Model/PositionInterface";
|
||||
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone";
|
||||
import {
|
||||
EmoteCallback,
|
||||
EntersCallback,
|
||||
LeavesCallback,
|
||||
MovesCallback,
|
||||
PlayerDetailsUpdatedCallback,
|
||||
} from "_Model/Zone";
|
||||
import { PositionNotifier } from "./PositionNotifier";
|
||||
import { Movable } from "_Model/Movable";
|
||||
import {
|
||||
@ -11,6 +17,7 @@ import {
|
||||
EmoteEventMessage,
|
||||
ErrorMessage,
|
||||
JoinRoomMessage,
|
||||
SetPlayerDetailsMessage,
|
||||
SubToPusherRoomMessage,
|
||||
VariableMessage,
|
||||
VariableWithTagMessage,
|
||||
@ -56,10 +63,19 @@ export class GameRoom {
|
||||
onEnters: EntersCallback,
|
||||
onMoves: MovesCallback,
|
||||
onLeaves: LeavesCallback,
|
||||
onEmote: EmoteCallback
|
||||
onEmote: EmoteCallback,
|
||||
onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback
|
||||
) {
|
||||
// A zone is 10 sprites wide.
|
||||
this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves, onEmote);
|
||||
this.positionNotifier = new PositionNotifier(
|
||||
320,
|
||||
320,
|
||||
onEnters,
|
||||
onMoves,
|
||||
onLeaves,
|
||||
onEmote,
|
||||
onPlayerDetailsUpdated
|
||||
);
|
||||
}
|
||||
|
||||
public static async create(
|
||||
@ -71,7 +87,8 @@ export class GameRoom {
|
||||
onEnters: EntersCallback,
|
||||
onMoves: MovesCallback,
|
||||
onLeaves: LeavesCallback,
|
||||
onEmote: EmoteCallback
|
||||
onEmote: EmoteCallback,
|
||||
onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback
|
||||
): Promise<GameRoom> {
|
||||
const mapDetails = await GameRoom.getMapDetails(roomUrl);
|
||||
|
||||
@ -85,7 +102,8 @@ export class GameRoom {
|
||||
onEnters,
|
||||
onMoves,
|
||||
onLeaves,
|
||||
onEmote
|
||||
onEmote,
|
||||
onPlayerDetailsUpdated
|
||||
);
|
||||
|
||||
return gameRoom;
|
||||
@ -180,6 +198,14 @@ export class GameRoom {
|
||||
this.updateUserGroup(user);
|
||||
}
|
||||
|
||||
updatePlayerDetails(user: User, playerDetailsMessage: SetPlayerDetailsMessage) {
|
||||
if (playerDetailsMessage.getRemoveoutlinecolor()) {
|
||||
user.outlineColor = undefined;
|
||||
} else {
|
||||
user.outlineColor = playerDetailsMessage.getOutlinecolor();
|
||||
}
|
||||
}
|
||||
|
||||
private updateUserGroup(user: User): void {
|
||||
user.group?.updatePosition();
|
||||
user.group?.searchForNearbyUsers();
|
||||
|
@ -8,12 +8,19 @@
|
||||
* The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted
|
||||
* number of players around the current player.
|
||||
*/
|
||||
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone } from "./Zone";
|
||||
import {
|
||||
EmoteCallback,
|
||||
EntersCallback,
|
||||
LeavesCallback,
|
||||
MovesCallback,
|
||||
PlayerDetailsUpdatedCallback,
|
||||
Zone,
|
||||
} from "./Zone";
|
||||
import { Movable } from "_Model/Movable";
|
||||
import { PositionInterface } from "_Model/PositionInterface";
|
||||
import { ZoneSocket } from "../RoomManager";
|
||||
import { User } from "../Model/User";
|
||||
import { EmoteEventMessage } from "../Messages/generated/messages_pb";
|
||||
import { EmoteEventMessage, SetPlayerDetailsMessage } from "../Messages/generated/messages_pb";
|
||||
|
||||
interface ZoneDescriptor {
|
||||
i: number;
|
||||
@ -42,7 +49,8 @@ export class PositionNotifier {
|
||||
private onUserEnters: EntersCallback,
|
||||
private onUserMoves: MovesCallback,
|
||||
private onUserLeaves: LeavesCallback,
|
||||
private onEmote: EmoteCallback
|
||||
private onEmote: EmoteCallback,
|
||||
private onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback
|
||||
) {}
|
||||
|
||||
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
|
||||
@ -98,7 +106,15 @@ export class PositionNotifier {
|
||||
|
||||
let zone = this.zones[j][i];
|
||||
if (zone === undefined) {
|
||||
zone = new Zone(this.onUserEnters, this.onUserMoves, this.onUserLeaves, this.onEmote, i, j);
|
||||
zone = new Zone(
|
||||
this.onUserEnters,
|
||||
this.onUserMoves,
|
||||
this.onUserLeaves,
|
||||
this.onEmote,
|
||||
this.onPlayerDetailsUpdated,
|
||||
i,
|
||||
j
|
||||
);
|
||||
this.zones[j][i] = zone;
|
||||
}
|
||||
return zone;
|
||||
@ -132,4 +148,11 @@ export class PositionNotifier {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public updatePlayerDetails(user: User, playerDetails: SetPlayerDetailsMessage) {
|
||||
const position = user.getPosition();
|
||||
const zoneDesc = this.getZoneDescriptorFromCoordinates(position.x, position.y);
|
||||
const zone = this.getZone(zoneDesc.i, zoneDesc.j);
|
||||
zone.updatePlayerDetails(user, playerDetails);
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
CompanionMessage,
|
||||
PusherToBackMessage,
|
||||
ServerToClientMessage,
|
||||
SetPlayerDetailsMessage,
|
||||
SubMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
|
||||
@ -31,7 +32,8 @@ export class User implements Movable {
|
||||
public readonly visitCardUrl: string | null,
|
||||
public readonly name: string,
|
||||
public readonly characterLayers: CharacterLayer[],
|
||||
public readonly companion?: CompanionMessage
|
||||
public readonly companion?: CompanionMessage,
|
||||
private _outlineColor?: number | undefined
|
||||
) {
|
||||
this.listenedZones = new Set<Zone>();
|
||||
|
||||
@ -69,4 +71,17 @@ export class User implements Movable {
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
public set outlineColor(value: number | undefined) {
|
||||
this._outlineColor = value;
|
||||
|
||||
const playerDetails = new SetPlayerDetailsMessage();
|
||||
if (value === undefined) {
|
||||
playerDetails.setRemoveoutlinecolor(true);
|
||||
} else {
|
||||
playerDetails.setOutlinecolor(value);
|
||||
}
|
||||
|
||||
this.positionNotifier.updatePlayerDetails(this, playerDetails);
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,20 @@ import { PositionInterface } from "_Model/PositionInterface";
|
||||
import { Movable } from "./Movable";
|
||||
import { Group } from "./Group";
|
||||
import { ZoneSocket } from "../RoomManager";
|
||||
import { EmoteEventMessage } from "../Messages/generated/messages_pb";
|
||||
import {
|
||||
EmoteEventMessage,
|
||||
SetPlayerDetailsMessage,
|
||||
PlayerDetailsUpdatedMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
|
||||
export type EntersCallback = (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => void;
|
||||
export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void;
|
||||
export type LeavesCallback = (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => void;
|
||||
export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void;
|
||||
export type PlayerDetailsUpdatedCallback = (
|
||||
playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage,
|
||||
listener: ZoneSocket
|
||||
) => void;
|
||||
|
||||
export class Zone {
|
||||
private things: Set<Movable> = new Set<Movable>();
|
||||
@ -19,6 +27,7 @@ export class Zone {
|
||||
private onMoves: MovesCallback,
|
||||
private onLeaves: LeavesCallback,
|
||||
private onEmote: EmoteCallback,
|
||||
private onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback,
|
||||
public readonly x: number,
|
||||
public readonly y: number
|
||||
) {}
|
||||
@ -106,4 +115,14 @@ export class Zone {
|
||||
this.onEmote(emoteEventMessage, listener);
|
||||
}
|
||||
}
|
||||
|
||||
public updatePlayerDetails(user: User, playerDetails: SetPlayerDetailsMessage) {
|
||||
const playerDetailsUpdatedMessage = new PlayerDetailsUpdatedMessage();
|
||||
playerDetailsUpdatedMessage.setUserid(user.id);
|
||||
playerDetailsUpdatedMessage.setDetails(playerDetails);
|
||||
|
||||
for (const listener of this.listeners) {
|
||||
this.onPlayerDetailsUpdated(playerDetailsUpdatedMessage, listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
AdminPusherToBackMessage,
|
||||
AdminRoomMessage,
|
||||
BanMessage,
|
||||
BanUserMessage,
|
||||
BatchToPusherMessage,
|
||||
BatchToPusherRoomMessage,
|
||||
EmotePromptMessage,
|
||||
@ -16,7 +17,9 @@ import {
|
||||
QueryJitsiJwtMessage,
|
||||
RefreshRoomPromptMessage,
|
||||
RoomMessage,
|
||||
SendUserMessage,
|
||||
ServerToAdminClientMessage,
|
||||
SetPlayerDetailsMessage,
|
||||
SilentMessage,
|
||||
UserMovesMessage,
|
||||
VariableMessage,
|
||||
@ -118,14 +121,17 @@ const roomManager: IRoomManagerServer = {
|
||||
);
|
||||
} else if (message.hasSendusermessage()) {
|
||||
const sendUserMessage = message.getSendusermessage();
|
||||
if (sendUserMessage !== undefined) {
|
||||
socketManager.handlerSendUserMessage(user, sendUserMessage);
|
||||
}
|
||||
socketManager.handleSendUserMessage(user, sendUserMessage as SendUserMessage);
|
||||
} else if (message.hasBanusermessage()) {
|
||||
const banUserMessage = message.getBanusermessage();
|
||||
if (banUserMessage !== undefined) {
|
||||
socketManager.handlerBanUserMessage(room, user, banUserMessage);
|
||||
}
|
||||
socketManager.handlerBanUserMessage(room, user, banUserMessage as BanUserMessage);
|
||||
} else if (message.hasSetplayerdetailsmessage()) {
|
||||
const setPlayerDetailsMessage = message.getSetplayerdetailsmessage();
|
||||
socketManager.handleSetPlayerDetails(
|
||||
room,
|
||||
user,
|
||||
setPlayerDetailsMessage as SetPlayerDetailsMessage
|
||||
);
|
||||
} else {
|
||||
throw new Error("Unhandled message type");
|
||||
}
|
||||
@ -251,7 +257,12 @@ const roomManager: IRoomManagerServer = {
|
||||
},
|
||||
sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||
socketManager
|
||||
.sendAdminMessage(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage())
|
||||
.sendAdminMessage(
|
||||
call.request.getRoomid(),
|
||||
call.request.getRecipientuuid(),
|
||||
call.request.getMessage(),
|
||||
call.request.getType()
|
||||
)
|
||||
.catch((e) => console.error(e));
|
||||
|
||||
callback(null, new EmptyMessage());
|
||||
|
@ -33,6 +33,8 @@ import {
|
||||
VariableMessage,
|
||||
BatchToPusherRoomMessage,
|
||||
SubToPusherRoomMessage,
|
||||
SetPlayerDetailsMessage,
|
||||
PlayerDetailsUpdatedMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { User, UserSocket } from "../Model/User";
|
||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||
@ -97,6 +99,7 @@ export class SocketManager {
|
||||
}
|
||||
const roomJoinedMessage = new RoomJoinedMessage();
|
||||
roomJoinedMessage.setTagList(joinRoomMessage.getTagList());
|
||||
roomJoinedMessage.setUserroomtoken(joinRoomMessage.getUserroomtoken());
|
||||
|
||||
for (const [itemId, item] of room.getItemsState().entries()) {
|
||||
const itemStateMessage = new ItemStateMessage();
|
||||
@ -150,20 +153,9 @@ export class SocketManager {
|
||||
//room.setViewport(client, client.viewport);
|
||||
}
|
||||
|
||||
// Useless now, will be useful again if we allow editing details in game
|
||||
/*handleSetPlayerDetails(client: UserSocket, playerDetailsMessage: SetPlayerDetailsMessage) {
|
||||
const playerDetails = {
|
||||
name: playerDetailsMessage.getName(),
|
||||
characterLayers: playerDetailsMessage.getCharacterlayersList()
|
||||
};
|
||||
//console.log(SocketIoEvent.SET_PLAYER_DETAILS, playerDetails);
|
||||
if (!isSetPlayerDetailsMessage(playerDetails)) {
|
||||
emitError(client, 'Invalid SET_PLAYER_DETAILS message received: ');
|
||||
return;
|
||||
}
|
||||
client.name = playerDetails.name;
|
||||
client.characterLayers = SocketManager.mergeCharacterLayersAndCustomTextures(playerDetails.characterLayers, client.textures);
|
||||
}*/
|
||||
handleSetPlayerDetails(room: GameRoom, user: User, playerDetailsMessage: SetPlayerDetailsMessage) {
|
||||
room.updatePlayerDetails(user, playerDetailsMessage);
|
||||
}
|
||||
|
||||
handleSilentMessage(room: GameRoom, user: User, silentMessage: SilentMessage) {
|
||||
room.setSilent(user, silentMessage.getSilent());
|
||||
@ -281,7 +273,9 @@ export class SocketManager {
|
||||
(thing: Movable, newZone: Zone | null, listener: ZoneSocket) =>
|
||||
this.onClientLeave(thing, newZone, listener),
|
||||
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
|
||||
this.onEmote(emoteEventMessage, listener)
|
||||
this.onEmote(emoteEventMessage, listener),
|
||||
(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, listener: ZoneSocket) =>
|
||||
this.onPlayerDetailsUpdated(playerDetailsUpdatedMessage, listener)
|
||||
)
|
||||
.then((gameRoom) => {
|
||||
gaugeManager.incNbRoomGauge();
|
||||
@ -328,6 +322,12 @@ export class SocketManager {
|
||||
userJoinedZoneMessage.setVisitcardurl(thing.visitCardUrl);
|
||||
}
|
||||
userJoinedZoneMessage.setCompanion(thing.companion);
|
||||
if (thing.outlineColor === undefined) {
|
||||
userJoinedZoneMessage.setHasoutline(false);
|
||||
} else {
|
||||
userJoinedZoneMessage.setHasoutline(true);
|
||||
userJoinedZoneMessage.setOutlinecolor(thing.outlineColor);
|
||||
}
|
||||
|
||||
const subMessage = new SubToPusherMessage();
|
||||
subMessage.setUserjoinedzonemessage(userJoinedZoneMessage);
|
||||
@ -377,6 +377,13 @@ export class SocketManager {
|
||||
emitZoneMessage(subMessage, client);
|
||||
}
|
||||
|
||||
private onPlayerDetailsUpdated(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, client: ZoneSocket) {
|
||||
const subMessage = new SubToPusherMessage();
|
||||
subMessage.setPlayerdetailsupdatedmessage(playerDetailsUpdatedMessage);
|
||||
|
||||
emitZoneMessage(subMessage, client);
|
||||
}
|
||||
|
||||
private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone | null, group: Group): void {
|
||||
const position = group.getPosition();
|
||||
const pointMessage = new PointMessage();
|
||||
@ -571,7 +578,7 @@ export class SocketManager {
|
||||
user.socket.write(serverToClientMessage);
|
||||
}
|
||||
|
||||
public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage) {
|
||||
public handleSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage) {
|
||||
const sendUserMessage = new SendUserMessage();
|
||||
sendUserMessage.setMessage(sendUserMessageToSend.getMessage());
|
||||
sendUserMessage.setType(sendUserMessageToSend.getType());
|
||||
@ -690,7 +697,7 @@ export class SocketManager {
|
||||
}
|
||||
}
|
||||
|
||||
public async sendAdminMessage(roomId: string, recipientUuid: string, message: string): Promise<void> {
|
||||
public async sendAdminMessage(roomId: string, recipientUuid: string, message: string, type: string): Promise<void> {
|
||||
const room = await this.roomsPromises.get(roomId);
|
||||
if (!room) {
|
||||
console.error(
|
||||
@ -714,7 +721,7 @@ export class SocketManager {
|
||||
for (const recipient of recipients) {
|
||||
const sendUserMessage = new SendUserMessage();
|
||||
sendUserMessage.setMessage(message);
|
||||
sendUserMessage.setType("ban"); //todo: is the type correct?
|
||||
sendUserMessage.setType(type);
|
||||
|
||||
const serverToClientMessage = new ServerToClientMessage();
|
||||
serverToClientMessage.setSendusermessage(sendUserMessage);
|
||||
|
@ -1,12 +1,7 @@
|
||||
/**
|
||||
* Handles variables shared between the scripting API and the server.
|
||||
*/
|
||||
import {
|
||||
ITiledMap,
|
||||
ITiledMapLayer,
|
||||
ITiledMapObject,
|
||||
ITiledMapObjectLayer,
|
||||
} from "@workadventure/tiled-map-type-guard/dist";
|
||||
import { ITiledMap, ITiledMapLayer, ITiledMapObject } from "@workadventure/tiled-map-type-guard/dist";
|
||||
import { User } from "_Model/User";
|
||||
import { variablesRepository } from "./Repository/VariablesRepository";
|
||||
import { redisClient } from "./RedisClient";
|
||||
|
@ -51,7 +51,8 @@ describe("GameRoom", () => {
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
emote
|
||||
emote,
|
||||
() => {}
|
||||
);
|
||||
|
||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
||||
@ -86,7 +87,8 @@ describe("GameRoom", () => {
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
emote
|
||||
emote,
|
||||
() => {}
|
||||
);
|
||||
|
||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
||||
@ -125,7 +127,8 @@ describe("GameRoom", () => {
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
emote
|
||||
emote,
|
||||
() => {}
|
||||
);
|
||||
|
||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
||||
|
@ -19,7 +19,8 @@ describe("PositionNotifier", () => {
|
||||
moveTriggered = true;
|
||||
}, (thing: Movable) => {
|
||||
leaveTriggered = true;
|
||||
}, () => {});
|
||||
}, () => {},
|
||||
() => {});
|
||||
|
||||
const user1 = new User(1, 'test', '10.0.0.2', {
|
||||
x: 500,
|
||||
@ -94,7 +95,8 @@ describe("PositionNotifier", () => {
|
||||
moveTriggered = true;
|
||||
}, (thing: Movable) => {
|
||||
leaveTriggered = true;
|
||||
}, () => {});
|
||||
}, () => {},
|
||||
() => {});
|
||||
|
||||
const user1 = new User(1, 'test', '10.0.0.2', {
|
||||
x: 500,
|
||||
|
@ -101,7 +101,10 @@
|
||||
"host": {
|
||||
"url": "maps-"+url
|
||||
},
|
||||
"ports": [80]
|
||||
"ports": [80],
|
||||
"env": {
|
||||
"FRONT_URL": "https://play-"+url
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"image": "redis:6",
|
||||
|
@ -92,11 +92,12 @@ services:
|
||||
- "traefik.http.routers.pusher-ssl.service=pusher"
|
||||
|
||||
maps:
|
||||
image: thecodingmachine/nodejs:12-apache
|
||||
image: thecodingmachine/php:8.1-v4-apache-node12
|
||||
environment:
|
||||
DEBUG_MODE: "$DEBUG_MODE"
|
||||
HOST: "0.0.0.0"
|
||||
NODE_ENV: development
|
||||
FRONT_URL: http://play.workadventure.localhost
|
||||
#APACHE_DOCUMENT_ROOT: dist/
|
||||
#APACHE_EXTENSIONS: headers
|
||||
#APACHE_EXTENSION_HEADERS: 1
|
||||
|
@ -1,12 +1,19 @@
|
||||
version: "3"
|
||||
version: "3.5"
|
||||
services:
|
||||
testcafe:
|
||||
image: testcafe/testcafe:1.17.1
|
||||
working_dir: /tests
|
||||
build: tests/
|
||||
working_dir: /project/tests
|
||||
command:
|
||||
- --dev
|
||||
# Run as root to have the right to access /var/run/docker.sock
|
||||
user: root
|
||||
environment:
|
||||
BROWSER: "chromium --use-fake-device-for-media-stream"
|
||||
PROJECT_DIR: ${PROJECT_DIR}
|
||||
ADMIN_API_TOKEN: ${ADMIN_API_TOKEN}
|
||||
volumes:
|
||||
- ./tests:/tests
|
||||
- ./:/project
|
||||
- ./maps:/maps
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# security_opt:
|
||||
# - seccomp:unconfined
|
||||
|
@ -1,20 +1,21 @@
|
||||
version: "3"
|
||||
version: "3.5"
|
||||
services:
|
||||
reverse-proxy:
|
||||
image: traefik:v2.0
|
||||
image: traefik:v2.5
|
||||
command:
|
||||
- --api.insecure=true
|
||||
- --providers.docker
|
||||
- --entryPoints.web.address=:80
|
||||
- --entryPoints.websecure.address=:443
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
# The Web UI (enabled by --api.insecure=true)
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
- back
|
||||
- front
|
||||
#depends_on:
|
||||
# - back
|
||||
# - front
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
networks:
|
||||
@ -51,10 +52,12 @@ services:
|
||||
MAX_USERNAME_LENGTH: "$MAX_USERNAME_LENGTH"
|
||||
DISABLE_ANONYMOUS: "$DISABLE_ANONYMOUS"
|
||||
OPID_LOGIN_SCREEN_PROVIDER: "$OPID_LOGIN_SCREEN_PROVIDER"
|
||||
LIVE_RELOAD: "$LIVE_RELOAD:-true"
|
||||
command: yarn run start
|
||||
volumes:
|
||||
- ./front:/usr/src/app
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.front.rule=Host(`play.workadventure.localhost`)"
|
||||
- "traefik.http.routers.front.entryPoints=web"
|
||||
- "traefik.http.services.front.loadbalancer.server.port=8080"
|
||||
@ -87,6 +90,7 @@ services:
|
||||
volumes:
|
||||
- ./pusher:/usr/src/app
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.pusher.rule=Host(`pusher.workadventure.localhost`)"
|
||||
- "traefik.http.routers.pusher.entryPoints=web"
|
||||
- "traefik.http.services.pusher.loadbalancer.server.port=8080"
|
||||
@ -96,11 +100,12 @@ services:
|
||||
- "traefik.http.routers.pusher-ssl.service=pusher"
|
||||
|
||||
maps:
|
||||
image: thecodingmachine/nodejs:12-apache
|
||||
image: thecodingmachine/php:8.1-v4-apache-node12
|
||||
environment:
|
||||
DEBUG_MODE: "$DEBUG_MODE"
|
||||
HOST: "0.0.0.0"
|
||||
NODE_ENV: development
|
||||
FRONT_URL: http://play.workadventure.localhost
|
||||
#APACHE_DOCUMENT_ROOT: dist/
|
||||
#APACHE_EXTENSIONS: headers
|
||||
#APACHE_EXTENSION_HEADERS: 1
|
||||
@ -110,6 +115,7 @@ services:
|
||||
volumes:
|
||||
- ./maps:/var/www/html
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.maps.rule=Host(`maps.workadventure.localhost`)"
|
||||
- "traefik.http.routers.maps.entryPoints=web,traefik"
|
||||
- "traefik.http.services.maps.loadbalancer.server.port=80"
|
||||
@ -141,6 +147,7 @@ services:
|
||||
volumes:
|
||||
- ./back:/usr/src/app
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.back.rule=Host(`api.workadventure.localhost`)"
|
||||
- "traefik.http.routers.back.entryPoints=web"
|
||||
- "traefik.http.services.back.loadbalancer.server.port=8080"
|
||||
@ -159,6 +166,7 @@ services:
|
||||
volumes:
|
||||
- ./uploader:/usr/src/app
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.uploader.rule=Host(`uploader.workadventure.localhost`)"
|
||||
- "traefik.http.routers.uploader.entryPoints=web"
|
||||
- "traefik.http.services.uploader.loadbalancer.server.port=8080"
|
||||
@ -186,6 +194,7 @@ services:
|
||||
redisinsight:
|
||||
image: redislabs/redisinsight:latest
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.redisinsight.rule=Host(`redis.workadventure.localhost`)"
|
||||
- "traefik.http.routers.redisinsight.entryPoints=web"
|
||||
- "traefik.http.services.redisinsight.loadbalancer.server.port=8001"
|
||||
@ -197,6 +206,7 @@ services:
|
||||
icon:
|
||||
image: matthiasluedtke/iconserver:v3.13.0
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.icon.rule=Host(`icon.workadventure.localhost`)"
|
||||
- "traefik.http.routers.icon.entryPoints=web"
|
||||
- "traefik.http.services.icon.loadbalancer.server.port=8080"
|
||||
|
16
docs/dev/README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Developer documentation
|
||||
|
||||
This (work in progress) documentation provides a number of "how-to" guides explaining how to work on the WorkAdventure
|
||||
code.
|
||||
|
||||
This documentation is targeted at developers looking to open Pull Requests on WorkAdventure.
|
||||
|
||||
If you "only" want to design dynamic maps, please refer instead to the [scripting API documentation](https://workadventu.re/map-building/scripting.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
Check out the [contributing guide](../../CONTRIBUTING.md)
|
||||
|
||||
## Front documentation
|
||||
|
||||
- [How to add new functions in the scripting API](contributing-to-scripting-api.md)
|
276
docs/dev/contributing-to-scripting-api.md
Normal file
@ -0,0 +1,276 @@
|
||||
# How to add new functions in the scripting API
|
||||
|
||||
This documentation is intended at contributors who want to participate in the development of WorkAdventure itself.
|
||||
Before reading this, please be sure you are familiar with the [scripting API](https://workadventu.re/map-building/scripting.md).
|
||||
|
||||
The [scripting API](https://workadventu.re/map-building/scripting.md) allows map developers to add dynamic features in their maps.
|
||||
|
||||
## Why extend the scripting API?
|
||||
|
||||
The philosophy behind WorkAdventure is to build a platform that is as open as possible. Part of this strategy is to
|
||||
offer map developers the ability to turn a WorkAdventures map into something unexpected, using the API. For instance,
|
||||
you could use it to develop games (we have seen a PacMan and a mine-sweeper on WorkAdventure!)
|
||||
|
||||
We started working on the WorkAdventure scripting API with this in mind, but at some point, maybe you will find that
|
||||
a feature is missing in the API. This article is here to explain to you how to add this feature.
|
||||
|
||||
## How to extend the scripting API?
|
||||
|
||||
Extending the scripting API means modifying the core of WorkAdventure. You can of course run these
|
||||
modifications on your self-hosted instance.
|
||||
But if you want to share it with the wider community, I strongly encourage you to start by [opening an issue](https://github.com/thecodingmachine/workadventure/issues)
|
||||
on GitHub before starting the development. Check with the core maintainers that they are willing to merge your idea
|
||||
before starting developing it. Once a new function makes it into the scripting API, it is very difficult to make it
|
||||
evolve (or to deprecate), so the design of the function you add needs to be carefully considered.
|
||||
|
||||
## How does it work?
|
||||
|
||||
Scripts are executed in the browser, inside an iframe.
|
||||
|
||||
![](images/scripting_1.svg)
|
||||
|
||||
The iframe allows WorkAdventure to isolate the script in a sandbox. Because the iframe is sandbox (or on a different
|
||||
domain than the WorkAdventure server), scripts cannot directly manipulate the DOM of WorkAdventure. They also cannot
|
||||
directly access Phaser objects (Phaser is the game engine used in WorkAdventure). This is by-design. Since anyone
|
||||
can contribute a map, we cannot allow anyone to run any code in the scope of the WorkAdventure server (that would be
|
||||
a huge XSS security flaw).
|
||||
|
||||
Instead, the only way the script can interact with WorkAdventure is by sending messages using the
|
||||
[postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage).
|
||||
|
||||
![](images/scripting_2.svg)
|
||||
|
||||
We want to make life easy for map developers. So instead of asking them to directly send messages using the postMessage
|
||||
API, we provide a nice library that does this work for them. This library is what we call the "Scripting API" (we sometimes
|
||||
refer to it as the "Client API").
|
||||
|
||||
The scripting API provides the global `WA` object.
|
||||
|
||||
## A simple example
|
||||
|
||||
So let's take an example with a sample script:
|
||||
|
||||
```typescript
|
||||
WA.chat.sendChatMessage('Hello world!', 'John Doe');
|
||||
```
|
||||
|
||||
When this script is called, the scripting API is dispatching a JSON message to WorkAdventure.
|
||||
|
||||
In our case, the `sendChatMessage` function looks like this:
|
||||
|
||||
**src/Api/iframe/chat.ts**
|
||||
```typescript
|
||||
sendChatMessage(message: string, author: string) {
|
||||
sendToWorkadventure({
|
||||
type: "chat",
|
||||
data: {
|
||||
message: message,
|
||||
author: author,
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The `sendToWorkadventure` function is a utility function that dispatches the message to the main frame.
|
||||
|
||||
In WorkAdventure, the message is received in the [`IframeListener` listener class](http://github.com/thecodingmachine/workadventure/blob/1e6ce4dec8697340e2c91798864b94da9528b482/front/src/Api/IframeListener.ts#L200-L203).
|
||||
This class is in charge of analyzing the JSON messages received and dispatching them to the right place in the WorkAdventure application.
|
||||
|
||||
The message callback implemented in `IframeListener` is a giant (and disgusting) `if` statement branching to the correct
|
||||
part of the code depending on the `type` property.
|
||||
|
||||
**src/Api/IframeListener.ts**
|
||||
```typescript
|
||||
// ...
|
||||
} else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) {
|
||||
this._setPropertyStream.next(payload.data);
|
||||
} else if (payload.type === "chat" && isChatEvent(payload.data)) {
|
||||
scriptUtils.sendAnonymousChat(payload.data);
|
||||
} else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) {
|
||||
this._openPopupStream.next(payload.data);
|
||||
} else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) {
|
||||
// ...
|
||||
```
|
||||
|
||||
In this particular case, we call `scriptUtils.sendAnonymousChat` that is doing the work of displaying the chat message.
|
||||
|
||||
## Scripting API entry point
|
||||
|
||||
The `WA` object originates from the scripting API. This script is hosted on the front server, at `https://[front_WA_server]/iframe_api.js.`.
|
||||
|
||||
The entry point for this script is the file `front/src/iframe_api.ts`.
|
||||
All the other files dedicated to the iframe API are located in the `src/Api/iframe` directory.
|
||||
|
||||
## Utility functions to exchange messages
|
||||
|
||||
In the example above, we already saw you can easily send a message from the iframe to WorkAdventure using the
|
||||
[`sendToWorkadventure`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/iframe/IframeApiContribution.ts#L11-L13) utility function.
|
||||
|
||||
Of course, messaging can go the other way around and WorkAdventure can also send messages to the iframes.
|
||||
We use the [`IFrameListener.postMessage`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/IframeListener.ts#L455-L459) function for this.
|
||||
|
||||
Finally, there is a last type of utility function (a quite powerful one). It is quite common to need to call a function
|
||||
from the iframe in WorkAdventure, and to expect a response. For those use cases, the iframe API comes with a
|
||||
[`queryWorkadventure`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/iframe/IframeApiContribution.ts#L30-L49) utility function.
|
||||
|
||||
## Types
|
||||
|
||||
The JSON messages sent over the postMessage API are strictly defined using Typescript types.
|
||||
Those types are not defined using classical Typescript interfaces.
|
||||
|
||||
Indeed, Typescript interfaces only exist at compilation time but cannot be enforced on runtime. The postMessage API
|
||||
is an entry point to WorkAdventure, and as with any entry point, data must be checked (otherwise, a hacker could
|
||||
send specially crafted JSON packages to try to hack WA).
|
||||
|
||||
In WorkAdventure, we use the [generic-type-guard](https://github.com/mscharley/generic-type-guard) package. This package
|
||||
allows us to create interfaces AND custom type guards in one go.
|
||||
|
||||
Let's go back at our example. Let's have a look at the JSON message sent when we want to send a chat message from the API:
|
||||
|
||||
```typescript
|
||||
sendToWorkadventure({
|
||||
type: "chat",
|
||||
data: {
|
||||
message: message,
|
||||
author: author,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The "data" part of the message is defined in `front/src/Api/Events/ChatEvent.ts`:
|
||||
|
||||
```typescript
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isChatEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
message: tg.isString,
|
||||
author: tg.isString,
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type ChatEvent = tg.GuardedType<typeof isChatEvent>;
|
||||
```
|
||||
|
||||
Using the generic-type-guard library, we start by writing a type guard function (`isChatEvent`).
|
||||
From this type guard, the library can automatically generate the `ChatEvent` type that we can refer in our code.
|
||||
|
||||
The advantage of this technique is that, **at runtime**, WorkAdventure can verify that the JSON message received
|
||||
over the postMessage API is indeed correctly formatted.
|
||||
|
||||
If you are not familiar with Typescript type guards, you can read [an introduction to type guards in the Typescript documentation](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards).
|
||||
|
||||
### Typing one way messages
|
||||
|
||||
For "one-way" messages (from the iframe to WorkAdventure), the `sendToWorkadventure` method expects the passed
|
||||
object to be of type `IframeEvent<keyof IframeEventMap>`.
|
||||
|
||||
Note: I'd like here to thank @jonnytest1 for helping set up this type system. It rocks ;)
|
||||
|
||||
The `IFrameEvent` type is defined in `front/src/Api/Events/IframeEvent.ts`:
|
||||
|
||||
```typescript
|
||||
export type IframeEventMap = {
|
||||
loadPage: LoadPageEvent;
|
||||
chat: ChatEvent;
|
||||
openPopup: OpenPopupEvent;
|
||||
closePopup: ClosePopupEvent;
|
||||
openTab: OpenTabEvent;
|
||||
// ...
|
||||
// All the possible messages go here
|
||||
// The key goes into the "type" JSON property
|
||||
// ...
|
||||
};
|
||||
export interface IframeEvent<T extends keyof IframeEventMap> {
|
||||
type: T;
|
||||
data: IframeEventMap[T];
|
||||
}
|
||||
```
|
||||
|
||||
Similarly, if you want to type messages from WorkAdventure to the iframe, there is a very similar `IframeResponseEvent`.
|
||||
|
||||
```typescript
|
||||
export interface IframeResponseEventMap {
|
||||
userInputChat: UserInputChatEvent;
|
||||
enterEvent: EnterLeaveEvent;
|
||||
leaveEvent: EnterLeaveEvent;
|
||||
// ...
|
||||
// All the possible messages go here
|
||||
// The key goes into the "type" JSON property
|
||||
// ...
|
||||
}
|
||||
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
|
||||
type: T;
|
||||
data: IframeResponseEventMap[T];
|
||||
}
|
||||
```
|
||||
|
||||
### Typing queries (messages with answers)
|
||||
|
||||
If you want to add a new "query" (if you are using the `queryWorkadventure` utility function), you will need to
|
||||
define the type of the query and the type of the response.
|
||||
|
||||
The signature of `queryWorkadventure` is:
|
||||
|
||||
```typescript
|
||||
function queryWorkadventure<T extends keyof IframeQueryMap>(
|
||||
content: IframeQuery<T>
|
||||
): Promise<IframeQueryMap[T]["answer"]>
|
||||
```
|
||||
|
||||
Yes, that's a bit cryptic. Hopefully, all you need to know is that to add a new query, you need to edit the `iframeQueryMapTypeGuards`
|
||||
array in `front/src/Api/Events/IframeEvent.ts`:
|
||||
|
||||
```typescript
|
||||
export const iframeQueryMapTypeGuards = {
|
||||
openCoWebsite: {
|
||||
query: isOpenCoWebsiteEvent,
|
||||
answer: isCoWebsite,
|
||||
},
|
||||
getCoWebsites: {
|
||||
query: tg.isUndefined,
|
||||
answer: tg.isArray(isCoWebsite),
|
||||
},
|
||||
// ...
|
||||
// the `query` key points to the type guard of the query
|
||||
// the `answer` key points to the type guard of the response
|
||||
};
|
||||
```
|
||||
|
||||
### Responding to a query on the WorkAdventure side
|
||||
|
||||
In the WorkAdventure code, each possible query should be handled by what we call an "answerer".
|
||||
|
||||
Registering an answerer happens using the `iframeListener.registerAnswerer()` method.
|
||||
|
||||
Here is a sample:
|
||||
|
||||
```typescript
|
||||
iframeListener.registerAnswerer("openCoWebsite", (openCoWebsiteEvent, source) => {
|
||||
// ...
|
||||
|
||||
return /*...*/;
|
||||
});
|
||||
```
|
||||
|
||||
The `registerAnswerer` callback is passed the event, and should return a response (or a promise to the response) in the expected format
|
||||
(the one you defined in the `answer` key of `iframeQueryMapTypeGuards`).
|
||||
|
||||
Important:
|
||||
|
||||
- there can be only one answerer registered for a given query type.
|
||||
- if the answerer is not valid any more, you need to unregister the answerer using `iframeListener.unregisterAnswerer`.
|
||||
|
||||
|
||||
## sendToWorkadventure VS queryWorkadventure
|
||||
|
||||
- `sendToWorkadventure` is used to send messages one way from the iframe to WorkAdventure. No response is expected. In particular
|
||||
if an error happens in WorkAdventure, the iframe will not be notified.
|
||||
- `queryWorkadventure` is used to send queries that expect an answer. If an error happens in WorkAdventure (i.e. if an
|
||||
exception is raised), the exception will be propagated to the iframe.
|
||||
|
||||
Because `queryWorkadventure` handles exceptions properly, it can be interesting to use `queryWorkadventure` instead
|
||||
of `sendToWorkadventure`, even for "one-way" messages. The return message type is simply `undefined` in this case.
|
||||
|
1
docs/dev/images/scripting_1.svg
Normal file
After Width: | Height: | Size: 86 KiB |
1
docs/dev/images/scripting_2.svg
Normal file
After Width: | Height: | Size: 64 KiB |
@ -58,6 +58,34 @@ WA.onInit().then(() => {
|
||||
})
|
||||
```
|
||||
|
||||
### Get the user-room token of the player
|
||||
|
||||
```
|
||||
WA.player.userRoomToken: string;
|
||||
```
|
||||
|
||||
The user-room token is available from the `WA.player.userRoomToken` property.
|
||||
|
||||
This token can be used by third party services to authenticate a player and prove that the player is in a given room.
|
||||
The token is generated by the administration panel linked to WorkAdventure. The token is a string and is depending on your implementation of the administration panel.
|
||||
In WorkAdventure SAAS version, the token is a JWT token that contains information such as the player's room ID and its associated membership ID.
|
||||
|
||||
If you are using the self-hosted version of WorkAdventure and you developed your own administration panel, the token can be anything.
|
||||
By default, self-hosted versions of WorkAdventure don't come with an administration panel, so the token string will be empty.
|
||||
|
||||
{.alert.alert-info}
|
||||
A typical use-case for the user-room token is providing logo upload capabilities in a map.
|
||||
The token can be used as a way to authenticate a WorkAdventure player and ensure he is indeed in the map and authorized to upload a logo.
|
||||
|
||||
{.alert.alert-info}
|
||||
You need to wait for the end of the initialization before accessing `WA.player.userRoomToken`
|
||||
|
||||
```typescript
|
||||
WA.onInit().then(() => {
|
||||
console.log('Token: ', WA.player.userRoomToken);
|
||||
})
|
||||
```
|
||||
|
||||
### Listen to player movement
|
||||
```
|
||||
WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void;
|
||||
@ -78,3 +106,25 @@ Example :
|
||||
```javascript
|
||||
WA.player.onPlayerMove(console.log);
|
||||
```
|
||||
|
||||
### Set the outline color of the player
|
||||
```
|
||||
WA.player.setOutlineColor(red: number, green: number, blue: number): Promise<void>;
|
||||
WA.player.removeOutlineColor(): Promise<void>;
|
||||
```
|
||||
|
||||
You can display a thin line around your player's name (the "outline").
|
||||
|
||||
Use `setOutlineColor` to set the outline and `removeOutlineColor` to remove it.
|
||||
|
||||
Colors are expressed in RGB. Each parameter is an integer between 0 and 255.
|
||||
|
||||
```typescript
|
||||
// Let's add a red outline to our player
|
||||
WA.player.setOutlineColor(255, 0, 0);
|
||||
```
|
||||
|
||||
When you set the outline on your player, other players will see the outline too (the outline color is shared across
|
||||
browsers automatically).
|
||||
|
||||
![](images/outlines.png)
|
||||
|
92
docs/maps/camera.md
Normal file
@ -0,0 +1,92 @@
|
||||
{.section-title.accent.text-primary}
|
||||
# Working with camera
|
||||
|
||||
## Focusable Zones
|
||||
|
||||
It is possible to define special regions on the map that can make the camera zoom and center on themselves. We call them "Focusable Zones". When player gets inside, his camera view will be altered - focused, zoomed and locked on defined zone, like this:
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/0_focusable_zone.png" alt="" />
|
||||
</div>
|
||||
|
||||
### Adding new **Focusable Zone**:
|
||||
|
||||
1. Make sure you are editing an **Object Layer**
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/1_object_layer.png" alt="" />
|
||||
</div>
|
||||
|
||||
2. Select **Insert Rectangle** tool
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/2_rectangle_zone.png" alt="" />
|
||||
</div>
|
||||
|
||||
3. Define new object wherever you want. For example, you can make your chilling room event cosier!
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/3_define_new_zone.png" alt="" />
|
||||
</div>
|
||||
|
||||
4. Make sure your object is of type "zone"!
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/4_add_zone_type.png" alt="" />
|
||||
</div>
|
||||
|
||||
5. Edit this new object and click on **Add Property**, like this:
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/5_click_add_property.png" alt="" />
|
||||
</div>
|
||||
|
||||
6. Add a **bool** property of name *focusable*:
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/6_add_focusable_prop.png" alt="" />
|
||||
</div>
|
||||
|
||||
7. Make sure it's checked! :)
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/7_make_sure_checked.png" alt="" />
|
||||
</div>
|
||||
|
||||
All should be set up now and your new **Focusable Zone** should be working fine!
|
||||
|
||||
### Defining custom zoom margin:
|
||||
|
||||
If you want, you can add an additional property to control how much should the camera zoom onto focusable zone.
|
||||
|
||||
1. Like before, click on **Add Property**
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/5_click_add_property.png" alt="" />
|
||||
</div>
|
||||
|
||||
2. Add a **float** property of name *zoom_margin*:
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/8_add_zoom_margin.png" alt="" />
|
||||
</div>
|
||||
|
||||
2. Define how much (in percentage value) should the zoom be decreased:
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/9_optional_zoom_margin_defined.png" alt="" />
|
||||
</div>
|
||||
|
||||
For example, if you define your zone as a 300x200 rectangle, setting this property to 0.5 *(50%)* means the camera will try to fit within the viewport the entire zone + margin of 50% of its dimensions, so 450x300.
|
||||
|
||||
- No margin defined
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/no_margin.png" alt="" />
|
||||
</div>
|
||||
|
||||
- Margin set to **0.35**
|
||||
|
||||
<div class="px-5 card rounded d-inline-block">
|
||||
<img class="document-img" src="images/camera/with_margin.png" alt="" />
|
||||
</div>
|
BIN
docs/maps/images/camera/0_focusable_zone.png
Normal file
After Width: | Height: | Size: 150 KiB |
BIN
docs/maps/images/camera/1_object_layer.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
docs/maps/images/camera/2_rectangle_zone.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
docs/maps/images/camera/3_define_new_zone.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
docs/maps/images/camera/4_add_zone_type.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
docs/maps/images/camera/5_click_add_property.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
docs/maps/images/camera/6_add_focusable_prop.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
docs/maps/images/camera/7_make_sure_checked.png
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
docs/maps/images/camera/8_add_zoom_margin.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
docs/maps/images/camera/9_optional_zoom_margin_defined.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
docs/maps/images/camera/no_margin.png
Normal file
After Width: | Height: | Size: 153 KiB |
BIN
docs/maps/images/camera/with_margin.png
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
docs/maps/images/mapProperties.png
Normal file
After Width: | Height: | Size: 130 KiB |
@ -51,6 +51,12 @@ return [
|
||||
'markdown' => 'maps.website-in-map',
|
||||
'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/website-in-map.md',
|
||||
],
|
||||
[
|
||||
'title' => 'Camera',
|
||||
'url' => '/map-building/camera.md',
|
||||
'markdown' => 'maps.camera',
|
||||
'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/camera.md',
|
||||
],
|
||||
[
|
||||
'title' => 'Variables',
|
||||
'url' => '/map-building/variables.md',
|
||||
|
@ -60,7 +60,7 @@ WA.chat.sendChatMessage('Hello world', 'Mr Robot');
|
||||
|
||||
The `WA` objects contains a number of useful methods enabling you to interact with the WorkAdventure game. For instance, `WA.chat.sendChatMessage` opens the chat and adds a message in it.
|
||||
|
||||
In your browser console, when you open the map, the chat message should be displayed right away.
|
||||
The message should be displayed in the chat history as soon as you enter the room.
|
||||
|
||||
## Adding a script in an iFrame
|
||||
|
||||
|
@ -92,3 +92,20 @@ You can add properties either on individual tiles of a tileset OR on a complete
|
||||
If you put a property on a layer, it will be triggered if your Woka walks on any tile of the layer.
|
||||
|
||||
The exception is the "collides" property that can only be set on tiles, but not on a complete layer.
|
||||
|
||||
## Insert helpful information in your map
|
||||
|
||||
By setting properties on the map itself, you can help visitors know more about the creators of the map.
|
||||
|
||||
The following *map* properties are supported:
|
||||
* `mapName` (string): The name of your map
|
||||
* `mapLink` (string): A link to your map, for example a repository
|
||||
* `mapDescription` (string): A short description of your map
|
||||
* `mapCopyright` (string): Copyright notice
|
||||
|
||||
Each *tileset* can also have a property called `tilesetCopyright` (string).
|
||||
If you are using audio files in your map, you can declare a layer property `audioCopyright` (string).
|
||||
|
||||
Resulting in a "credit" page in the menu looking like this:
|
||||
|
||||
![](images/mapProperties.png){.document-img}
|
||||
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
module.exports = {
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
@ -18,10 +18,18 @@
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
"project": "./tsconfig.json",
|
||||
"extraFileExtensions": [".svelte"]
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
"@typescript-eslint",
|
||||
"svelte3"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.svelte"],
|
||||
"processor": "svelte3/svelte3"
|
||||
}
|
||||
],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
@ -33,6 +41,11 @@
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/restrict-template-expressions": "off"
|
||||
"@typescript-eslint/restrict-template-expressions": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
},
|
||||
"settings": {
|
||||
"svelte3/typescript": true,
|
||||
"svelte3/ignore-styles": () => true
|
||||
}
|
||||
}
|
@ -1 +1,2 @@
|
||||
src/Messages/generated
|
||||
src/Messages/JsonMessages
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4
|
||||
"tabWidth": 4,
|
||||
"plugins": ["prettier-plugin-svelte"]
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ FROM thecodingmachine/nodejs:14-apache
|
||||
|
||||
COPY --chown=docker:docker front .
|
||||
COPY --from=builder --chown=docker:docker /usr/src/generated /var/www/html/src/Messages/generated
|
||||
COPY --from=builder --chown=docker:docker /usr/src/JsonMessages /var/www/html/src/Messages/JsonMessages
|
||||
|
||||
# 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
|
||||
|
1
front/dist/iframe.html
vendored
@ -11,6 +11,7 @@
|
||||
const scriptUrl = urlParams.get('script');
|
||||
const script = document.createElement('script');
|
||||
script.src = scriptUrl;
|
||||
script.type = "module";
|
||||
document.head.append(script);
|
||||
</script>
|
||||
</head>
|
||||
|
@ -12,11 +12,12 @@
|
||||
"@types/quill": "^1.3.7",
|
||||
"@types/uuidv4": "^5.0.0",
|
||||
"@types/webpack-dev-server": "^3.11.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.23.0",
|
||||
"@typescript-eslint/parser": "^4.23.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"css-loader": "^5.2.4",
|
||||
"eslint": "^7.26.0",
|
||||
"fork-ts-checker-webpack-plugin": "^6.2.9",
|
||||
"eslint": "^8.4.1",
|
||||
"eslint-plugin-svelte3": "^3.2.1",
|
||||
"fork-ts-checker-webpack-plugin": "^6.5.0",
|
||||
"html-webpack-plugin": "^5.3.1",
|
||||
"jasmine": "^3.5.0",
|
||||
"lint-staged": "^11.0.0",
|
||||
@ -24,23 +25,24 @@
|
||||
"node-polyfill-webpack-plugin": "^1.1.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.3.1",
|
||||
"prettier-plugin-svelte": "^2.5.0",
|
||||
"sass": "^1.32.12",
|
||||
"sass-loader": "^11.1.0",
|
||||
"svelte": "^3.38.2",
|
||||
"svelte-check": "^2.1.0",
|
||||
"svelte-loader": "^3.1.1",
|
||||
"svelte-preprocess": "^4.7.3",
|
||||
"ts-loader": "^9.1.2",
|
||||
"ts-node": "^9.1.1",
|
||||
"ts-loader": "^9.2.6",
|
||||
"ts-node": "^10.4.0",
|
||||
"tsconfig-paths": "^3.9.0",
|
||||
"typescript": "^4.2.4",
|
||||
"typescript": "^4.5.3",
|
||||
"webpack": "^5.37.0",
|
||||
"webpack-cli": "^4.7.0",
|
||||
"webpack-dev-server": "^3.11.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/press-start-2p": "^4.3.0",
|
||||
"@joeattardi/emoji-button": "^4.6.0",
|
||||
"@joeattardi/emoji-button": "^4.6.2",
|
||||
"@types/simple-peer": "^9.11.1",
|
||||
"@types/socket.io-client": "^1.4.32",
|
||||
"axios": "^0.21.2",
|
||||
@ -55,6 +57,7 @@
|
||||
"queue-typescript": "^1.0.1",
|
||||
"quill": "1.3.6",
|
||||
"quill-delta-to-html": "^0.12.0",
|
||||
"retry-axios": "^2.6.0",
|
||||
"rxjs": "^6.6.3",
|
||||
"simple-peer": "^9.11.0",
|
||||
"socket.io-client": "^2.3.0",
|
||||
@ -68,17 +71,21 @@
|
||||
"build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack",
|
||||
"build-typings": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production BUILD_TYPINGS=1 webpack",
|
||||
"test": "cross-env TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
|
||||
"lint": "node_modules/.bin/eslint src/ . --ext .ts",
|
||||
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts",
|
||||
"lint": "node_modules/.bin/eslint src/ tests/ --ext .ts,.svelte",
|
||||
"fix": "node_modules/.bin/eslint --fix src/ tests/ --ext .ts,.svelte",
|
||||
"precommit": "lint-staged",
|
||||
"svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\" --watch",
|
||||
"svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\"",
|
||||
"pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'",
|
||||
"pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'"
|
||||
"pretty": "yarn prettier --write 'src/**/*.{ts,svelte}'",
|
||||
"pretty-check": "yarn prettier --check 'src/**/*.{ts,svelte}'"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.ts": [
|
||||
"prettier --write"
|
||||
"*.svelte": [
|
||||
"yarn run svelte-check"
|
||||
],
|
||||
"*.{ts,svelte}": [
|
||||
"yarn run fix",
|
||||
"yarn run pretty"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
11
front/src/Api/Events/ChangeZoneEvent.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isChangeZoneEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
name: tg.isString,
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
* A message sent from the game to the iFrame when a user enters or leaves a zone.
|
||||
*/
|
||||
export type ChangeZoneEvent = tg.GuardedType<typeof isChangeZoneEvent>;
|
13
front/src/Api/Events/ColorEvent.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isColorEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
red: tg.isNumber,
|
||||
green: tg.isNumber,
|
||||
blue: tg.isNumber,
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
* A message sent from the iFrame to the game to dynamically set the outline of the player.
|
||||
*/
|
||||
export type ColorEvent = tg.GuardedType<typeof isColorEvent>;
|
@ -9,6 +9,7 @@ export const isGameStateEvent = new tg.IsInterface()
|
||||
startLayerName: tg.isUnion(tg.isString, tg.isNull),
|
||||
tags: tg.isArray(tg.isString),
|
||||
variables: tg.isObject,
|
||||
userRoomToken: tg.isUnion(tg.isString, tg.isUndefined),
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
|
@ -28,6 +28,8 @@ import type { MessageReferenceEvent } from "./ui/TriggerActionMessageEvent";
|
||||
import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent";
|
||||
import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEvent";
|
||||
import type { ChangeLayerEvent } from "./ChangeLayerEvent";
|
||||
import type { ChangeZoneEvent } from "./ChangeZoneEvent";
|
||||
import { isColorEvent } from "./ColorEvent";
|
||||
|
||||
export interface TypedMessageEvent<T> extends MessageEvent {
|
||||
data: T;
|
||||
@ -76,6 +78,8 @@ export interface IframeResponseEventMap {
|
||||
leaveEvent: EnterLeaveEvent;
|
||||
enterLayerEvent: ChangeLayerEvent;
|
||||
leaveLayerEvent: ChangeLayerEvent;
|
||||
enterZoneEvent: ChangeZoneEvent;
|
||||
leaveZoneEvent: ChangeZoneEvent;
|
||||
buttonClickedEvent: ButtonClickedEvent;
|
||||
hasPlayerMoved: HasPlayerMovedEvent;
|
||||
menuItemClicked: MenuItemClickedEvent;
|
||||
@ -149,6 +153,14 @@ export const iframeQueryMapTypeGuards = {
|
||||
query: isCreateEmbeddedWebsiteEvent,
|
||||
answer: tg.isUndefined,
|
||||
},
|
||||
setPlayerOutline: {
|
||||
query: isColorEvent,
|
||||
answer: tg.isUndefined,
|
||||
},
|
||||
removePlayerOutline: {
|
||||
query: tg.isUndefined,
|
||||
answer: tg.isUndefined,
|
||||
},
|
||||
};
|
||||
|
||||
type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never;
|
||||
|
@ -31,6 +31,7 @@ import type { SetVariableEvent } from "./Events/SetVariableEvent";
|
||||
import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent";
|
||||
import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore";
|
||||
import type { ChangeLayerEvent } from "./Events/ChangeLayerEvent";
|
||||
import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent";
|
||||
|
||||
type AnswererCallback<T extends keyof IframeQueryMap> = (
|
||||
query: IframeQueryMap[T]["query"],
|
||||
@ -311,7 +312,7 @@ class IframeListener {
|
||||
"//" +
|
||||
window.location.host +
|
||||
'/iframe_api.js" ></script>\n' +
|
||||
'<script src="' +
|
||||
'<script type="module" src="' +
|
||||
scriptUrl +
|
||||
'" ></script>\n' +
|
||||
"<title></title>\n" +
|
||||
@ -414,6 +415,24 @@ class IframeListener {
|
||||
});
|
||||
}
|
||||
|
||||
sendEnterZoneEvent(zoneName: string) {
|
||||
this.postMessage({
|
||||
type: "enterZoneEvent",
|
||||
data: {
|
||||
name: zoneName,
|
||||
} as ChangeZoneEvent,
|
||||
});
|
||||
}
|
||||
|
||||
sendLeaveZoneEvent(zoneName: string) {
|
||||
this.postMessage({
|
||||
type: "leaveZoneEvent",
|
||||
data: {
|
||||
name: zoneName,
|
||||
} as ChangeZoneEvent,
|
||||
});
|
||||
}
|
||||
|
||||
hasPlayerMoved(event: HasPlayerMovedEvent) {
|
||||
if (this.sendPlayerMove) {
|
||||
this.postMessage({
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events/HasPlayerMovedEvent";
|
||||
import { Subject } from "rxjs";
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
@ -20,6 +20,12 @@ export const setTags = (_tags: string[]) => {
|
||||
|
||||
let uuid: string | undefined;
|
||||
|
||||
let userRoomToken: string | undefined;
|
||||
|
||||
export const setUserRoomToken = (token: string | undefined) => {
|
||||
userRoomToken = token;
|
||||
};
|
||||
|
||||
export const setUuid = (_uuid: string | undefined) => {
|
||||
uuid = _uuid;
|
||||
};
|
||||
@ -67,6 +73,33 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
|
||||
get userRoomToken(): string | undefined {
|
||||
if (userRoomToken === undefined) {
|
||||
throw new Error(
|
||||
"User-room token not initialized yet. You should call WA.player.userRoomToken within a WA.onInit callback."
|
||||
);
|
||||
}
|
||||
return userRoomToken;
|
||||
}
|
||||
|
||||
public setOutlineColor(red: number, green: number, blue: number): Promise<void> {
|
||||
return queryWorkadventure({
|
||||
type: "setPlayerOutline",
|
||||
data: {
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public removeOutlineColor(): Promise<void> {
|
||||
return queryWorkadventure({
|
||||
type: "removePlayerOutline",
|
||||
data: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new WorkadventurePlayerCommands();
|
||||
|
@ -1,147 +1,146 @@
|
||||
<script lang="typescript">
|
||||
import MenuIcon from "./Menu/MenuIcon.svelte";
|
||||
import {menuIconVisiblilityStore, menuVisiblilityStore} from "../Stores/MenuStore";
|
||||
import {emoteMenuStore} from "../Stores/EmoteStore";
|
||||
import {enableCameraSceneVisibilityStore} from "../Stores/MediaStore";
|
||||
import { menuIconVisiblilityStore, menuVisiblilityStore } from "../Stores/MenuStore";
|
||||
import { emoteMenuStore } from "../Stores/EmoteStore";
|
||||
import { enableCameraSceneVisibilityStore } from "../Stores/MediaStore";
|
||||
import CameraControls from "./CameraControls.svelte";
|
||||
import MyCamera from "./MyCamera.svelte";
|
||||
import SelectCompanionScene from "./SelectCompanion/SelectCompanionScene.svelte";
|
||||
import {selectCompanionSceneVisibleStore} from "../Stores/SelectCompanionStore";
|
||||
import {selectCharacterSceneVisibleStore} from "../Stores/SelectCharacterStore";
|
||||
import { selectCompanionSceneVisibleStore } from "../Stores/SelectCompanionStore";
|
||||
import { selectCharacterSceneVisibleStore } from "../Stores/SelectCharacterStore";
|
||||
import SelectCharacterScene from "./selectCharacter/SelectCharacterScene.svelte";
|
||||
import {customCharacterSceneVisibleStore} from "../Stores/CustomCharacterStore";
|
||||
import {errorStore} from "../Stores/ErrorStore";
|
||||
import { customCharacterSceneVisibleStore } from "../Stores/CustomCharacterStore";
|
||||
import { errorStore } from "../Stores/ErrorStore";
|
||||
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
|
||||
import LoginScene from "./Login/LoginScene.svelte";
|
||||
import Chat from "./Chat/Chat.svelte";
|
||||
import {loginSceneVisibleStore} from "../Stores/LoginSceneStore";
|
||||
import { loginSceneVisibleStore } from "../Stores/LoginSceneStore";
|
||||
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
|
||||
import VisitCard from "./VisitCard/VisitCard.svelte";
|
||||
import {requestVisitCardsStore} from "../Stores/GameStore";
|
||||
import { requestVisitCardsStore } from "../Stores/GameStore";
|
||||
|
||||
import type {Game} from "../Phaser/Game/Game";
|
||||
import {chatVisibilityStore} from "../Stores/ChatStore";
|
||||
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
|
||||
import type { Game } from "../Phaser/Game/Game";
|
||||
import { chatVisibilityStore } from "../Stores/ChatStore";
|
||||
import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
|
||||
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
|
||||
import AudioPlaying from "./UI/AudioPlaying.svelte";
|
||||
import {soundPlayingStore} from "../Stores/SoundPlayingStore";
|
||||
import { soundPlayingStore } from "../Stores/SoundPlayingStore";
|
||||
import ErrorDialog from "./UI/ErrorDialog.svelte";
|
||||
import Menu from "./Menu/Menu.svelte";
|
||||
import EmoteMenu from "./EmoteMenu/EmoteMenu.svelte";
|
||||
import VideoOverlay from "./Video/VideoOverlay.svelte";
|
||||
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility";
|
||||
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
|
||||
import AdminMessage from "./TypeMessage/BanMessage.svelte";
|
||||
import TextMessage from "./TypeMessage/TextMessage.svelte";
|
||||
import {banMessageVisibleStore} from "../Stores/TypeMessageStore/BanMessageStore";
|
||||
import {textMessageVisibleStore} from "../Stores/TypeMessageStore/TextMessageStore";
|
||||
import {warningContainerStore} from "../Stores/MenuStore";
|
||||
import { banMessageVisibleStore } from "../Stores/TypeMessageStore/BanMessageStore";
|
||||
import { textMessageVisibleStore } from "../Stores/TypeMessageStore/TextMessageStore";
|
||||
import { warningContainerStore } from "../Stores/MenuStore";
|
||||
import WarningContainer from "./WarningContainer/WarningContainer.svelte";
|
||||
import {layoutManagerVisibilityStore} from "../Stores/LayoutManagerStore";
|
||||
import { layoutManagerVisibilityStore } from "../Stores/LayoutManagerStore";
|
||||
import LayoutManager from "./LayoutManager/LayoutManager.svelte";
|
||||
import {audioManagerVisibilityStore} from "../Stores/AudioManagerStore";
|
||||
import AudioManager from "./AudioManager/AudioManager.svelte"
|
||||
import { audioManagerVisibilityStore } from "../Stores/AudioManagerStore";
|
||||
import AudioManager from "./AudioManager/AudioManager.svelte";
|
||||
import { showReportScreenStore, userReportEmpty } from "../Stores/ShowReportScreenStore";
|
||||
import ReportMenu from "./ReportMenu/ReportMenu.svelte";
|
||||
|
||||
export let game: Game;
|
||||
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if $loginSceneVisibleStore}
|
||||
<div class="scrollable">
|
||||
<LoginScene game={game}></LoginScene>
|
||||
<LoginScene {game} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $selectCharacterSceneVisibleStore}
|
||||
<div>
|
||||
<SelectCharacterScene game={ game }></SelectCharacterScene>
|
||||
<SelectCharacterScene {game} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $customCharacterSceneVisibleStore}
|
||||
<div>
|
||||
<CustomCharacterScene game={ game }></CustomCharacterScene>
|
||||
<CustomCharacterScene {game} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $selectCompanionSceneVisibleStore}
|
||||
<div>
|
||||
<SelectCompanionScene game={ game }></SelectCompanionScene>
|
||||
<SelectCompanionScene {game} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $enableCameraSceneVisibilityStore}
|
||||
<div class="scrollable">
|
||||
<EnableCameraScene game={game}></EnableCameraScene>
|
||||
<EnableCameraScene {game} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $banMessageVisibleStore}
|
||||
<div>
|
||||
<AdminMessage></AdminMessage>
|
||||
<AdminMessage />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $textMessageVisibleStore}
|
||||
<div>
|
||||
<TextMessage></TextMessage>
|
||||
<TextMessage />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $soundPlayingStore}
|
||||
<div>
|
||||
<AudioPlaying url={$soundPlayingStore} />
|
||||
</div>
|
||||
<div>
|
||||
<AudioPlaying url={$soundPlayingStore} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $audioManagerVisibilityStore}
|
||||
<div>
|
||||
<AudioManager></AudioManager>
|
||||
</div>
|
||||
<div>
|
||||
<AudioManager />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $layoutManagerVisibilityStore}
|
||||
<div>
|
||||
<LayoutManager></LayoutManager>
|
||||
<LayoutManager />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $showReportScreenStore !== userReportEmpty}
|
||||
<div>
|
||||
<ReportMenu></ReportMenu>
|
||||
<ReportMenu />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $menuIconVisiblilityStore}
|
||||
<div>
|
||||
<MenuIcon></MenuIcon>
|
||||
<MenuIcon />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $menuVisiblilityStore}
|
||||
<div>
|
||||
<Menu></Menu>
|
||||
<Menu />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $emoteMenuStore}
|
||||
<div>
|
||||
<EmoteMenu></EmoteMenu>
|
||||
<EmoteMenu />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $gameOverlayVisibilityStore}
|
||||
<div>
|
||||
<VideoOverlay></VideoOverlay>
|
||||
<MyCamera></MyCamera>
|
||||
<CameraControls></CameraControls>
|
||||
<VideoOverlay />
|
||||
<MyCamera />
|
||||
<CameraControls />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $helpCameraSettingsVisibleStore}
|
||||
<div>
|
||||
<HelpCameraSettingsPopup></HelpCameraSettingsPopup>
|
||||
<HelpCameraSettingsPopup />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $requestVisitCardsStore}
|
||||
<VisitCard visitCardUrl={$requestVisitCardsStore}></VisitCard>
|
||||
<VisitCard visitCardUrl={$requestVisitCardsStore} />
|
||||
{/if}
|
||||
{#if $errorStore.length > 0}
|
||||
<div>
|
||||
<ErrorDialog></ErrorDialog>
|
||||
</div>
|
||||
<div>
|
||||
<ErrorDialog />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $chatVisibilityStore}
|
||||
<Chat></Chat>
|
||||
<Chat />
|
||||
{/if}
|
||||
{#if $warningContainerStore}
|
||||
<WarningContainer></WarningContainer>
|
||||
<WarningContainer />
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -1,10 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { localUserStore } from "../../Connexion/LocalUserStore";
|
||||
import type { audioManagerVolume } from "../../Stores/AudioManagerStore";
|
||||
import {
|
||||
audioManagerFileStore,
|
||||
audioManagerVolumeStore,
|
||||
} from "../../Stores/AudioManagerStore";
|
||||
import { audioManagerFileStore, audioManagerVolumeStore } from "../../Stores/AudioManagerStore";
|
||||
import { get } from "svelte/store";
|
||||
import type { Unsubscriber } from "svelte/store";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
@ -15,15 +12,12 @@
|
||||
let unsubscriberFileStore: Unsubscriber | null = null;
|
||||
let unsubscriberVolumeStore: Unsubscriber | null = null;
|
||||
|
||||
let volume: number = 1;
|
||||
let decreaseWhileTalking: boolean = true;
|
||||
|
||||
onMount(() => {
|
||||
volume = localUserStore.getAudioPlayerVolume();
|
||||
let volume = Math.min(localUserStore.getAudioPlayerVolume(), get(audioManagerVolumeStore).volume);
|
||||
audioManagerVolumeStore.setVolume(volume);
|
||||
audioManagerVolumeStore.setMuted(localUserStore.getAudioPlayerMuted());
|
||||
changeVolume();
|
||||
|
||||
loadAudioSettings();
|
||||
|
||||
unsubscriberFileStore = audioManagerFileStore.subscribe(() => {
|
||||
HTMLAudioPlayer.pause();
|
||||
@ -43,8 +37,9 @@
|
||||
HTMLAudioPlayer.volume = audioManager.volume;
|
||||
HTMLAudioPlayer.muted = audioManager.muted;
|
||||
HTMLAudioPlayer.loop = audioManager.loop;
|
||||
})
|
||||
})
|
||||
updateVolumeUI();
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (unsubscriberFileStore) {
|
||||
@ -53,23 +48,24 @@
|
||||
if (unsubscriberVolumeStore) {
|
||||
unsubscriberVolumeStore();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
function changeVolume() {
|
||||
function updateVolumeUI() {
|
||||
if (get(audioManagerVolumeStore).muted) {
|
||||
audioPlayerVolumeIcon.classList.add('muted');
|
||||
audioPlayerVolumeIcon.classList.add("muted");
|
||||
audioPlayerVol.value = "0";
|
||||
} else {
|
||||
let volume = HTMLAudioPlayer.volume;
|
||||
audioPlayerVol.value = "" + volume;
|
||||
audioPlayerVolumeIcon.classList.remove('muted');
|
||||
audioPlayerVolumeIcon.classList.remove("muted");
|
||||
if (volume < 0.3) {
|
||||
audioPlayerVolumeIcon.classList.add('low');
|
||||
audioPlayerVolumeIcon.classList.add("low");
|
||||
} else if (volume < 0.7) {
|
||||
audioPlayerVolumeIcon.classList.remove('low');
|
||||
audioPlayerVolumeIcon.classList.add('mid');
|
||||
audioPlayerVolumeIcon.classList.remove("low");
|
||||
audioPlayerVolumeIcon.classList.add("mid");
|
||||
} else {
|
||||
audioPlayerVolumeIcon.classList.remove('low');
|
||||
audioPlayerVolumeIcon.classList.remove('mid');
|
||||
audioPlayerVolumeIcon.classList.remove("low");
|
||||
audioPlayerVolumeIcon.classList.remove("mid");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -78,21 +74,14 @@
|
||||
const muted = !get(audioManagerVolumeStore).muted;
|
||||
audioManagerVolumeStore.setMuted(muted);
|
||||
localUserStore.setAudioPlayerMuted(muted);
|
||||
changeVolume();
|
||||
}
|
||||
|
||||
function loadAudioSettings() {
|
||||
audioManagerVolumeStore.setVolume(localUserStore.getAudioPlayerVolume());
|
||||
audioManagerVolumeStore.setMuted(localUserStore.getAudioPlayerMuted());
|
||||
}
|
||||
|
||||
function setVolume() {
|
||||
volume = parseFloat(audioPlayerVol.value);
|
||||
let volume = parseFloat(audioPlayerVol.value);
|
||||
audioManagerVolumeStore.setVolume(volume);
|
||||
localUserStore.setAudioPlayerVolume(volume);
|
||||
audioManagerVolumeStore.setMuted(false);
|
||||
localUserStore.setAudioPlayerMuted(false);
|
||||
changeVolume();
|
||||
}
|
||||
|
||||
function disallowKeys() {
|
||||
@ -105,45 +94,67 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div class="main-audio-manager nes-container is-rounded">
|
||||
<div class="audio-manager-player-volume">
|
||||
<span id="audioplayer_volume_icon_playing" alt="player volume" bind:this={audioPlayerVolumeIcon}
|
||||
on:click={onMute}>
|
||||
<svg width="2em" height="2em" viewBox="0 0 16 16" class="bi bi-volume-up" fill="white"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd"
|
||||
d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04z" />
|
||||
<span
|
||||
id="audioplayer_volume_icon_playing"
|
||||
alt="player volume"
|
||||
bind:this={audioPlayerVolumeIcon}
|
||||
on:click={onMute}
|
||||
>
|
||||
<svg
|
||||
width="2em"
|
||||
height="2em"
|
||||
viewBox="0 0 16 16"
|
||||
class="bi bi-volume-up"
|
||||
fill="white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04z"
|
||||
/>
|
||||
<g id="audioplayer_volume_icon_playing_high">
|
||||
<path
|
||||
d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z" />
|
||||
d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z"
|
||||
/>
|
||||
</g>
|
||||
<g id="audioplayer_volume_icon_playing_mid">
|
||||
<path
|
||||
d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z" />
|
||||
d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z"
|
||||
/>
|
||||
</g>
|
||||
<g id="audioplayer_volume_icon_playing_low">
|
||||
<path
|
||||
d="M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707z" />
|
||||
d="M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
<input type="range" min="0" max="1" step="0.025" bind:this={audioPlayerVol} on:change={setVolume} on:keydown={disallowKeys}>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.025"
|
||||
bind:this={audioPlayerVol}
|
||||
on:change={setVolume}
|
||||
on:keydown={disallowKeys}
|
||||
/>
|
||||
</div>
|
||||
<div class="audio-manager-reduce-conversation">
|
||||
<label>
|
||||
reduce in conversations
|
||||
<input type="checkbox" bind:checked={decreaseWhileTalking} on:change={setDecrease}>
|
||||
<input type="checkbox" bind:checked={decreaseWhileTalking} on:change={setDecrease} />
|
||||
</label>
|
||||
<section class="audio-manager-file">
|
||||
<!-- svelte-ignore a11y-media-has-caption -->
|
||||
<audio class="audio-manager-audioplayer" bind:this={HTMLAudioPlayer}>
|
||||
<source src={$audioManagerFileStore}>
|
||||
<source src={$audioManagerFileStore} />
|
||||
</audio>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
div.main-audio-manager.nes-container.is-rounded {
|
||||
position: relative;
|
||||
@ -208,4 +219,4 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="typescript">
|
||||
import {requestedScreenSharingState, screenSharingAvailableStore} from "../Stores/ScreenSharingStore";
|
||||
import {isSilentStore, requestedCameraState, requestedMicrophoneState} from "../Stores/MediaStore";
|
||||
import { requestedScreenSharingState, screenSharingAvailableStore } from "../Stores/ScreenSharingStore";
|
||||
import { isSilentStore, requestedCameraState, requestedMicrophoneState } from "../Stores/MediaStore";
|
||||
import monitorImg from "./images/monitor.svg";
|
||||
import monitorCloseImg from "./images/monitor-close.svg";
|
||||
import cinemaImg from "./images/cinema.svg";
|
||||
@ -9,10 +9,10 @@
|
||||
import microphoneCloseImg from "./images/microphone-close.svg";
|
||||
import layoutPresentationImg from "./images/layout-presentation.svg";
|
||||
import layoutChatImg from "./images/layout-chat.svg";
|
||||
import {layoutModeStore} from "../Stores/StreamableCollectionStore";
|
||||
import {LayoutMode} from "../WebRtc/LayoutManager";
|
||||
import {peerStore} from "../Stores/PeerStore";
|
||||
import {onDestroy} from "svelte";
|
||||
import { layoutModeStore } from "../Stores/StreamableCollectionStore";
|
||||
import { LayoutMode } from "../WebRtc/LayoutManager";
|
||||
import { peerStore } from "../Stores/PeerStore";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
function screenSharingClick(): void {
|
||||
if (isSilent) return;
|
||||
@ -50,7 +50,7 @@
|
||||
}
|
||||
|
||||
let isSilent: boolean;
|
||||
const unsubscribeIsSilent = isSilentStore.subscribe(value => {
|
||||
const unsubscribeIsSilent = isSilentStore.subscribe((value) => {
|
||||
isSilent = value;
|
||||
});
|
||||
onDestroy(unsubscribeIsSilent);
|
||||
@ -59,31 +59,36 @@
|
||||
<div>
|
||||
<div class="btn-cam-action">
|
||||
<div class="btn-layout" on:click={switchLayoutMode} class:hide={$peerStore.size === 0}>
|
||||
{#if $layoutModeStore === LayoutMode.Presentation }
|
||||
<img src={layoutPresentationImg} style="padding: 2px" alt="Switch to mosaic mode">
|
||||
{#if $layoutModeStore === LayoutMode.Presentation}
|
||||
<img src={layoutPresentationImg} style="padding: 2px" alt="Switch to mosaic mode" />
|
||||
{:else}
|
||||
<img src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode">
|
||||
<img src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="btn-monitor" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore || isSilent} class:enabled={$requestedScreenSharingState}>
|
||||
<div
|
||||
class="btn-monitor"
|
||||
on:click={screenSharingClick}
|
||||
class:hide={!$screenSharingAvailableStore || isSilent}
|
||||
class:enabled={$requestedScreenSharingState}
|
||||
>
|
||||
{#if $requestedScreenSharingState && !isSilent}
|
||||
<img src={monitorImg} alt="Start screen sharing">
|
||||
<img src={monitorImg} alt="Start screen sharing" />
|
||||
{:else}
|
||||
<img src={monitorCloseImg} alt="Stop screen sharing">
|
||||
<img src={monitorCloseImg} alt="Stop screen sharing" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="btn-video" on:click={cameraClick} class:disabled={!$requestedCameraState || isSilent}>
|
||||
{#if $requestedCameraState && !isSilent}
|
||||
<img src={cinemaImg} alt="Turn on webcam">
|
||||
<img src={cinemaImg} alt="Turn on webcam" />
|
||||
{:else}
|
||||
<img src={cinemaCloseImg} alt="Turn off webcam">
|
||||
<img src={cinemaCloseImg} alt="Turn off webcam" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="btn-micro" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState || isSilent}>
|
||||
{#if $requestedMicrophoneState && !isSilent}
|
||||
<img src={microphoneImg} alt="Turn on microphone">
|
||||
<img src={microphoneImg} alt="Turn on microphone" />
|
||||
{:else}
|
||||
<img src={microphoneCloseImg} alt="Turn off microphone">
|
||||
<img src={microphoneCloseImg} alt="Turn off microphone" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,23 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition';
|
||||
import { fly } from "svelte/transition";
|
||||
import { chatMessagesStore, chatVisibilityStore } from "../../Stores/ChatStore";
|
||||
import ChatMessageForm from './ChatMessageForm.svelte';
|
||||
import ChatElement from './ChatElement.svelte';
|
||||
import {afterUpdate, beforeUpdate, onMount} from "svelte";
|
||||
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
|
||||
|
||||
import ChatMessageForm from "./ChatMessageForm.svelte";
|
||||
import ChatElement from "./ChatElement.svelte";
|
||||
import { afterUpdate, beforeUpdate, onMount } from "svelte";
|
||||
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
|
||||
|
||||
let listDom: HTMLElement;
|
||||
let chatWindowElement: HTMLElement;
|
||||
let handleFormBlur: { blur():void };
|
||||
let handleFormBlur: { blur(): void };
|
||||
let autoscroll: boolean;
|
||||
|
||||
beforeUpdate(() => {
|
||||
autoscroll = listDom && (listDom.offsetHeight + listDom.scrollTop) > (listDom.scrollHeight - 20);
|
||||
autoscroll = listDom && listDom.offsetHeight + listDom.scrollTop > listDom.scrollHeight - 20;
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
listDom.scrollTo(0, listDom.scrollHeight);
|
||||
})
|
||||
});
|
||||
|
||||
afterUpdate(() => {
|
||||
if (autoscroll) listDom.scrollTo(0, listDom.scrollHeight);
|
||||
@ -32,83 +32,82 @@
|
||||
function closeChat() {
|
||||
chatVisibilityStore.set(false);
|
||||
}
|
||||
function onKeyDown(e:KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
closeChat();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} on:click={onClick}/>
|
||||
<svelte:window on:keydown={onKeyDown} on:click={onClick} />
|
||||
|
||||
|
||||
<aside class="chatWindow" 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}>×</p>
|
||||
<section class="messagesList" bind:this={listDom}>
|
||||
<ul>
|
||||
<li><p class="system-text">Here is your chat history: </p></li>
|
||||
{#each $chatMessagesStore as message, i}
|
||||
<li><ChatElement message={message} line={i}></ChatElement></li>
|
||||
{/each}
|
||||
<li><p class="system-text">Here is your chat history:</p></li>
|
||||
{#each $chatMessagesStore as message, i}
|
||||
<li><ChatElement {message} line={i} /></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
<section class="messageForm">
|
||||
<ChatMessageForm bind:handleForm={handleFormBlur}></ChatMessageForm>
|
||||
<ChatMessageForm bind:handleForm={handleFormBlur} />
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<style lang="scss">
|
||||
p.close-icon {
|
||||
position: absolute;
|
||||
padding: 4px;
|
||||
right: 12px;
|
||||
font-size: 30px;
|
||||
line-height: 25px;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
padding: 4px;
|
||||
right: 12px;
|
||||
font-size: 30px;
|
||||
line-height: 25px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
p.system-text {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
padding:6px;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 100%;
|
||||
background: gray;
|
||||
display: inline-block;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
padding: 6px;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 100%;
|
||||
background: gray;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
aside.chatWindow {
|
||||
z-index:100;
|
||||
pointer-events: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width:30vw;
|
||||
min-width: 350px;
|
||||
background: rgb(5, 31, 51, 0.9);
|
||||
color: whitesmoke;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 100;
|
||||
pointer-events: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width: 30vw;
|
||||
min-width: 350px;
|
||||
background: rgb(5, 31, 51, 0.9);
|
||||
color: whitesmoke;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
border-bottom-right-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
|
||||
.messagesList {
|
||||
margin-top: 35px;
|
||||
overflow-y: auto;
|
||||
flex: auto;
|
||||
padding: 10px;
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
border-bottom-right-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
|
||||
.messagesList {
|
||||
margin-top: 35px;
|
||||
overflow-y: auto;
|
||||
flex: auto;
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
.messageForm {
|
||||
flex: 0 70px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
}
|
||||
.messageForm {
|
||||
flex: 0 70px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1,45 +1,53 @@
|
||||
<script lang="ts">
|
||||
import {ChatMessageTypes} from "../../Stores/ChatStore";
|
||||
import type {ChatMessage} from "../../Stores/ChatStore";
|
||||
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
|
||||
import ChatPlayerName from './ChatPlayerName.svelte';
|
||||
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
|
||||
import { ChatMessageTypes } from "../../Stores/ChatStore";
|
||||
import type { ChatMessage } from "../../Stores/ChatStore";
|
||||
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
|
||||
import ChatPlayerName from "./ChatPlayerName.svelte";
|
||||
import type { PlayerInterface } from "../../Phaser/Game/PlayerInterface";
|
||||
|
||||
export let message: ChatMessage;
|
||||
export let line: number;
|
||||
const chatStyleLink = "color: white; text-decoration: underline;";
|
||||
|
||||
|
||||
$: author = message.author as PlayerInterface;
|
||||
$: targets = message.targets || [];
|
||||
$: texts = message.text || [];
|
||||
|
||||
|
||||
function urlifyText(text: string): string {
|
||||
return HtmlUtils.urlify(text, chatStyleLink);
|
||||
}
|
||||
function renderDate(date: Date) {
|
||||
return date.toLocaleTimeString(navigator.language, {
|
||||
hour: '2-digit',
|
||||
minute:'2-digit'
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
function isLastIteration(index: number) {
|
||||
return targets.length -1 === index;
|
||||
return targets.length - 1 === index;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="chatElement">
|
||||
<div class="messagePart">
|
||||
{#if message.type === ChatMessageTypes.userIncoming}
|
||||
>> {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} entered <span class="date">({renderDate(message.date)})</span>
|
||||
>> {#each targets as target, index}<ChatPlayerName
|
||||
player={target}
|
||||
{line}
|
||||
/>{#if !isLastIteration(index)}, {/if}{/each} entered
|
||||
<span class="date">({renderDate(message.date)})</span>
|
||||
{:else if message.type === ChatMessageTypes.userOutcoming}
|
||||
<< {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} left <span class="date">({renderDate(message.date)})</span>
|
||||
<< {#each targets as target, index}<ChatPlayerName
|
||||
player={target}
|
||||
{line}
|
||||
/>{#if !isLastIteration(index)}, {/if}{/each} left
|
||||
<span class="date">({renderDate(message.date)})</span>
|
||||
{:else if message.type === ChatMessageTypes.me}
|
||||
<h4>Me: <span class="date">({renderDate(message.date)})</span></h4>
|
||||
{#each texts as text}
|
||||
<div><p class="my-text">{@html urlifyText(text)}</p></div>
|
||||
{/each}
|
||||
{:else}
|
||||
<h4><ChatPlayerName player={author} line={line}></ChatPlayerName>: <span class="date">({renderDate(message.date)})</span></h4>
|
||||
<h4><ChatPlayerName player={author} {line} />: <span class="date">({renderDate(message.date)})</span></h4>
|
||||
{#each texts as text}
|
||||
<div><p class="other-text">{@html urlifyText(text)}</p></div>
|
||||
{/each}
|
||||
@ -48,37 +56,38 @@
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
h4, p {
|
||||
h4,
|
||||
p {
|
||||
font-family: Lato;
|
||||
}
|
||||
div.chatElement {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.messagePart {
|
||||
flex-grow:1;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
|
||||
span.date {
|
||||
font-size: 80%;
|
||||
color: gray;
|
||||
}
|
||||
.messagePart {
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
|
||||
div > p {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
padding:6px;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
&.other-text {
|
||||
background: gray;
|
||||
}
|
||||
|
||||
&.my-text {
|
||||
background: #6489ff;
|
||||
}
|
||||
span.date {
|
||||
font-size: 80%;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
div > p {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
padding: 6px;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
&.other-text {
|
||||
background: gray;
|
||||
}
|
||||
|
||||
&.my-text {
|
||||
background: #6489ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1,14 +1,14 @@
|
||||
<script lang="ts">
|
||||
import {chatMessagesStore, chatInputFocusStore} from "../../Stores/ChatStore";
|
||||
import { chatMessagesStore, chatInputFocusStore } from "../../Stores/ChatStore";
|
||||
|
||||
export const handleForm = {
|
||||
blur() {
|
||||
inputElement.blur();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
let inputElement: HTMLElement;
|
||||
let newMessageText = '';
|
||||
|
||||
let newMessageText = "";
|
||||
|
||||
function onFocus() {
|
||||
chatInputFocusStore.set(true);
|
||||
}
|
||||
@ -19,14 +19,21 @@
|
||||
function saveMessage() {
|
||||
if (!newMessageText) return;
|
||||
chatMessagesStore.addPersonnalMessage(newMessageText);
|
||||
newMessageText = '';
|
||||
newMessageText = "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={saveMessage}>
|
||||
<input type="text" bind:value={newMessageText} placeholder="Enter your message..." on:focus={onFocus} on:blur={onBlur} bind:this={inputElement}>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newMessageText}
|
||||
placeholder="Enter your message..."
|
||||
on:focus={onFocus}
|
||||
on:blur={onBlur}
|
||||
bind:this={inputElement}
|
||||
/>
|
||||
<button type="submit">
|
||||
<img src="/static/images/send.png" alt="Send" width="20">
|
||||
<img src="/static/images/send.png" alt="Send" width="20" />
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@ -35,28 +42,28 @@
|
||||
display: flex;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
|
||||
|
||||
input {
|
||||
flex: auto;
|
||||
background-color: #254560;
|
||||
color: white;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-top-left-radius: 4px;
|
||||
border: none;
|
||||
font-size: 22px;
|
||||
font-family: Lato;
|
||||
padding-left: 6px;
|
||||
min-width: 0; //Needed so that the input doesn't overflow the container in firefox
|
||||
outline: none;
|
||||
flex: auto;
|
||||
background-color: #254560;
|
||||
color: white;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-top-left-radius: 4px;
|
||||
border: none;
|
||||
font-size: 22px;
|
||||
font-family: Lato;
|
||||
padding-left: 6px;
|
||||
min-width: 0; //Needed so that the input doesn't overflow the container in firefox
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
button {
|
||||
background-color: #254560;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
border: none;
|
||||
border-left: solid white 1px;
|
||||
font-size: 16px;
|
||||
background-color: #254560;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
border: none;
|
||||
border-left: solid white 1px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
|
||||
import {chatSubMenuVisibilityStore} from "../../Stores/ChatStore";
|
||||
import {onDestroy, onMount} from "svelte";
|
||||
import type {Unsubscriber} from "svelte/store";
|
||||
import type { PlayerInterface } from "../../Phaser/Game/PlayerInterface";
|
||||
import { chatSubMenuVisibilityStore } from "../../Stores/ChatStore";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import type { Unsubscriber } from "svelte/store";
|
||||
import ChatSubMenu from "./ChatSubMenu.svelte";
|
||||
|
||||
export let player: PlayerInterface;
|
||||
@ -17,35 +17,33 @@
|
||||
|
||||
onMount(() => {
|
||||
chatSubMenuVisivilytUnsubcribe = chatSubMenuVisibilityStore.subscribe((newValue) => {
|
||||
isSubMenuOpen = (newValue === player.name + line);
|
||||
})
|
||||
})
|
||||
isSubMenuOpen = newValue === player.name + line;
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
chatSubMenuVisivilytUnsubcribe();
|
||||
})
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<span class="subMenu">
|
||||
<span class="chatPlayerName" style="color: {player.color || 'white'}" on:click={openSubMenu}>
|
||||
{player.name}
|
||||
{player.name}
|
||||
</span>
|
||||
{#if isSubMenuOpen}
|
||||
<ChatSubMenu player={player}/>
|
||||
<ChatSubMenu {player} />
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
span.subMenu {
|
||||
display: inline-block;
|
||||
}
|
||||
span.chatPlayerName {
|
||||
margin-left: 3px;
|
||||
}
|
||||
.chatPlayerName:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
span.subMenu {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
span.chatPlayerName {
|
||||
margin-left: 3px;
|
||||
}
|
||||
.chatPlayerName:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,10 +1,9 @@
|
||||
<script lang="ts">
|
||||
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
|
||||
import {requestVisitCardsStore} from "../../Stores/GameStore";
|
||||
import type { PlayerInterface } from "../../Phaser/Game/PlayerInterface";
|
||||
import { requestVisitCardsStore } from "../../Stores/GameStore";
|
||||
|
||||
export let player: PlayerInterface;
|
||||
|
||||
|
||||
function openVisitCard() {
|
||||
if (player.visitCardUrl) {
|
||||
requestVisitCardsStore.set(player.visitCardUrl);
|
||||
@ -17,17 +16,16 @@
|
||||
<li><button class="text-btn" disabled>Add friend</button></li>
|
||||
</ul>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
ul.selectMenu {
|
||||
background-color: whitesmoke;
|
||||
position: absolute;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
list-style-type: none;
|
||||
ul.selectMenu {
|
||||
background-color: whitesmoke;
|
||||
position: absolute;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
list-style-type: none;
|
||||
|
||||
li {
|
||||
text-align: center;
|
||||
li {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
50
front/src/Components/Companion/Companion.svelte
Normal file
@ -0,0 +1,50 @@
|
||||
<script lang="typescript">
|
||||
import { gameManager } from "../../Phaser/Game/GameManager";
|
||||
import type { PictureStore } from "../../Stores/PictureStore";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
export let userId: number;
|
||||
export let placeholderSrc: string;
|
||||
export let width: string = "62px";
|
||||
export let height: string = "62px";
|
||||
|
||||
const gameScene = gameManager.getCurrentGameScene();
|
||||
let companionWokaPictureStore: PictureStore | undefined;
|
||||
if (userId === -1) {
|
||||
companionWokaPictureStore = gameScene.CurrentPlayer.companion?.pictureStore;
|
||||
} else {
|
||||
companionWokaPictureStore = gameScene.MapPlayersByKey.getNestedStore(
|
||||
userId,
|
||||
(item) => item.companion?.pictureStore
|
||||
);
|
||||
}
|
||||
|
||||
let src = placeholderSrc;
|
||||
|
||||
if (companionWokaPictureStore) {
|
||||
const unsubscribe = companionWokaPictureStore.subscribe((source) => {
|
||||
src = source ?? placeholderSrc;
|
||||
});
|
||||
|
||||
onDestroy(unsubscribe);
|
||||
}
|
||||
</script>
|
||||
|
||||
<img {src} alt="" class="nes-pointer" style="--theme-width: {width}; --theme-height: {height}" />
|
||||
|
||||
<style>
|
||||
img {
|
||||
display: inline-block;
|
||||
pointer-events: auto;
|
||||
width: var(--theme-width);
|
||||
height: var(--theme-height);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: static;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
</style>
|
@ -1,7 +1,7 @@
|
||||
<script lang="typescript">
|
||||
import type { Game } from "../../Phaser/Game/Game";
|
||||
import {CustomizeScene, CustomizeSceneName} from "../../Phaser/Login/CustomizeScene";
|
||||
import {activeRowStore} from "../../Stores/CustomCharacterStore";
|
||||
import { CustomizeScene, CustomizeSceneName } from "../../Phaser/Login/CustomizeScene";
|
||||
import { activeRowStore } from "../../Stores/CustomCharacterStore";
|
||||
|
||||
export let game: Game;
|
||||
|
||||
@ -30,7 +30,6 @@
|
||||
function finish() {
|
||||
customCharacterScene.nextSceneToCamera();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<form class="customCharacterScene">
|
||||
@ -38,80 +37,100 @@
|
||||
<h2>Customize your WOKA</h2>
|
||||
</section>
|
||||
<section class="action action-move">
|
||||
<button class="customCharacterSceneButton customCharacterSceneButtonLeft nes-btn" on:click|preventDefault={ selectLeft }> < </button>
|
||||
<button class="customCharacterSceneButton customCharacterSceneButtonRight nes-btn" on:click|preventDefault={ selectRight }> > </button>
|
||||
<button
|
||||
class="customCharacterSceneButton customCharacterSceneButtonLeft nes-btn"
|
||||
on:click|preventDefault={selectLeft}
|
||||
>
|
||||
<
|
||||
</button>
|
||||
<button
|
||||
class="customCharacterSceneButton customCharacterSceneButtonRight nes-btn"
|
||||
on:click|preventDefault={selectRight}
|
||||
>
|
||||
>
|
||||
</button>
|
||||
</section>
|
||||
<section class="action">
|
||||
{#if $activeRowStore === 0}
|
||||
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={ previousScene }>Return</button>
|
||||
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={previousScene}
|
||||
>Return</button
|
||||
>
|
||||
{/if}
|
||||
{#if $activeRowStore !== 0}
|
||||
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={ selectUp }>Back <img src="resources/objects/arrow_up_black.png" alt=""/></button>
|
||||
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={selectUp}
|
||||
>Back <img src="resources/objects/arrow_up_black.png" alt="" /></button
|
||||
>
|
||||
{/if}
|
||||
{#if $activeRowStore === 5}
|
||||
<button type="submit" class="customCharacterSceneFormSubmit nes-btn is-primary" on:click|preventDefault={ finish }>Finish</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="customCharacterSceneFormSubmit nes-btn is-primary"
|
||||
on:click|preventDefault={finish}>Finish</button
|
||||
>
|
||||
{/if}
|
||||
{#if $activeRowStore !== 5}
|
||||
<button type="submit" class="customCharacterSceneFormSubmit nes-btn is-primary" on:click|preventDefault={ selectDown }>Next <img src="resources/objects/arrow_down.png" alt=""/></button>
|
||||
<button
|
||||
type="submit"
|
||||
class="customCharacterSceneFormSubmit nes-btn is-primary"
|
||||
on:click|preventDefault={selectDown}>Next <img src="resources/objects/arrow_down.png" alt="" /></button
|
||||
>
|
||||
{/if}
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<style lang="scss">
|
||||
form.customCharacterScene {
|
||||
font-family: "Press Start 2P";
|
||||
pointer-events: auto;
|
||||
color: #ebeeee;
|
||||
|
||||
section {
|
||||
margin: 10px;
|
||||
|
||||
&.action {
|
||||
text-align: center;
|
||||
margin-top: 55vh;
|
||||
}
|
||||
|
||||
h2 {
|
||||
form.customCharacterScene {
|
||||
font-family: "Press Start 2P";
|
||||
margin: 1px;
|
||||
}
|
||||
pointer-events: auto;
|
||||
color: #ebeeee;
|
||||
|
||||
&.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
section {
|
||||
margin: 10px;
|
||||
|
||||
button.customCharacterSceneButton {
|
||||
position: absolute;
|
||||
top: 33vh;
|
||||
margin: 0;
|
||||
}
|
||||
&.action {
|
||||
text-align: center;
|
||||
margin-top: 55vh;
|
||||
}
|
||||
|
||||
button.customCharacterSceneFormBack {
|
||||
color: #292929;
|
||||
}
|
||||
h2 {
|
||||
font-family: "Press Start 2P";
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
&.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button.customCharacterSceneButton {
|
||||
position: absolute;
|
||||
top: 33vh;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button.customCharacterSceneFormBack {
|
||||
color: #292929;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: "Press Start 2P";
|
||||
|
||||
&.customCharacterSceneButtonLeft {
|
||||
left: 33vw;
|
||||
}
|
||||
|
||||
&.customCharacterSceneButtonRight {
|
||||
right: 33vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: "Press Start 2P";
|
||||
|
||||
&.customCharacterSceneButtonLeft {
|
||||
left: 33vw;
|
||||
}
|
||||
|
||||
&.customCharacterSceneButtonRight {
|
||||
right: 33vw;
|
||||
}
|
||||
@media only screen and (max-width: 800px) {
|
||||
form.customCharacterScene button.customCharacterSceneButtonLeft {
|
||||
left: 5vw;
|
||||
}
|
||||
form.customCharacterScene button.customCharacterSceneButtonRight {
|
||||
right: 5vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
form.customCharacterScene button.customCharacterSceneButtonLeft{
|
||||
left: 5vw;
|
||||
}
|
||||
form.customCharacterScene button.customCharacterSceneButtonRight{
|
||||
right: 5vw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
@ -1,75 +1,72 @@
|
||||
<script lang="typescript">
|
||||
import type { Unsubscriber } from "svelte/store";
|
||||
import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { EmojiButton } from "@joeattardi/emoji-button";
|
||||
import { isMobile } from "../../Enum/EnvironmentVariable";
|
||||
|
||||
import type { Unsubscriber } from "svelte/store";
|
||||
import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { EmojiButton } from '@joeattardi/emoji-button';
|
||||
import { isMobile } from "../../Enum/EnvironmentVariable";
|
||||
let emojiContainer: HTMLElement;
|
||||
let picker: EmojiButton;
|
||||
|
||||
let emojiContainer: HTMLElement;
|
||||
let picker: EmojiButton;
|
||||
let unsubscriber: Unsubscriber | null = null;
|
||||
|
||||
let unsubscriber: Unsubscriber | null = null;
|
||||
onMount(() => {
|
||||
picker = new EmojiButton({
|
||||
rootElement: emojiContainer,
|
||||
styleProperties: {
|
||||
"--font": "Press Start 2P",
|
||||
},
|
||||
emojisPerRow: isMobile() ? 6 : 8,
|
||||
autoFocusSearch: false,
|
||||
style: "twemoji",
|
||||
});
|
||||
//the timeout is here to prevent the menu from flashing
|
||||
setTimeout(() => picker.showPicker(emojiContainer), 100);
|
||||
|
||||
onMount(() => {
|
||||
picker = new EmojiButton({
|
||||
rootElement: emojiContainer,
|
||||
styleProperties: {
|
||||
'--font': 'Press Start 2P'
|
||||
},
|
||||
emojisPerRow: isMobile() ? 6 : 8,
|
||||
autoFocusSearch: false,
|
||||
style: 'twemoji',
|
||||
});
|
||||
//the timeout is here to prevent the menu from flashing
|
||||
setTimeout(() => picker.showPicker(emojiContainer), 100);
|
||||
picker.on("emoji", (selection) => {
|
||||
emoteStore.set({
|
||||
unicode: selection.emoji,
|
||||
url: selection.url,
|
||||
name: selection.name,
|
||||
});
|
||||
});
|
||||
|
||||
picker.on("emoji", (selection) => {
|
||||
emoteStore.set({
|
||||
unicode: selection.emoji,
|
||||
url: selection.url,
|
||||
name: selection.name
|
||||
});
|
||||
picker.on("hidden", () => {
|
||||
emoteMenuStore.closeEmoteMenu();
|
||||
});
|
||||
});
|
||||
|
||||
picker.on("hidden", () => {
|
||||
emoteMenuStore.closeEmoteMenu();
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
function onKeyDown(e:KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
emoteMenuStore.closeEmoteMenu();
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (unsubscriber) {
|
||||
unsubscriber();
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
emoteMenuStore.closeEmoteMenu();
|
||||
}
|
||||
}
|
||||
|
||||
picker.destroyPicker();
|
||||
})
|
||||
onDestroy(() => {
|
||||
if (unsubscriber) {
|
||||
unsubscriber();
|
||||
}
|
||||
|
||||
picker.destroyPicker();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown}/>
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
<div class="emote-menu-container">
|
||||
<div class="emote-menu" bind:this={emojiContainer}></div>
|
||||
<div class="emote-menu" bind:this={emojiContainer} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.emote-menu-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.emote-menu-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.emote-menu {
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
.emote-menu {
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,22 +1,22 @@
|
||||
<script lang="typescript">
|
||||
import type {Game} from "../../Phaser/Game/Game";
|
||||
import {EnableCameraScene, EnableCameraSceneName} from "../../Phaser/Login/EnableCameraScene";
|
||||
import type { Game } from "../../Phaser/Game/Game";
|
||||
import { EnableCameraScene, EnableCameraSceneName } from "../../Phaser/Login/EnableCameraScene";
|
||||
import {
|
||||
audioConstraintStore,
|
||||
cameraListStore,
|
||||
localStreamStore,
|
||||
microphoneListStore,
|
||||
videoConstraintStore
|
||||
videoConstraintStore,
|
||||
} from "../../Stores/MediaStore";
|
||||
import {onDestroy} from "svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
import HorizontalSoundMeterWidget from "./HorizontalSoundMeterWidget.svelte";
|
||||
import cinemaCloseImg from "../images/cinema-close.svg";
|
||||
import cinemaImg from "../images/cinema.svg";
|
||||
import microphoneImg from "../images/microphone.svg";
|
||||
|
||||
export let game: Game;
|
||||
let selectedCamera : string|undefined = undefined;
|
||||
let selectedMicrophone : string|undefined = undefined;
|
||||
let selectedCamera: string | undefined = undefined;
|
||||
let selectedMicrophone: string | undefined = undefined;
|
||||
|
||||
const enableCameraScene = game.scene.getScene(EnableCameraSceneName) as EnableCameraScene;
|
||||
|
||||
@ -29,16 +29,16 @@
|
||||
return {
|
||||
update(newStream: MediaStream) {
|
||||
if (node.srcObject != newStream) {
|
||||
node.srcObject = newStream
|
||||
node.srcObject = newStream;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let stream: MediaStream | null;
|
||||
|
||||
const unsubscribe = localStreamStore.subscribe(value => {
|
||||
if (value.type === 'success') {
|
||||
const unsubscribe = localStreamStore.subscribe((value) => {
|
||||
if (value.type === "success") {
|
||||
stream = value.stream;
|
||||
|
||||
if (stream !== null) {
|
||||
@ -62,7 +62,7 @@
|
||||
|
||||
function normalizeDeviceName(label: string): string {
|
||||
// remove IDs (that can appear in Chrome, like: "HD Pro Webcam (4df7:4eda)"
|
||||
return label.replace(/(\([[0-9a-f]{4}:[0-9a-f]{4}\))/g, '').trim();
|
||||
return label.replace(/(\([[0-9a-f]{4}:[0-9a-f]{4}\))/g, "").trim();
|
||||
}
|
||||
|
||||
function selectCamera() {
|
||||
@ -72,28 +72,27 @@
|
||||
function selectMicrophone() {
|
||||
audioConstraintStore.setDeviceId(selectedMicrophone);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<form class="enableCameraScene" on:submit|preventDefault={submit}>
|
||||
<section class="text-center">
|
||||
<h2>Turn on your camera and microphone</h2>
|
||||
</section>
|
||||
{#if $localStreamStore.type === 'success' && $localStreamStore.stream}
|
||||
<video class="myCamVideoSetup" use:srcObject={$localStreamStore.stream} autoplay muted playsinline></video>
|
||||
{:else }
|
||||
<div class="webrtcsetup">
|
||||
<img class="background-img" src={cinemaCloseImg} alt="">
|
||||
</div>
|
||||
{/if}
|
||||
<HorizontalSoundMeterWidget stream={stream}></HorizontalSoundMeterWidget>
|
||||
{#if $localStreamStore.type === "success" && $localStreamStore.stream}
|
||||
<video class="myCamVideoSetup" use:srcObject={$localStreamStore.stream} autoplay muted playsinline />
|
||||
{:else}
|
||||
<div class="webrtcsetup">
|
||||
<img class="background-img" src={cinemaCloseImg} alt="" />
|
||||
</div>
|
||||
{/if}
|
||||
<HorizontalSoundMeterWidget {stream} />
|
||||
|
||||
<section class="selectWebcamForm">
|
||||
|
||||
{#if $cameraListStore.length > 1 }
|
||||
{#if $cameraListStore.length > 1}
|
||||
<div class="control-group">
|
||||
<img src={cinemaImg} alt="Camera" />
|
||||
<div class="nes-select is-dark">
|
||||
<!-- svelte-ignore a11y-no-onchange -->
|
||||
<select bind:value={selectedCamera} on:change={selectCamera}>
|
||||
{#each $cameraListStore as camera}
|
||||
<option value={camera.deviceId}>
|
||||
@ -105,10 +104,11 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $microphoneListStore.length > 1 }
|
||||
{#if $microphoneListStore.length > 1}
|
||||
<div class="control-group">
|
||||
<img src={microphoneImg} alt="Microphone" />
|
||||
<div class="nes-select is-dark">
|
||||
<!-- svelte-ignore a11y-no-onchange -->
|
||||
<select bind:value={selectedMicrophone} on:change={selectMicrophone}>
|
||||
{#each $microphoneListStore as microphone}
|
||||
<option value={microphone.deviceId}>
|
||||
@ -119,111 +119,109 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</section>
|
||||
<section class="action">
|
||||
<button type="submit" class="nes-btn is-primary letsgo" >Let's go!</button>
|
||||
<button type="submit" class="nes-btn is-primary letsgo">Let's go!</button>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<style lang="scss">
|
||||
.enableCameraScene {
|
||||
pointer-events: auto;
|
||||
margin: 20px auto 0;
|
||||
color: #ebeeee;
|
||||
pointer-events: auto;
|
||||
margin: 20px auto 0;
|
||||
color: #ebeeee;
|
||||
|
||||
section.selectWebcamForm {
|
||||
margin-top: 3vh;
|
||||
margin-bottom: 3vh;
|
||||
min-height: 10vh;
|
||||
width: 50vw;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
section.selectWebcamForm {
|
||||
margin-top: 3vh;
|
||||
margin-bottom: 3vh;
|
||||
min-height: 10vh;
|
||||
width: 50vw;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
select {
|
||||
font-family: "Press Start 2P";
|
||||
margin-top: 1vh;
|
||||
margin-bottom: 1vh;
|
||||
select {
|
||||
font-family: "Press Start 2P";
|
||||
margin-top: 1vh;
|
||||
margin-bottom: 1vh;
|
||||
}
|
||||
|
||||
option {
|
||||
font-family: "Press Start 2P";
|
||||
}
|
||||
}
|
||||
|
||||
option {
|
||||
section.action {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: "Press Start 2P";
|
||||
}
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
section.action{
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h2{
|
||||
font-family: "Press Start 2P";
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
section.text-center{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button.letsgo {
|
||||
font-size: 200%;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-height: 60px;
|
||||
margin-top: 10px;
|
||||
|
||||
img {
|
||||
width: 30px;
|
||||
margin-right: 10px;
|
||||
section.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.webrtcsetup{
|
||||
margin-top: 2vh;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
height: 28.125vw;
|
||||
width: 50vw;
|
||||
border: white 6px solid;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img.background-img {
|
||||
width: 40%;
|
||||
button.letsgo {
|
||||
font-size: 200%;
|
||||
}
|
||||
}
|
||||
.myCamVideoSetup {
|
||||
margin-top: 2vh;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-height: 50vh;
|
||||
width: 50vw;
|
||||
border: white 6px solid;
|
||||
-webkit-transform: scaleX(-1);
|
||||
transform: scaleX(-1);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-height: 60px;
|
||||
margin-top: 10px;
|
||||
|
||||
img {
|
||||
width: 30px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.webrtcsetup {
|
||||
margin-top: 2vh;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
height: 28.125vw;
|
||||
width: 50vw;
|
||||
border: white 6px solid;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img.background-img {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
.myCamVideoSetup {
|
||||
margin-top: 2vh;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-height: 50vh;
|
||||
width: 50vw;
|
||||
border: white 6px solid;
|
||||
-webkit-transform: scaleX(-1);
|
||||
transform: scaleX(-1);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
.enableCameraScene h2 {
|
||||
font-size: 80%;
|
||||
}
|
||||
.enableCameraScene .control-group .nes-select {
|
||||
font-size: 80%;
|
||||
}
|
||||
.enableCameraScene button.letsgo {
|
||||
font-size: 160%;
|
||||
}
|
||||
.enableCameraScene h2 {
|
||||
font-size: 80%;
|
||||
}
|
||||
.enableCameraScene .control-group .nes-select {
|
||||
font-size: 80%;
|
||||
}
|
||||
.enableCameraScene button.letsgo {
|
||||
font-size: 160%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="typescript">
|
||||
import { AudioContext } from 'standardized-audio-context';
|
||||
import {SoundMeter} from "../../Phaser/Components/SoundMeter";
|
||||
import {onDestroy} from "svelte";
|
||||
import { AudioContext } from "standardized-audio-context";
|
||||
import { SoundMeter } from "../../Phaser/Components/SoundMeter";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
export let stream: MediaStream | null;
|
||||
let volume = 0;
|
||||
@ -11,6 +11,7 @@
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
const soundMeter = new SoundMeter();
|
||||
let display = false;
|
||||
let error = false;
|
||||
|
||||
$: {
|
||||
if (stream && stream.getAudioTracks().length > 0) {
|
||||
@ -19,17 +20,19 @@
|
||||
|
||||
if (timeout) {
|
||||
clearInterval(timeout);
|
||||
error = false;
|
||||
}
|
||||
|
||||
timeout = setInterval(() => {
|
||||
try{
|
||||
volume = parseInt((soundMeter.getVolume() / 100 * NB_BARS).toFixed(0));
|
||||
//console.log(volume);
|
||||
}catch(err){
|
||||
|
||||
try {
|
||||
volume = parseInt(((soundMeter.getVolume() / 100) * NB_BARS).toFixed(0));
|
||||
} catch (err) {
|
||||
if (!error) {
|
||||
console.error(err);
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
} else {
|
||||
display = false;
|
||||
}
|
||||
@ -40,11 +43,10 @@
|
||||
if (timeout) {
|
||||
clearInterval(timeout);
|
||||
}
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
function color(i: number, volume: number) {
|
||||
const red = 255 * i / NB_BARS;
|
||||
const red = (255 * i) / NB_BARS;
|
||||
const green = 255 * (1 - i / NB_BARS);
|
||||
|
||||
let alpha = 1;
|
||||
@ -52,31 +54,29 @@
|
||||
alpha = 0.5;
|
||||
}
|
||||
|
||||
return 'background-color:rgba('+red+', '+green+', 0, '+alpha+')';
|
||||
return "background-color:rgba(" + red + ", " + green + ", 0, " + alpha + ")";
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div class="horizontal-sound-meter" class:active={display}>
|
||||
{#each [...Array(NB_BARS).keys()] as i (i)}
|
||||
<div style={color(i, volume)}></div>
|
||||
<div style={color(i, volume)} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.horizontal-sound-meter {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 50%;
|
||||
height: 30px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 1vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 50%;
|
||||
height: 30px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 1vh;
|
||||
}
|
||||
|
||||
.horizontal-sound-meter div {
|
||||
margin-left: 5px;
|
||||
flex-grow: 1;
|
||||
margin-left: 5px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@ -1,9 +1,9 @@
|
||||
<script lang="typescript">
|
||||
import { fly } from 'svelte/transition';
|
||||
import {helpCameraSettingsVisibleStore} from "../../Stores/HelpCameraSettingsStore";
|
||||
import { fly } from "svelte/transition";
|
||||
import { helpCameraSettingsVisibleStore } from "../../Stores/HelpCameraSettingsStore";
|
||||
import firefoxImg from "./images/help-setting-camera-permission-firefox.png";
|
||||
import chromeImg from "./images/help-setting-camera-permission-chrome.png";
|
||||
import {getNavigatorType, isAndroid as isAndroidFct, NavigatorType} from "../../WebRtc/DeviceUtils";
|
||||
import { getNavigatorType, isAndroid as isAndroidFct, NavigatorType } from "../../WebRtc/DeviceUtils";
|
||||
|
||||
let isAndroid = isAndroidFct();
|
||||
let isFirefox = getNavigatorType() === NavigatorType.firefox;
|
||||
@ -16,30 +16,37 @@
|
||||
function close() {
|
||||
helpCameraSettingsVisibleStore.set(false);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<form class="helpCameraSettings nes-container" on:submit|preventDefault={close} transition:fly="{{ y: -900, duration: 500 }}">
|
||||
<form
|
||||
class="helpCameraSettings nes-container"
|
||||
on:submit|preventDefault={close}
|
||||
transition:fly={{ y: -900, duration: 500 }}
|
||||
>
|
||||
<section>
|
||||
<h2>Camera / Microphone access needed</h2>
|
||||
<p class="err">Permission denied</p>
|
||||
<p>You must allow camera and microphone access in your browser.</p>
|
||||
<p>
|
||||
{#if isFirefox }
|
||||
<p class="err">Please click the "Remember this decision" checkbox, if you don't want Firefox to keep asking you the authorization.</p>
|
||||
{#if isFirefox}
|
||||
<p class="err">
|
||||
Please click the "Remember this decision" checkbox, if you don't want Firefox to keep asking you the
|
||||
authorization.
|
||||
</p>
|
||||
<img src={firefoxImg} alt="" />
|
||||
{:else if isChrome && !isAndroid }
|
||||
{:else if isChrome && !isAndroid}
|
||||
<img src={chromeImg} alt="" />
|
||||
{/if}
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<button class="helpCameraSettingsFormRefresh nes-btn" on:click|preventDefault={refresh}>Refresh</button>
|
||||
<button type="submit" class="helpCameraSettingsFormContinue nes-btn is-primary" on:click|preventDefault={close}>Continue without webcam</button>
|
||||
<button type="submit" class="helpCameraSettingsFormContinue nes-btn is-primary" on:click|preventDefault={close}
|
||||
>Continue without webcam</button
|
||||
>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.helpCameraSettings {
|
||||
pointer-events: auto;
|
||||
@ -53,13 +60,13 @@
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
font-family: 'Press Start 2P';
|
||||
font-family: "Press Start 2P";
|
||||
}
|
||||
|
||||
section {
|
||||
p {
|
||||
margin: 15px;
|
||||
font-family: 'Press Start 2P';
|
||||
font-family: "Press Start 2P";
|
||||
|
||||
& .err {
|
||||
color: #ff0000;
|
||||
|
@ -6,7 +6,6 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div class="layout-manager-list">
|
||||
{#each $layoutManagerActionStore as action}
|
||||
<div class="nes-container is-rounded {action.type}" on:click={() => onClick(action.callback)}>
|
||||
@ -15,43 +14,48 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
div.layout-manager-list {
|
||||
pointer-events: auto;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 40px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
width: clamp(200px, 20vw, 20vw);
|
||||
div.layout-manager-list {
|
||||
pointer-events: auto;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 40px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
width: clamp(200px, 20vw, 20vw);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
animation: moveMessage .5s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
div.nes-container.is-rounded {
|
||||
padding: 8px 4px;
|
||||
text-align: center;
|
||||
|
||||
font-family: Lato;
|
||||
color: whitesmoke;
|
||||
background-color: rgb(0,0,0,0.5);
|
||||
|
||||
&.warning {
|
||||
background-color: #ff9800eb;
|
||||
color: #000;
|
||||
animation: moveMessage 0.5s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveMessage {
|
||||
0% {bottom: 40px;}
|
||||
50% {bottom: 30px;}
|
||||
100% {bottom: 40px;}
|
||||
}
|
||||
div.nes-container.is-rounded {
|
||||
padding: 8px 4px;
|
||||
text-align: center;
|
||||
|
||||
font-family: Lato;
|
||||
color: whitesmoke;
|
||||
background-color: rgb(0, 0, 0, 0.5);
|
||||
|
||||
&.warning {
|
||||
background-color: #ff9800eb;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveMessage {
|
||||
0% {
|
||||
bottom: 40px;
|
||||
}
|
||||
50% {
|
||||
bottom: 30px;
|
||||
}
|
||||
100% {
|
||||
bottom: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,22 +1,22 @@
|
||||
<script lang="typescript">
|
||||
import type {Game} from "../../Phaser/Game/Game";
|
||||
import {LoginScene, LoginSceneName} from "../../Phaser/Login/LoginScene";
|
||||
import {DISPLAY_TERMS_OF_USE, MAX_USERNAME_LENGTH} from "../../Enum/EnvironmentVariable";
|
||||
import type { Game } from "../../Phaser/Game/Game";
|
||||
import { LoginScene, LoginSceneName } from "../../Phaser/Login/LoginScene";
|
||||
import { DISPLAY_TERMS_OF_USE, MAX_USERNAME_LENGTH } from "../../Enum/EnvironmentVariable";
|
||||
import logoImg from "../images/logo.png";
|
||||
import {gameManager} from "../../Phaser/Game/GameManager";
|
||||
import { gameManager } from "../../Phaser/Game/GameManager";
|
||||
|
||||
export let game: Game;
|
||||
|
||||
const loginScene = game.scene.getScene(LoginSceneName) as LoginScene;
|
||||
|
||||
let name = gameManager.getPlayerName() || '';
|
||||
let name = gameManager.getPlayerName() || "";
|
||||
let startValidating = false;
|
||||
|
||||
function submit() {
|
||||
startValidating = true;
|
||||
|
||||
let finalName = name.trim();
|
||||
if (finalName !== '') {
|
||||
if (finalName !== "") {
|
||||
loginScene.login(finalName);
|
||||
}
|
||||
}
|
||||
@ -29,17 +29,34 @@
|
||||
<section class="text-center">
|
||||
<h2>Enter your name</h2>
|
||||
</section>
|
||||
<input type="text" name="loginSceneName" class="nes-input is-dark" autofocus maxlength={MAX_USERNAME_LENGTH} bind:value={name} on:keypress={() => {startValidating = true}} class:is-error={name.trim() === '' && startValidating} />
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
type="text"
|
||||
name="loginSceneName"
|
||||
class="nes-input is-dark"
|
||||
autofocus
|
||||
maxlength={MAX_USERNAME_LENGTH}
|
||||
bind:value={name}
|
||||
on:keypress={() => {
|
||||
startValidating = true;
|
||||
}}
|
||||
class:is-error={name.trim() === "" && startValidating}
|
||||
/>
|
||||
<section class="error-section">
|
||||
{#if name.trim() === '' && startValidating }
|
||||
<p class="err">The name is empty</p>
|
||||
{/if}
|
||||
{#if name.trim() === "" && startValidating}
|
||||
<p class="err">The name is empty</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#if DISPLAY_TERMS_OF_USE}
|
||||
<section class="terms-and-conditions">
|
||||
<p>By continuing, you are agreeing our <a href="https://workadventu.re/terms-of-use" target="_blank">terms of use</a>, <a href="https://workadventu.re/privacy-policy" target="_blank">privacy policy</a> and <a href="https://workadventu.re/cookie-policy" target="_blank">cookie policy</a>.</p>
|
||||
</section>
|
||||
<section class="terms-and-conditions">
|
||||
<p>
|
||||
By continuing, you are agreeing our <a href="https://workadventu.re/terms-of-use" target="_blank"
|
||||
>terms of use</a
|
||||
>, <a href="https://workadventu.re/privacy-policy" target="_blank">privacy policy</a> and
|
||||
<a href="https://workadventu.re/cookie-policy" target="_blank">cookie policy</a>.
|
||||
</p>
|
||||
</section>
|
||||
{/if}
|
||||
<section class="action">
|
||||
<button type="submit" class="nes-btn is-primary loginSceneFormSubmit">Continue</button>
|
||||
@ -47,76 +64,75 @@
|
||||
</form>
|
||||
|
||||
<style lang="scss">
|
||||
.loginScene {
|
||||
pointer-events: auto;
|
||||
margin: 20px auto 0;
|
||||
width: 90%;
|
||||
color: #ebeeee;
|
||||
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
text-align: center;
|
||||
font-family: "Press Start 2P";
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.terms-and-conditions {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
p.err {
|
||||
color: #ce372b;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
section {
|
||||
margin: 10px;
|
||||
|
||||
&.error-section {
|
||||
min-height: 2rem;
|
||||
margin: 0;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.action {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: "Press Start 2P";
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
&.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
.loginScene {
|
||||
pointer-events: auto;
|
||||
margin: 20px auto 0;
|
||||
width: 90%;
|
||||
color: #ebeeee;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
font-weight: 700;
|
||||
}
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
text-align: left;
|
||||
margin: 10px 10px;
|
||||
}
|
||||
input {
|
||||
text-align: center;
|
||||
font-family: "Press Start 2P";
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.terms-and-conditions {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
p.err {
|
||||
color: #ce372b;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
section {
|
||||
margin: 10px;
|
||||
|
||||
&.error-section {
|
||||
min-height: 2rem;
|
||||
margin: 0;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.action {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: "Press Start 2P";
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
&.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: #ebeeee;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: left;
|
||||
margin: 10px 10px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@ -1,42 +1,64 @@
|
||||
<script lang="ts">
|
||||
import { gameManager } from "../../Phaser/Game/GameManager";
|
||||
import {onMount} from "svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let gameScene = gameManager.getCurrentGameScene();
|
||||
|
||||
let expandedMapCopyright = false;
|
||||
let expandedTilesetCopyright = false;
|
||||
let expandedAudioCopyright = false;
|
||||
|
||||
let mapName: string = "";
|
||||
let mapLink: string = "";
|
||||
let mapDescription: string = "";
|
||||
let mapCopyright: string = "The map creator did not declare a copyright for the map.";
|
||||
let tilesetCopyright: string[] = [];
|
||||
let audioCopyright: string[] = [];
|
||||
|
||||
onMount(() => {
|
||||
if (gameScene.mapFile.properties !== undefined) {
|
||||
const propertyName = gameScene.mapFile.properties.find((property) => property.name === 'mapName')
|
||||
if ( propertyName !== undefined && typeof propertyName.value === 'string') {
|
||||
const propertyName = gameScene.mapFile.properties.find((property) => property.name === "mapName");
|
||||
if (propertyName !== undefined && typeof propertyName.value === "string") {
|
||||
mapName = propertyName.value;
|
||||
}
|
||||
const propertyDescription = gameScene.mapFile.properties.find((property) => property.name === 'mapDescription')
|
||||
if (propertyDescription !== undefined && typeof propertyDescription.value === 'string') {
|
||||
const propertyLink = gameScene.mapFile.properties.find((property) => property.name === "mapLink");
|
||||
if (propertyLink !== undefined && typeof propertyLink.value === "string") {
|
||||
mapLink = propertyLink.value;
|
||||
}
|
||||
const propertyDescription = gameScene.mapFile.properties.find(
|
||||
(property) => property.name === "mapDescription"
|
||||
);
|
||||
if (propertyDescription !== undefined && typeof propertyDescription.value === "string") {
|
||||
mapDescription = propertyDescription.value;
|
||||
}
|
||||
const propertyCopyright = gameScene.mapFile.properties.find((property) => property.name === 'mapCopyright')
|
||||
if (propertyCopyright !== undefined && typeof propertyCopyright.value === 'string') {
|
||||
const propertyCopyright = gameScene.mapFile.properties.find((property) => property.name === "mapCopyright");
|
||||
if (propertyCopyright !== undefined && typeof propertyCopyright.value === "string") {
|
||||
mapCopyright = propertyCopyright.value;
|
||||
}
|
||||
}
|
||||
|
||||
for (const tileset of gameScene.mapFile.tilesets) {
|
||||
if (tileset.properties !== undefined) {
|
||||
const propertyTilesetCopyright = tileset.properties.find((property) => property.name === 'tilesetCopyright')
|
||||
if (propertyTilesetCopyright !== undefined && typeof propertyTilesetCopyright.value === 'string') {
|
||||
tilesetCopyright = [...tilesetCopyright, propertyTilesetCopyright.value]; //Assignment needed to trigger Svelte's reactivity
|
||||
const propertyTilesetCopyright = tileset.properties.find(
|
||||
(property) => property.name === "tilesetCopyright"
|
||||
);
|
||||
if (propertyTilesetCopyright !== undefined && typeof propertyTilesetCopyright.value === "string") {
|
||||
// Assignment needed to trigger Svelte's reactivity
|
||||
tilesetCopyright = [...tilesetCopyright, propertyTilesetCopyright.value];
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
for (const layer of gameScene.mapFile.layers) {
|
||||
if (layer.type && layer.type === "tilelayer" && layer.properties) {
|
||||
const propertyAudioCopyright = layer.properties.find((property) => property.name === "audioCopyright");
|
||||
if (propertyAudioCopyright !== undefined && typeof propertyAudioCopyright.value === "string") {
|
||||
// Assignment needed to trigger Svelte's reactivity
|
||||
audioCopyright = [...audioCopyright, propertyAudioCopyright.value];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="about-room-main">
|
||||
@ -44,51 +66,74 @@
|
||||
<section class="container-overflow">
|
||||
<h3>{mapName}</h3>
|
||||
<p class="string-HTML">{mapDescription}</p>
|
||||
<h3 class="nes-pointer hoverable" on:click={() => expandedMapCopyright = !expandedMapCopyright}>Copyrights of the map</h3>
|
||||
<p class="string-HTML" hidden="{!expandedMapCopyright}">{mapCopyright}</p>
|
||||
<h3 class="nes-pointer hoverable" on:click={() => expandedTilesetCopyright = !expandedTilesetCopyright}>Copyrights of the tilesets</h3>
|
||||
<section hidden="{!expandedTilesetCopyright}">
|
||||
{#if mapLink}
|
||||
<p class="string-HTML">> <a href={mapLink} target="_blank">link to this map</a> <</p>
|
||||
{/if}
|
||||
<h3 class="nes-pointer hoverable" on:click={() => (expandedMapCopyright = !expandedMapCopyright)}>
|
||||
Copyrights of the map
|
||||
</h3>
|
||||
<p class="string-HTML" hidden={!expandedMapCopyright}>{mapCopyright}</p>
|
||||
<h3 class="nes-pointer hoverable" on:click={() => (expandedTilesetCopyright = !expandedTilesetCopyright)}>
|
||||
Copyrights of the tilesets
|
||||
</h3>
|
||||
<section hidden={!expandedTilesetCopyright}>
|
||||
{#each tilesetCopyright as copyright}
|
||||
<p class="string-HTML">{copyright}</p>
|
||||
{:else}
|
||||
<p>The map creator did not declare a copyright for the tilesets. Warning, This doesn't mean that those tilesets have no license.</p>
|
||||
<p>
|
||||
The map creator did not declare a copyright for the tilesets. This doesn't mean that those tilesets
|
||||
have no license.
|
||||
</p>
|
||||
{/each}
|
||||
</section>
|
||||
<h3 class="nes-pointer hoverable" on:click={() => (expandedAudioCopyright = !expandedAudioCopyright)}>
|
||||
Copyrights of audio files
|
||||
</h3>
|
||||
<section hidden={!expandedAudioCopyright}>
|
||||
{#each audioCopyright as copyright}
|
||||
<p class="string-HTML">{copyright}</p>
|
||||
{:else}
|
||||
<p>
|
||||
The map creator did not declare a copyright for audio files. This doesn't mean that those tilesets
|
||||
have no license.
|
||||
</p>
|
||||
{/each}
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.string-HTML{
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
div.about-room-main {
|
||||
height: calc(100% - 56px);
|
||||
|
||||
h2, h3 {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
.string-HTML {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
h3.hoverable:hover {
|
||||
background-color: #3c3e40;
|
||||
border-radius: 32px;
|
||||
}
|
||||
|
||||
section.container-overflow {
|
||||
height: calc(100% - 220px);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
|
||||
div.about-room-main {
|
||||
section.container-overflow {
|
||||
height: calc(100% - 120px);
|
||||
}
|
||||
height: calc(100% - 56px);
|
||||
|
||||
h2,
|
||||
h3 {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h3.hoverable:hover {
|
||||
background-color: #3c3e40;
|
||||
border-radius: 32px;
|
||||
}
|
||||
|
||||
section.container-overflow {
|
||||
height: calc(100% - 220px);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
|
||||
div.about-room-main {
|
||||
section.container-overflow {
|
||||
height: calc(100% - 120px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -26,31 +26,31 @@
|
||||
const selectedFile = inputAudio.files ? inputAudio.files[0] : null;
|
||||
if (!selectedFile) {
|
||||
errorFile = true;
|
||||
throw 'no file selected';
|
||||
throw "no file selected";
|
||||
}
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('file', selectedFile);
|
||||
fd.append("file", selectedFile);
|
||||
const res = await gameScene.connection?.uploadAudio(fd);
|
||||
|
||||
const audioGlobalMessage: PlayGlobalMessageInterface = {
|
||||
content: (res as { path: string }).path,
|
||||
type: AUDIO_TYPE,
|
||||
broadcastToWorld: broadcast
|
||||
}
|
||||
inputAudio.value = '';
|
||||
broadcastToWorld: broadcast,
|
||||
};
|
||||
inputAudio.value = "";
|
||||
gameScene.connection?.emitGlobalMessage(audioGlobalMessage);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function inputAudioFile(event: Event) {
|
||||
const eventTarget : EventTargetFiles = (event.target as EventTargetFiles);
|
||||
if(!eventTarget || !eventTarget.files || eventTarget.files.length === 0){
|
||||
const eventTarget: EventTargetFiles = event.target as EventTargetFiles;
|
||||
if (!eventTarget || !eventTarget.files || eventTarget.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = eventTarget.files[0];
|
||||
if(!file) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -61,52 +61,65 @@
|
||||
|
||||
function getFileSize(number: number) {
|
||||
if (number < 1024) {
|
||||
return number + 'bytes';
|
||||
return number + "bytes";
|
||||
} else if (number >= 1024 && number < 1048576) {
|
||||
return (number / 1024).toFixed(1) + 'KB';
|
||||
return (number / 1024).toFixed(1) + "KB";
|
||||
} else if (number >= 1048576) {
|
||||
return (number / 1048576).toFixed(1) + 'MB';
|
||||
return (number / 1048576).toFixed(1) + "MB";
|
||||
} else {
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<section class="section-input-send-audio">
|
||||
<img class="nes-pointer" src="{uploadFile}" alt="Upload a file" on:click|preventDefault={ () => {fileInput.click();}}>
|
||||
<img
|
||||
class="nes-pointer"
|
||||
src={uploadFile}
|
||||
alt="Upload a file"
|
||||
on:click|preventDefault={() => {
|
||||
fileInput.click();
|
||||
}}
|
||||
/>
|
||||
{#if fileName !== undefined}
|
||||
<p>{fileName} : {fileSize}</p>
|
||||
{/if}
|
||||
{#if errorFile}
|
||||
<p class="err">No file selected. You need to upload a file before sending it.</p>
|
||||
{/if}
|
||||
<input type="file" id="input-send-audio" bind:this={fileInput} on:change={(e) => {inputAudioFile(e)}}>
|
||||
<input
|
||||
type="file"
|
||||
id="input-send-audio"
|
||||
bind:this={fileInput}
|
||||
on:change={(e) => {
|
||||
inputAudioFile(e);
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
section.section-input-send-audio {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
section.section-input-send-audio {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
flex: 1 1 auto;
|
||||
max-height: 80%;
|
||||
margin-bottom: 20px;
|
||||
img {
|
||||
flex: 1 1 auto;
|
||||
max-height: 80%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 5px;
|
||||
color: whitesmoke;
|
||||
font-size: 1rem;
|
||||
&.err {
|
||||
color: #ce372b;
|
||||
}
|
||||
}
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin-bottom: 5px;
|
||||
color: whitesmoke;
|
||||
font-size: 1rem;
|
||||
&.err {
|
||||
color: #ce372b;
|
||||
}
|
||||
}
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
|
||||
function goToGettingStarted() {
|
||||
const sparkHost = "https://workadventu.re/getting-started";
|
||||
window.open(sparkHost, "_blank");
|
||||
@ -10,7 +9,7 @@
|
||||
window.open(sparkHost, "_blank");
|
||||
}
|
||||
|
||||
import {contactPageStore} from "../../Stores/MenuStore";
|
||||
import { contactPageStore } from "../../Stores/MenuStore";
|
||||
</script>
|
||||
|
||||
<div class="create-map-main">
|
||||
@ -18,8 +17,8 @@
|
||||
<section>
|
||||
<h3>Getting started</h3>
|
||||
<p>
|
||||
WorkAdventure allows you to create an online space to communicate spontaneously with others.
|
||||
And it all starts with creating your own space. Choose from a large selection of prefabricated maps by our team.
|
||||
WorkAdventure allows you to create an online space to communicate spontaneously with others. And it all
|
||||
starts with creating your own space. Choose from a large selection of prefabricated maps by our team.
|
||||
</p>
|
||||
<button type="button" class="nes-btn is-primary" on:click={goToGettingStarted}>Getting started</button>
|
||||
</section>
|
||||
@ -30,35 +29,37 @@
|
||||
<button type="button" class="nes-btn" on:click={goToBuildingMap}>Create your map</button>
|
||||
</section>
|
||||
|
||||
<iframe title="contact"
|
||||
src="{$contactPageStore}"
|
||||
allow="clipboard-read; clipboard-write self {$contactPageStore}"
|
||||
allowfullscreen></iframe>
|
||||
<iframe
|
||||
title="contact"
|
||||
src={$contactPageStore}
|
||||
allow="clipboard-read; clipboard-write self {$contactPageStore}"
|
||||
allowfullscreen
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
div.create-map-main {
|
||||
height: calc(100% - 56px);
|
||||
height: calc(100% - 56px);
|
||||
|
||||
text-align: center;
|
||||
text-align: center;
|
||||
|
||||
section {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
section {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
section.container-overflow {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
section.container-overflow {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: none;
|
||||
height: calc(100% - 56px);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
border: none;
|
||||
height: calc(100% - 56px);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1,33 +1,32 @@
|
||||
<script lang="ts">
|
||||
import {onDestroy, onMount} from "svelte";
|
||||
import {iframeListener} from "../../Api/IframeListener";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { iframeListener } from "../../Api/IframeListener";
|
||||
|
||||
export let url: string;
|
||||
export let allowApi: boolean;
|
||||
|
||||
let HTMLIframe: HTMLIFrameElement;
|
||||
|
||||
onMount( () => {
|
||||
onMount(() => {
|
||||
if (allowApi) {
|
||||
iframeListener.registerIframe(HTMLIframe);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
onDestroy( () => {
|
||||
onDestroy(() => {
|
||||
if (allowApi) {
|
||||
iframeListener.unregisterIframe(HTMLIframe);
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<iframe title="customSubMenu" src="{url}" bind:this={HTMLIframe}></iframe>
|
||||
<iframe title="customSubMenu" src={url} bind:this={HTMLIframe} />
|
||||
|
||||
<style lang="scss">
|
||||
iframe {
|
||||
border: none;
|
||||
height: calc(100% - 56px);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
iframe {
|
||||
border: none;
|
||||
height: calc(100% - 56px);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import TextGlobalMessage from './TextGlobalMessage.svelte';
|
||||
import AudioGlobalMessage from './AudioGlobalMessage.svelte';
|
||||
import TextGlobalMessage from "./TextGlobalMessage.svelte";
|
||||
import AudioGlobalMessage from "./AudioGlobalMessage.svelte";
|
||||
|
||||
let handleSendText: { sendTextMessage(broadcast: boolean): void };
|
||||
let handleSendAudio: { sendAudioMessage(broadcast: boolean): Promise<void> };
|
||||
@ -32,23 +32,31 @@
|
||||
<div class="global-message-main">
|
||||
<div class="global-message-subOptions">
|
||||
<section>
|
||||
<button type="button" class="nes-btn {inputSendTextActive ? 'is-disabled' : ''}" on:click|preventDefault={activateInputText}>Text</button>
|
||||
<button
|
||||
type="button"
|
||||
class="nes-btn {inputSendTextActive ? 'is-disabled' : ''}"
|
||||
on:click|preventDefault={activateInputText}>Text</button
|
||||
>
|
||||
</section>
|
||||
<section>
|
||||
<button type="button" class="nes-btn {uploadAudioActive ? 'is-disabled' : ''}" on:click|preventDefault={activateUploadAudio}>Audio</button>
|
||||
<button
|
||||
type="button"
|
||||
class="nes-btn {uploadAudioActive ? 'is-disabled' : ''}"
|
||||
on:click|preventDefault={activateUploadAudio}>Audio</button
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
<div class="global-message-content">
|
||||
{#if inputSendTextActive}
|
||||
<TextGlobalMessage bind:handleSending={handleSendText}/>
|
||||
<TextGlobalMessage bind:handleSending={handleSendText} />
|
||||
{/if}
|
||||
{#if uploadAudioActive}
|
||||
<AudioGlobalMessage bind:handleSending={handleSendAudio}/>
|
||||
<AudioGlobalMessage bind:handleSending={handleSendAudio} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="global-message-footer">
|
||||
<label>
|
||||
<input type="checkbox" class="nes-checkbox is-dark nes-pointer" bind:checked={broadcastToWorld}>
|
||||
<input type="checkbox" class="nes-checkbox is-dark nes-pointer" bind:checked={broadcastToWorld} />
|
||||
<span>Broadcast to all rooms of the world</span>
|
||||
</label>
|
||||
<section>
|
||||
@ -57,62 +65,59 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
div.global-message-main {
|
||||
height: calc(100% - 50px);
|
||||
display: grid;
|
||||
grid-template-rows: 15% 65% 20%;
|
||||
|
||||
div.global-message-subOptions {
|
||||
height: calc(100% - 50px);
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
margin-bottom: 20px;
|
||||
grid-template-rows: 15% 65% 20%;
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
div.global-message-subOptions {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
margin-bottom: 20px;
|
||||
|
||||
div.global-message-footer {
|
||||
margin-bottom: 10px;
|
||||
|
||||
display: grid;
|
||||
grid-template-rows: 50% 50%;
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
div.global-message-footer {
|
||||
margin-bottom: 10px;
|
||||
|
||||
span {
|
||||
font-family: "Press Start 2P";
|
||||
}
|
||||
display: grid;
|
||||
grid-template-rows: 50% 50%;
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-family: "Press Start 2P";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
|
||||
.global-message-content {
|
||||
height: calc(100% - 5px);
|
||||
}
|
||||
.global-message-footer {
|
||||
margin-bottom: 0;
|
||||
|
||||
label {
|
||||
width: calc(100% - 10px);
|
||||
.global-message-content {
|
||||
height: calc(100% - 5px);
|
||||
}
|
||||
.global-message-footer {
|
||||
margin-bottom: 0;
|
||||
|
||||
label {
|
||||
width: calc(100% - 10px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1,18 +1,18 @@
|
||||
<script lang="ts">
|
||||
function copyLink() {
|
||||
const input: HTMLInputElement = document.getElementById('input-share-link') as HTMLInputElement;
|
||||
const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement;
|
||||
input.focus();
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
document.execCommand("copy");
|
||||
}
|
||||
|
||||
async function shareLink() {
|
||||
const shareData = {url: location.toString()};
|
||||
const shareData = { url: location.toString() };
|
||||
|
||||
try {
|
||||
await navigator.share(shareData);
|
||||
} catch (err) {
|
||||
console.error('Error: ' + err);
|
||||
console.error("Error: " + err);
|
||||
copyLink();
|
||||
}
|
||||
}
|
||||
@ -22,12 +22,12 @@
|
||||
<section class="container-overflow">
|
||||
<section class="share-url not-mobile">
|
||||
<h3>Share the link of the room !</h3>
|
||||
<input type="text" readonly id="input-share-link" value={location.toString()}>
|
||||
<input type="text" readonly id="input-share-link" value={location.toString()} />
|
||||
<button type="button" class="nes-btn is-primary" on:click={copyLink}>Copy</button>
|
||||
</section>
|
||||
<section class="is-mobile">
|
||||
<h3>Share the link of the room !</h3>
|
||||
<input type="hidden" readonly id="input-share-link" value={location.toString()}>
|
||||
<input type="hidden" readonly id="input-share-link" value={location.toString()} />
|
||||
<button type="button" class="nes-btn is-primary" on:click={shareLink}>Share</button>
|
||||
</section>
|
||||
</section>
|
||||
@ -35,41 +35,41 @@
|
||||
|
||||
<style lang="scss">
|
||||
div.guest-main {
|
||||
height: calc(100% - 56px);
|
||||
height: calc(100% - 56px);
|
||||
|
||||
text-align: center;
|
||||
text-align: center;
|
||||
|
||||
section {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
section.container-overflow {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
section.is-mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 900px), only screen and (max-height: 600px) {
|
||||
div.guest-main {
|
||||
section.share-url.not-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
section.is-mobile {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
section {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
section.container-overflow {
|
||||
height: calc(100% - 120px);
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
section.is-mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@media only screen and (max-width: 900px), only screen and (max-height: 600px) {
|
||||
div.guest-main {
|
||||
section.share-url.not-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
section.is-mobile {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
section.container-overflow {
|
||||
height: calc(100% - 120px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,46 +1,46 @@
|
||||
<script lang="typescript">
|
||||
import {fly} from "svelte/transition";
|
||||
import { fly } from "svelte/transition";
|
||||
import SettingsSubMenu from "./SettingsSubMenu.svelte";
|
||||
import ProfileSubMenu from "./ProfileSubMenu.svelte";
|
||||
import AboutRoomSubMenu from "./AboutRoomSubMenu.svelte";
|
||||
import GlobalMessageSubMenu from "./GlobalMessagesSubMenu.svelte";
|
||||
import ContactSubMenu from "./ContactSubMenu.svelte";
|
||||
import CustomSubMenu from "./CustomSubMenu.svelte"
|
||||
import CustomSubMenu from "./CustomSubMenu.svelte";
|
||||
import GuestSubMenu from "./GuestSubMenu.svelte";
|
||||
import {
|
||||
checkSubMenuToShow,
|
||||
customMenuIframe,
|
||||
menuVisiblilityStore,
|
||||
SubMenusInterface,
|
||||
subMenusStore
|
||||
subMenusStore,
|
||||
} from "../../Stores/MenuStore";
|
||||
import {onDestroy, onMount} from "svelte";
|
||||
import {get} from "svelte/store";
|
||||
import type {Unsubscriber} from "svelte/store";
|
||||
import {sendMenuClickedEvent} from "../../Api/iframe/Ui/MenuItem";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { get } from "svelte/store";
|
||||
import type { Unsubscriber } from "svelte/store";
|
||||
import { sendMenuClickedEvent } from "../../Api/iframe/Ui/MenuItem";
|
||||
|
||||
let activeSubMenu: string = SubMenusInterface.profile;
|
||||
let activeComponent: typeof ProfileSubMenu | typeof CustomSubMenu = ProfileSubMenu;
|
||||
let props: { url: string, allowApi: boolean };
|
||||
let props: { url: string; allowApi: boolean };
|
||||
let unsubscriberSubMenuStore: Unsubscriber;
|
||||
|
||||
onMount(() => {
|
||||
unsubscriberSubMenuStore = subMenusStore.subscribe(() => {
|
||||
if(!get(subMenusStore).includes(activeSubMenu)) {
|
||||
if (!get(subMenusStore).includes(activeSubMenu)) {
|
||||
switchMenu(SubMenusInterface.profile);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
checkSubMenuToShow();
|
||||
|
||||
switchMenu(SubMenusInterface.profile);
|
||||
})
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if(unsubscriberSubMenuStore) {
|
||||
if (unsubscriberSubMenuStore) {
|
||||
unsubscriberSubMenuStore();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
function switchMenu(menu: string) {
|
||||
if (get(subMenusStore).find((subMenu) => subMenu === menu)) {
|
||||
@ -64,7 +64,7 @@
|
||||
case SubMenusInterface.contact:
|
||||
activeComponent = ContactSubMenu;
|
||||
break;
|
||||
default:
|
||||
default: {
|
||||
const customMenu = customMenuIframe.get(menu);
|
||||
if (customMenu !== undefined) {
|
||||
props = { url: customMenu.url, allowApi: customMenu.allowApi };
|
||||
@ -74,109 +74,113 @@
|
||||
menuVisiblilityStore.set(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else throw ("There is no menu called " + menu);
|
||||
} else throw "There is no menu called " + menu;
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
menuVisiblilityStore.set(false);
|
||||
}
|
||||
function onKeyDown(e:KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
closeMenu();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown}/>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
<div class="menu-container-main">
|
||||
<div class="menu-nav-sidebar nes-container is-rounded" transition:fly="{{ x: -1000, duration: 500 }}">
|
||||
<div class="menu-nav-sidebar nes-container is-rounded" transition:fly={{ x: -1000, duration: 500 }}>
|
||||
<h2>Menu</h2>
|
||||
<nav>
|
||||
{#each $subMenusStore as submenu}
|
||||
<button type="button" class="nes-btn {activeSubMenu === submenu ? 'is-disabled' : ''}" on:click|preventDefault={() => switchMenu(submenu)}>
|
||||
<button
|
||||
type="button"
|
||||
class="nes-btn {activeSubMenu === submenu ? 'is-disabled' : ''}"
|
||||
on:click|preventDefault={() => switchMenu(submenu)}
|
||||
>
|
||||
{submenu}
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
<div class="menu-submenu-container nes-container is-rounded" transition:fly="{{ y: -1000, duration: 500 }}">
|
||||
<div class="menu-submenu-container nes-container is-rounded" transition:fly={{ y: -1000, duration: 500 }}>
|
||||
<button type="button" class="nes-btn is-error close" on:click={closeMenu}>×</button>
|
||||
<h2>{activeSubMenu}</h2>
|
||||
<svelte:component this={activeComponent} {...props}/>
|
||||
<svelte:component this={activeComponent} {...props} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.nes-container {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
div.menu-container-main {
|
||||
--size-first-columns-grid: 200px;
|
||||
|
||||
font-family: "Press Start 2P";
|
||||
pointer-events: auto;
|
||||
height: 80%;
|
||||
width: 75%;
|
||||
top: 10%;
|
||||
|
||||
position: relative;
|
||||
z-index: 80;
|
||||
margin: auto;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: var(--size-first-columns-grid) calc(100% - var(--size-first-columns-grid));
|
||||
grid-template-rows: 100%;
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
.nes-container {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
div.menu-nav-sidebar {
|
||||
background-color: #333333;
|
||||
color: whitesmoke;
|
||||
|
||||
nav button {
|
||||
width: calc(100% - 10px);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
div.menu-submenu-container {
|
||||
background-color: #333333;
|
||||
color: whitesmoke;
|
||||
|
||||
.nes-btn.is-error.close {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
right: -20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
div.menu-container-main {
|
||||
--size-first-columns-grid: 120px;
|
||||
height: 70%;
|
||||
top: 55px;
|
||||
width: 100%;
|
||||
font-size: 0.5em;
|
||||
--size-first-columns-grid: 200px;
|
||||
|
||||
div.menu-nav-sidebar {
|
||||
overflow-y: auto;
|
||||
}
|
||||
font-family: "Press Start 2P";
|
||||
pointer-events: auto;
|
||||
height: 80%;
|
||||
width: 75%;
|
||||
top: 10%;
|
||||
|
||||
div.menu-submenu-container {
|
||||
.nes-btn.is-error.close {
|
||||
position: absolute;
|
||||
top: -35px;
|
||||
right: 0;
|
||||
position: relative;
|
||||
z-index: 80;
|
||||
margin: auto;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: var(--size-first-columns-grid) calc(100% - var(--size-first-columns-grid));
|
||||
grid-template-rows: 100%;
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
div.menu-nav-sidebar {
|
||||
background-color: #333333;
|
||||
color: whitesmoke;
|
||||
|
||||
nav button {
|
||||
width: calc(100% - 10px);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
div.menu-submenu-container {
|
||||
background-color: #333333;
|
||||
color: whitesmoke;
|
||||
|
||||
.nes-btn.is-error.close {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
right: -20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
div.menu-container-main {
|
||||
--size-first-columns-grid: 120px;
|
||||
height: 70%;
|
||||
top: 55px;
|
||||
width: 100%;
|
||||
font-size: 0.5em;
|
||||
|
||||
div.menu-nav-sidebar {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
div.menu-submenu-container {
|
||||
.nes-btn.is-error.close {
|
||||
position: absolute;
|
||||
top: -35px;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,47 +1,65 @@
|
||||
<script lang="typescript">
|
||||
import logoWA from "../images/logo-WA-pixel.png"
|
||||
import logoTalk from "../images/logo-message-pixel.png"
|
||||
import {menuVisiblilityStore} from "../../Stores/MenuStore";
|
||||
import {chatVisibilityStore} from "../../Stores/ChatStore";
|
||||
import {get} from "svelte/store";
|
||||
import logoTalk from "../images/logo-message-pixel.png";
|
||||
import logoWA from "../images/logo-WA-pixel.png";
|
||||
import { menuVisiblilityStore } from "../../Stores/MenuStore";
|
||||
import { chatVisibilityStore } from "../../Stores/ChatStore";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
function showMenu(){
|
||||
menuVisiblilityStore.set(!get(menuVisiblilityStore))
|
||||
function showMenu() {
|
||||
menuVisiblilityStore.set(!get(menuVisiblilityStore));
|
||||
}
|
||||
function showChat(){
|
||||
function showChat() {
|
||||
chatVisibilityStore.set(true);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window/>
|
||||
<svelte:window />
|
||||
|
||||
<main class="menuIcon">
|
||||
<img src={logoWA} alt="open menu" class="nes-pointer" on:click|preventDefault={showMenu}>
|
||||
<img src={logoTalk} alt="open menu" class="nes-pointer" on:click|preventDefault={showChat}>
|
||||
<img src={logoWA} alt="open menu" class="nes-pointer" on:click|preventDefault={showMenu} />
|
||||
<img src={logoTalk} alt="open menu" class="nes-pointer" on:click|preventDefault={showChat} />
|
||||
</main>
|
||||
|
||||
<style lang="scss">
|
||||
.menuIcon {
|
||||
display: inline-grid;
|
||||
z-index: 90;
|
||||
position: relative;
|
||||
margin: 25px;
|
||||
img {
|
||||
pointer-events: auto;
|
||||
width: 60px;
|
||||
padding-top: 0;
|
||||
margin: 3px
|
||||
}
|
||||
}
|
||||
.menuIcon img:hover{
|
||||
transform: scale(1.2);
|
||||
}
|
||||
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
|
||||
.menuIcon {
|
||||
margin: 3px;
|
||||
img {
|
||||
width: 50px;
|
||||
}
|
||||
display: inline-grid;
|
||||
z-index: 90;
|
||||
position: relative;
|
||||
margin: 25px;
|
||||
img {
|
||||
pointer-events: auto;
|
||||
width: 60px;
|
||||
padding-top: 0;
|
||||
margin: 3px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
}
|
||||
.menuIcon img:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
|
||||
.menuIcon {
|
||||
display: inline-grid;
|
||||
z-index: 90;
|
||||
position: relative;
|
||||
margin: 25px;
|
||||
img {
|
||||
pointer-events: auto;
|
||||
width: 60px;
|
||||
padding-top: 0;
|
||||
margin: 3px;
|
||||
}
|
||||
}
|
||||
.menuIcon img:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
|
||||
.menuIcon {
|
||||
margin: 3px;
|
||||
img {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,60 +1,60 @@
|
||||
<script lang="typescript">
|
||||
import {gameManager} from "../../Phaser/Game/GameManager";
|
||||
import {SelectCompanionScene, SelectCompanionSceneName} from "../../Phaser/Login/SelectCompanionScene";
|
||||
import {menuIconVisiblilityStore, menuVisiblilityStore, userIsConnected} from "../../Stores/MenuStore";
|
||||
import {selectCompanionSceneVisibleStore} from "../../Stores/SelectCompanionStore";
|
||||
import {LoginScene, LoginSceneName} from "../../Phaser/Login/LoginScene";
|
||||
import {loginSceneVisibleStore} from "../../Stores/LoginSceneStore";
|
||||
import {selectCharacterSceneVisibleStore} from "../../Stores/SelectCharacterStore";
|
||||
import {SelectCharacterScene, SelectCharacterSceneName} from "../../Phaser/Login/SelectCharacterScene";
|
||||
import {connectionManager} from "../../Connexion/ConnectionManager";
|
||||
import {PROFILE_URL} from "../../Enum/EnvironmentVariable";
|
||||
import {localUserStore} from "../../Connexion/LocalUserStore";
|
||||
import {EnableCameraScene, EnableCameraSceneName} from "../../Phaser/Login/EnableCameraScene";
|
||||
import {enableCameraSceneVisibilityStore} from "../../Stores/MediaStore";
|
||||
import { gameManager } from "../../Phaser/Game/GameManager";
|
||||
import { SelectCompanionScene, SelectCompanionSceneName } from "../../Phaser/Login/SelectCompanionScene";
|
||||
import { menuIconVisiblilityStore, menuVisiblilityStore, userIsConnected } from "../../Stores/MenuStore";
|
||||
import { selectCompanionSceneVisibleStore } from "../../Stores/SelectCompanionStore";
|
||||
import { LoginScene, LoginSceneName } from "../../Phaser/Login/LoginScene";
|
||||
import { loginSceneVisibleStore } from "../../Stores/LoginSceneStore";
|
||||
import { selectCharacterSceneVisibleStore } from "../../Stores/SelectCharacterStore";
|
||||
import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene";
|
||||
import { connectionManager } from "../../Connexion/ConnectionManager";
|
||||
import { PROFILE_URL } from "../../Enum/EnvironmentVariable";
|
||||
import { localUserStore } from "../../Connexion/LocalUserStore";
|
||||
import { EnableCameraScene, EnableCameraSceneName } from "../../Phaser/Login/EnableCameraScene";
|
||||
import { enableCameraSceneVisibilityStore } from "../../Stores/MediaStore";
|
||||
import btnProfileSubMenuCamera from "../images/btn-menu-profile-camera.svg";
|
||||
import btnProfileSubMenuIdentity from "../images/btn-menu-profile-identity.svg";
|
||||
import btnProfileSubMenuCompanion from "../images/btn-menu-profile-companion.svg";
|
||||
import btnProfileSubMenuWoka from "../images/btn-menu-profile-woka.svg";
|
||||
import Woka from "../Woka/Woka.svelte";
|
||||
import Companion from "../Companion/Companion.svelte";
|
||||
|
||||
|
||||
function disableMenuStores(){
|
||||
function disableMenuStores() {
|
||||
menuVisiblilityStore.set(false);
|
||||
menuIconVisiblilityStore.set(false);
|
||||
}
|
||||
|
||||
function openEditCompanionScene(){
|
||||
function openEditCompanionScene() {
|
||||
disableMenuStores();
|
||||
selectCompanionSceneVisibleStore.set(true);
|
||||
gameManager.leaveGame(SelectCompanionSceneName,new SelectCompanionScene());
|
||||
gameManager.leaveGame(SelectCompanionSceneName, new SelectCompanionScene());
|
||||
}
|
||||
|
||||
function openEditNameScene(){
|
||||
function openEditNameScene() {
|
||||
disableMenuStores();
|
||||
loginSceneVisibleStore.set(true);
|
||||
gameManager.leaveGame(LoginSceneName,new LoginScene());
|
||||
gameManager.leaveGame(LoginSceneName, new LoginScene());
|
||||
}
|
||||
|
||||
function openEditSkinScene(){
|
||||
function openEditSkinScene() {
|
||||
disableMenuStores();
|
||||
selectCharacterSceneVisibleStore.set(true);
|
||||
gameManager.leaveGame(SelectCharacterSceneName,new SelectCharacterScene());
|
||||
gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene());
|
||||
}
|
||||
|
||||
function logOut(){
|
||||
function logOut() {
|
||||
disableMenuStores();
|
||||
loginSceneVisibleStore.set(true);
|
||||
connectionManager.logout();
|
||||
}
|
||||
|
||||
function getProfileUrl(){
|
||||
function getProfileUrl() {
|
||||
return PROFILE_URL + `?token=${localUserStore.getAuthToken()}`;
|
||||
}
|
||||
|
||||
function openEnableCameraScene(){
|
||||
function openEnableCameraScene() {
|
||||
disableMenuStores();
|
||||
enableCameraSceneVisibilityStore.showEnableCameraScene();
|
||||
gameManager.leaveGame(EnableCameraSceneName,new EnableCameraScene());
|
||||
gameManager.leaveGame(EnableCameraSceneName, new EnableCameraScene());
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -62,19 +62,19 @@
|
||||
<div class="submenu">
|
||||
<section>
|
||||
<button type="button" class="nes-btn" on:click|preventDefault={openEditNameScene}>
|
||||
<img src={btnProfileSubMenuIdentity} alt="Edit your name">
|
||||
<img src={btnProfileSubMenuIdentity} alt="Edit your name" />
|
||||
<span class="btn-hover">Edit your name</span>
|
||||
</button>
|
||||
<button type="button" class="nes-btn" on:click|preventDefault={openEditSkinScene}>
|
||||
<img src={btnProfileSubMenuWoka} alt="Edit your WOKA">
|
||||
<Woka userId={-1} placeholderSrc="" width="26px" height="26px" />
|
||||
<span class="btn-hover">Edit your WOKA</span>
|
||||
</button>
|
||||
<button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}>
|
||||
<img src={btnProfileSubMenuCompanion} alt="Edit your companion">
|
||||
<Companion userId={-1} placeholderSrc={btnProfileSubMenuCompanion} width="26px" height="26px" />
|
||||
<span class="btn-hover">Edit your companion</span>
|
||||
</button>
|
||||
<button type="button" class="nes-btn" on:click|preventDefault={openEnableCameraScene}>
|
||||
<img src={btnProfileSubMenuCamera} alt="Edit your camera">
|
||||
<img src={btnProfileSubMenuCamera} alt="Edit your camera" />
|
||||
<span class="btn-hover">Edit your camera</span>
|
||||
</button>
|
||||
</section>
|
||||
@ -84,7 +84,7 @@
|
||||
{#if $userIsConnected}
|
||||
<section>
|
||||
{#if PROFILE_URL != undefined}
|
||||
<iframe title="profile" src="{getProfileUrl()}"></iframe>
|
||||
<iframe title="profile" src={getProfileUrl()} />
|
||||
{/if}
|
||||
</section>
|
||||
<section>
|
||||
@ -99,68 +99,68 @@
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
div.customize-main{
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
|
||||
div.submenu{
|
||||
height: 100%;
|
||||
width: 50px;
|
||||
|
||||
button {
|
||||
transition: all .5s ease;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 10px;
|
||||
max-height: 44px;
|
||||
|
||||
img {
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span.btn-hover{
|
||||
display: none;
|
||||
font-family: "Press Start 2P";
|
||||
}
|
||||
|
||||
&:hover{
|
||||
width: auto;
|
||||
|
||||
span.btn-hover {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.content {
|
||||
div.customize-main {
|
||||
width: 100%;
|
||||
section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
display: inline-flex;
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 50vh;
|
||||
border: none;
|
||||
}
|
||||
div.submenu {
|
||||
height: 100%;
|
||||
width: 50px;
|
||||
|
||||
button {
|
||||
height: 50px;
|
||||
width: 250px;
|
||||
}
|
||||
button {
|
||||
transition: all 0.5s ease;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 10px;
|
||||
max-height: 44px;
|
||||
|
||||
img {
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span.btn-hover {
|
||||
display: none;
|
||||
font-family: "Press Start 2P";
|
||||
}
|
||||
|
||||
&:hover {
|
||||
width: auto;
|
||||
|
||||
span.btn-hover {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.content {
|
||||
width: 100%;
|
||||
section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 50vh;
|
||||
border: none;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 50px;
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
div.customize-main.content section button {
|
||||
width: 130px;
|
||||
}
|
||||
div.customize-main.content section button {
|
||||
width: 130px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1,62 +1,67 @@
|
||||
<script lang="typescript">
|
||||
import {localUserStore} from "../../Connexion/LocalUserStore";
|
||||
import {videoConstraintStore} from "../../Stores/MediaStore";
|
||||
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
|
||||
import {isMobile} from "../../Enum/EnvironmentVariable";
|
||||
import {menuVisiblilityStore} from "../../Stores/MenuStore";
|
||||
import { localUserStore } from "../../Connexion/LocalUserStore";
|
||||
import { videoConstraintStore } from "../../Stores/MediaStore";
|
||||
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
|
||||
import { isMobile } from "../../Enum/EnvironmentVariable";
|
||||
import { menuVisiblilityStore } from "../../Stores/MenuStore";
|
||||
|
||||
let fullscreen : boolean = localUserStore.getFullscreen();
|
||||
let notification : boolean = localUserStore.getNotification() === 'granted';
|
||||
let valueGame : number = localUserStore.getGameQualityValue();
|
||||
let valueVideo : number = localUserStore.getVideoQualityValue();
|
||||
let previewValueGame = valueGame;
|
||||
let previewValueVideo = valueVideo;
|
||||
let fullscreen: boolean = localUserStore.getFullscreen();
|
||||
let notification: boolean = localUserStore.getNotification() === "granted";
|
||||
let forceCowebsiteTrigger: boolean = localUserStore.getForceCowebsiteTrigger();
|
||||
let valueGame: number = localUserStore.getGameQualityValue();
|
||||
let valueVideo: number = localUserStore.getVideoQualityValue();
|
||||
let previewValueGame = valueGame;
|
||||
let previewValueVideo = valueVideo;
|
||||
|
||||
function saveSetting(){
|
||||
if (valueGame !== previewValueGame) {
|
||||
previewValueGame = valueGame;
|
||||
localUserStore.setGameQualityValue(valueGame);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
if (valueVideo !== previewValueVideo) {
|
||||
previewValueVideo = valueVideo;
|
||||
videoConstraintStore.setFrameRate(valueVideo);
|
||||
}
|
||||
|
||||
closeMenu();
|
||||
}
|
||||
|
||||
function changeFullscreen() {
|
||||
const body = HtmlUtils.querySelectorOrFail('body');
|
||||
if (body) {
|
||||
if (document.fullscreenElement !== null && !fullscreen) {
|
||||
document.exitFullscreen()
|
||||
} else {
|
||||
body.requestFullscreen();
|
||||
function saveSetting() {
|
||||
if (valueGame !== previewValueGame) {
|
||||
previewValueGame = valueGame;
|
||||
localUserStore.setGameQualityValue(valueGame);
|
||||
window.location.reload();
|
||||
}
|
||||
localUserStore.setFullscreen(fullscreen);
|
||||
}
|
||||
}
|
||||
|
||||
function changeNotification() {
|
||||
if (Notification.permission === 'granted') {
|
||||
localUserStore.setNotification(notification ? 'granted' : 'denied');
|
||||
} else {
|
||||
Notification.requestPermission().then((response) => {
|
||||
if (response === 'granted') {
|
||||
localUserStore.setNotification(notification ? 'granted' : 'denied');
|
||||
if (valueVideo !== previewValueVideo) {
|
||||
previewValueVideo = valueVideo;
|
||||
videoConstraintStore.setFrameRate(valueVideo);
|
||||
}
|
||||
|
||||
closeMenu();
|
||||
}
|
||||
|
||||
function changeFullscreen() {
|
||||
const body = HtmlUtils.querySelectorOrFail("body");
|
||||
if (body) {
|
||||
if (document.fullscreenElement !== null && !fullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else {
|
||||
localUserStore.setNotification('denied');
|
||||
notification = false;
|
||||
body.requestFullscreen();
|
||||
}
|
||||
})
|
||||
localUserStore.setFullscreen(fullscreen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
menuVisiblilityStore.set(false);
|
||||
}
|
||||
function changeNotification() {
|
||||
if (Notification.permission === "granted") {
|
||||
localUserStore.setNotification(notification ? "granted" : "denied");
|
||||
} else {
|
||||
Notification.requestPermission().then((response) => {
|
||||
if (response === "granted") {
|
||||
localUserStore.setNotification(notification ? "granted" : "denied");
|
||||
} else {
|
||||
localUserStore.setNotification("denied");
|
||||
notification = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function changeForceCowebsiteTrigger() {
|
||||
localUserStore.setForceCowebsiteTrigger(forceCowebsiteTrigger);
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
menuVisiblilityStore.set(false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="settings-main" on:submit|preventDefault={saveSetting}>
|
||||
@ -64,10 +69,12 @@ function closeMenu() {
|
||||
<h3>Game quality</h3>
|
||||
<div class="nes-select is-dark">
|
||||
<select bind:value={valueGame}>
|
||||
<option value="{120}">{isMobile() ? 'High (120 fps)' : 'High video quality (120 fps)'}</option>
|
||||
<option value="{60}">{isMobile() ? 'Medium (60 fps)' : 'Medium video quality (60 fps, recommended)'}</option>
|
||||
<option value="{40}">{isMobile() ? 'Minimum (40 fps)' : 'Minimum video quality (40 fps)'}</option>
|
||||
<option value="{20}">{isMobile() ? 'Small (20 fps)' : 'Small video quality (20 fps)'}</option>
|
||||
<option value={120}>{isMobile() ? "High (120 fps)" : "High video quality (120 fps)"}</option>
|
||||
<option value={60}
|
||||
>{isMobile() ? "Medium (60 fps)" : "Medium video quality (60 fps, recommended)"}</option
|
||||
>
|
||||
<option value={40}>{isMobile() ? "Minimum (40 fps)" : "Minimum video quality (40 fps)"}</option>
|
||||
<option value={20}>{isMobile() ? "Small (20 fps)" : "Small video quality (20 fps)"}</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
@ -75,10 +82,12 @@ function closeMenu() {
|
||||
<h3>Video quality</h3>
|
||||
<div class="nes-select is-dark">
|
||||
<select bind:value={valueVideo}>
|
||||
<option value="{30}">{isMobile() ? 'High (30 fps)' : 'High video quality (30 fps)'}</option>
|
||||
<option value="{20}">{isMobile() ? 'Medium (20 fps)' : 'Medium video quality (20 fps, recommended)'}</option>
|
||||
<option value="{10}">{isMobile() ? 'Minimum (10 fps)' : 'Minimum video quality (10 fps)'}</option>
|
||||
<option value="{5}">{isMobile() ? 'Small (5 fps)' : 'Small video quality (5 fps)'}</option>
|
||||
<option value={30}>{isMobile() ? "High (30 fps)" : "High video quality (30 fps)"}</option>
|
||||
<option value={20}
|
||||
>{isMobile() ? "Medium (20 fps)" : "Medium video quality (20 fps, recommended)"}</option
|
||||
>
|
||||
<option value={10}>{isMobile() ? "Minimum (10 fps)" : "Minimum video quality (10 fps)"}</option>
|
||||
<option value={5}>{isMobile() ? "Small (5 fps)" : "Small video quality (5 fps)"}</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
@ -88,55 +97,74 @@ function closeMenu() {
|
||||
</section>
|
||||
<section class="settings-section-noSaveOption">
|
||||
<label>
|
||||
<input type="checkbox" class="nes-checkbox is-dark" bind:checked={fullscreen} on:change={changeFullscreen}/>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="nes-checkbox is-dark"
|
||||
bind:checked={fullscreen}
|
||||
on:change={changeFullscreen}
|
||||
/>
|
||||
<span>Fullscreen</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" class="nes-checkbox is-dark" bind:checked={notification} on:change={changeNotification}>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="nes-checkbox is-dark"
|
||||
bind:checked={notification}
|
||||
on:change={changeNotification}
|
||||
/>
|
||||
<span>Notifications</span>
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="nes-checkbox is-dark"
|
||||
bind:checked={forceCowebsiteTrigger}
|
||||
on:change={changeForceCowebsiteTrigger}
|
||||
/>
|
||||
<span>Always ask before opening websites and Jitsi Meet rooms</span>
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
div.settings-main {
|
||||
height: calc(100% - 40px);
|
||||
overflow-y: auto;
|
||||
|
||||
section {
|
||||
width: 100%;
|
||||
padding: 20px 20px 0;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
|
||||
div.nes-select select:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
section.settings-section-save {
|
||||
text-align: center;
|
||||
p {
|
||||
margin: 16px 0;
|
||||
}
|
||||
}
|
||||
section.settings-section-noSaveOption {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
label {
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
margin: 0 0 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
|
||||
div.settings-main {
|
||||
section {
|
||||
padding: 0;
|
||||
}
|
||||
height: calc(100% - 40px);
|
||||
overflow-y: auto;
|
||||
|
||||
section {
|
||||
width: 100%;
|
||||
padding: 20px 20px 0;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
|
||||
div.nes-select select:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
section.settings-section-save {
|
||||
text-align: center;
|
||||
p {
|
||||
margin: 16px 0;
|
||||
}
|
||||
}
|
||||
section.settings-section-noSaveOption {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
label {
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
margin: 0 0 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@media only screen and (max-width: 800px), only screen and (max-height: 800px) {
|
||||
div.settings-main {
|
||||
section {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -8,25 +8,25 @@
|
||||
|
||||
//toolbar
|
||||
const toolbarOptions = [
|
||||
['bold', 'italic', 'underline', 'strike'], // toggled buttons
|
||||
['blockquote', 'code-block'],
|
||||
["bold", "italic", "underline", "strike"], // toggled buttons
|
||||
["blockquote", "code-block"],
|
||||
|
||||
[{'header': 1}, {'header': 2}], // custom button values
|
||||
[{'list': 'ordered'}, {'list': 'bullet'}],
|
||||
[{'script': 'sub'}, {'script': 'super'}], // superscript/subscript
|
||||
[{'indent': '-1'}, {'indent': '+1'}], // outdent/indent
|
||||
[{'direction': 'rtl'}], // text direction
|
||||
[{ header: 1 }, { header: 2 }], // custom button values
|
||||
[{ list: "ordered" }, { list: "bullet" }],
|
||||
[{ script: "sub" }, { script: "super" }], // superscript/subscript
|
||||
[{ indent: "-1" }, { indent: "+1" }], // outdent/indent
|
||||
[{ direction: "rtl" }], // text direction
|
||||
|
||||
[{'size': ['small', false, 'large', 'huge']}], // custom dropdown
|
||||
[{'header': [1, 2, 3, 4, 5, 6, false]}],
|
||||
[{ size: ["small", false, "large", "huge"] }], // custom dropdown
|
||||
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
||||
|
||||
[{'color': []}, {'background': []}], // dropdown with defaults from theme
|
||||
[{'font': []}],
|
||||
[{'align': []}],
|
||||
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
|
||||
[{ font: [] }],
|
||||
[{ align: [] }],
|
||||
|
||||
['clean'], // remove formatting button
|
||||
["clean"], // remove formatting button
|
||||
|
||||
['link', 'image', 'video']
|
||||
["link", "image", "video"],
|
||||
];
|
||||
|
||||
const gameScene = gameManager.getCurrentGameScene();
|
||||
@ -44,24 +44,24 @@
|
||||
const textGlobalMessage: PlayGlobalMessageInterface = {
|
||||
type: MESSAGE_TYPE,
|
||||
content: text,
|
||||
broadcastToWorld: broadcastToWorld
|
||||
broadcastToWorld: broadcastToWorld,
|
||||
};
|
||||
|
||||
quill.deleteText(0, quill.getLength());
|
||||
gameScene.connection?.emitGlobalMessage(textGlobalMessage);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
//Quill
|
||||
onMount(async () => {
|
||||
// Import quill
|
||||
const {default: Quill} = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
const { default: Quill } = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
quill = new Quill(QUILL_EDITOR, {
|
||||
placeholder: 'Enter your message here...',
|
||||
theme: 'snow',
|
||||
placeholder: "Enter your message here...",
|
||||
theme: "snow",
|
||||
modules: {
|
||||
toolbar: toolbarOptions
|
||||
toolbar: toolbarOptions,
|
||||
},
|
||||
});
|
||||
menuInputFocusStore.set(true);
|
||||
@ -69,14 +69,13 @@
|
||||
|
||||
onDestroy(() => {
|
||||
menuInputFocusStore.set(false);
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="section-input-send-text">
|
||||
<div class="input-send-text" bind:this={QUILL_EDITOR}></div>
|
||||
<div class="input-send-text" bind:this={QUILL_EDITOR} />
|
||||
</section>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
@import 'https://cdn.quilljs.com/1.3.7/quill.snow.css';
|
||||
@import "https://cdn.quilljs.com/1.3.7/quill.snow.css";
|
||||
</style>
|
||||
|
@ -1,14 +1,14 @@
|
||||
<script lang="typescript">
|
||||
import {obtainedMediaConstraintStore} from "../Stores/MediaStore";
|
||||
import {localStreamStore, isSilentStore} from "../Stores/MediaStore";
|
||||
import { obtainedMediaConstraintStore } from "../Stores/MediaStore";
|
||||
import { localStreamStore, isSilentStore } from "../Stores/MediaStore";
|
||||
import SoundMeterWidget from "./SoundMeterWidget.svelte";
|
||||
import {onDestroy} from "svelte";
|
||||
import {srcObject} from "./Video/utils";
|
||||
import { onDestroy } from "svelte";
|
||||
import { srcObject } from "./Video/utils";
|
||||
|
||||
let stream : MediaStream|null;
|
||||
let stream: MediaStream | null;
|
||||
|
||||
const unsubscribe = localStreamStore.subscribe(value => {
|
||||
if (value.type === 'success') {
|
||||
const unsubscribe = localStreamStore.subscribe((value) => {
|
||||
if (value.type === "success") {
|
||||
stream = value.stream;
|
||||
} else {
|
||||
stream = null;
|
||||
@ -17,25 +17,20 @@
|
||||
|
||||
onDestroy(unsubscribe);
|
||||
|
||||
|
||||
let isSilent: boolean;
|
||||
const unsubscribeIsSilent = isSilentStore.subscribe(value => {
|
||||
const unsubscribeIsSilent = isSilentStore.subscribe((value) => {
|
||||
isSilent = value;
|
||||
});
|
||||
|
||||
onDestroy(unsubscribeIsSilent);
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div>
|
||||
<div class="video-container div-myCamVideo" class:hide={!$obtainedMediaConstraintStore.video || isSilent}>
|
||||
{#if $localStreamStore.type === "success" && $localStreamStore.stream}
|
||||
<video class="myCamVideo" use:srcObject={stream} autoplay muted playsinline></video>
|
||||
<SoundMeterWidget stream={stream}></SoundMeterWidget>
|
||||
<video class="myCamVideo" use:srcObject={stream} autoplay muted playsinline />
|
||||
<SoundMeterWidget {stream} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="is-silent" class:hide={isSilent}>
|
||||
Silent zone
|
||||
</div>
|
||||
<div class="is-silent" class:hide={isSilent}>Silent zone</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {blackListManager} from "../../WebRtc/BlackListManager";
|
||||
import {showReportScreenStore, userReportEmpty} from "../../Stores/ShowReportScreenStore";
|
||||
import {onMount} from "svelte";
|
||||
import { blackListManager } from "../../WebRtc/BlackListManager";
|
||||
import { showReportScreenStore, userReportEmpty } from "../../Stores/ShowReportScreenStore";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let userUUID: string | undefined;
|
||||
export let userName: string;
|
||||
@ -14,7 +14,7 @@
|
||||
} else {
|
||||
userIsBlocked = blackListManager.isBlackListed(userUUID);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
function blockUser(): void {
|
||||
if (userUUID === undefined) {
|
||||
@ -32,13 +32,12 @@
|
||||
<h3>Block</h3>
|
||||
<p>Block any communication from and to {userName}. This can be reverted.</p>
|
||||
<button type="button" class="nes-btn is-error" on:click|preventDefault={blockUser}>
|
||||
{userIsBlocked ? 'Unblock this user' : 'Block this user'}
|
||||
{userIsBlocked ? "Unblock this user" : "Block this user"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
div.block-container {
|
||||
text-align: center;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import {showReportScreenStore, userReportEmpty} from "../../Stores/ShowReportScreenStore";
|
||||
import { showReportScreenStore, userReportEmpty } from "../../Stores/ShowReportScreenStore";
|
||||
import BlockSubMenu from "./BlockSubMenu.svelte";
|
||||
import ReportSubMenu from "./ReportSubMenu.svelte";
|
||||
import {onDestroy, onMount} from "svelte";
|
||||
import type {Unsubscriber} from "svelte/store";
|
||||
import {playersStore} from "../../Stores/PlayersStore";
|
||||
import {connectionManager} from "../../Connexion/ConnectionManager";
|
||||
import {GameConnexionTypes} from "../../Url/UrlManager";
|
||||
import {get} from "svelte/store";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import type { Unsubscriber } from "svelte/store";
|
||||
import { playersStore } from "../../Stores/PlayersStore";
|
||||
import { connectionManager } from "../../Connexion/ConnectionManager";
|
||||
import { GameConnexionTypes } from "../../Url/UrlManager";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
let blockActive = true;
|
||||
let blockActive = true;
|
||||
let reportActive = !blockActive;
|
||||
let anonymous: boolean = false;
|
||||
let userUUID: string | undefined = playersStore.getPlayerById(get(showReportScreenStore).userId)?.userUuid;
|
||||
let userName = "No name";
|
||||
let unsubscriber: Unsubscriber
|
||||
let unsubscriber: Unsubscriber;
|
||||
|
||||
onMount(() => {
|
||||
unsubscriber = showReportScreenStore.subscribe((reportScreenStore) => {
|
||||
@ -25,15 +25,15 @@
|
||||
console.error("Could not find UUID for user with ID " + reportScreenStore.userId);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
anonymous = connectionManager.getConnexionType === GameConnexionTypes.anonymous;
|
||||
})
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (unsubscriber) {
|
||||
unsubscriber();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
function close() {
|
||||
showReportScreenStore.set(userReportEmpty);
|
||||
@ -49,14 +49,14 @@
|
||||
reportActive = true;
|
||||
}
|
||||
|
||||
function onKeyDown(e:KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
close();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown}/>
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
<div class="report-menu-main nes-container is-rounded">
|
||||
<section class="report-menu-title">
|
||||
@ -67,75 +67,83 @@
|
||||
</section>
|
||||
<section class="report-menu-action {anonymous ? 'hidden' : ''}">
|
||||
<section class="justify-center">
|
||||
<button type="button" class="nes-btn {blockActive ? 'is-disabled' : ''}" on:click|preventDefault={activateBlock}>Block</button>
|
||||
<button
|
||||
type="button"
|
||||
class="nes-btn {blockActive ? 'is-disabled' : ''}"
|
||||
on:click|preventDefault={activateBlock}>Block</button
|
||||
>
|
||||
</section>
|
||||
<section class="justify-center">
|
||||
<button type="button" class="nes-btn {reportActive ? 'is-disabled' : ''}" on:click|preventDefault={activateReport}>Report</button>
|
||||
<button
|
||||
type="button"
|
||||
class="nes-btn {reportActive ? 'is-disabled' : ''}"
|
||||
on:click|preventDefault={activateReport}>Report</button
|
||||
>
|
||||
</section>
|
||||
</section>
|
||||
<section class="report-menu-content">
|
||||
{#if blockActive}
|
||||
<BlockSubMenu userUUID="{userUUID}" userName="{userName}"/>
|
||||
<BlockSubMenu {userUUID} {userName} />
|
||||
{:else if reportActive}
|
||||
<ReportSubMenu userUUID="{userUUID}"/>
|
||||
{:else }
|
||||
<ReportSubMenu {userUUID} />
|
||||
{:else}
|
||||
<p>ERROR : There is no action selected.</p>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.nes-container {
|
||||
padding: 5px;
|
||||
}
|
||||
.nes-container {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
section.justify-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.report-menu-main {
|
||||
font-family: "Press Start 2P";
|
||||
pointer-events: auto;
|
||||
background-color: #333333;
|
||||
color: whitesmoke;
|
||||
|
||||
position: relative;
|
||||
height: 70vh;
|
||||
width: 50vw;
|
||||
top: 10vh;
|
||||
margin: auto;
|
||||
|
||||
section.report-menu-title {
|
||||
display: grid;
|
||||
grid-template-columns: calc(100% - 45px) 40px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h2 {
|
||||
section.justify-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
section.report-menu-action {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
section.report-menu-action.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
div.report-menu-main {
|
||||
top: 21vh;
|
||||
height: 60vh;
|
||||
width: 100vw;
|
||||
font-size: 0.5em;
|
||||
font-family: "Press Start 2P";
|
||||
pointer-events: auto;
|
||||
background-color: #333333;
|
||||
color: whitesmoke;
|
||||
|
||||
position: relative;
|
||||
height: 70vh;
|
||||
width: 50vw;
|
||||
top: 10vh;
|
||||
margin: auto;
|
||||
|
||||
section.report-menu-title {
|
||||
display: grid;
|
||||
grid-template-columns: calc(100% - 45px) 40px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h2 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
section.report-menu-action {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
section.report-menu-action.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
div.report-menu-main {
|
||||
top: 21vh;
|
||||
height: 60vh;
|
||||
width: 100vw;
|
||||
font-size: 0.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,22 +1,22 @@
|
||||
<script lang="ts">
|
||||
import {showReportScreenStore, userReportEmpty} from "../../Stores/ShowReportScreenStore";
|
||||
import {gameManager} from "../../Phaser/Game/GameManager";
|
||||
import { showReportScreenStore, userReportEmpty } from "../../Stores/ShowReportScreenStore";
|
||||
import { gameManager } from "../../Phaser/Game/GameManager";
|
||||
|
||||
export let userUUID: string | undefined;
|
||||
let reportMessage: string;
|
||||
let hiddenError = true;
|
||||
|
||||
function submitReport() {
|
||||
if (reportMessage === '') {
|
||||
if (reportMessage === "") {
|
||||
hiddenError = true;
|
||||
} else {
|
||||
hiddenError = false;
|
||||
if( userUUID === undefined) {
|
||||
console.error('User UUID is not valid.');
|
||||
if (userUUID === undefined) {
|
||||
console.error("User UUID is not valid.");
|
||||
return;
|
||||
}
|
||||
gameManager.getCurrentGameScene().connection?.emitReportPlayerMessage(userUUID, reportMessage);
|
||||
showReportScreenStore.set(userReportEmpty)
|
||||
showReportScreenStore.set(userReportEmpty);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -28,9 +28,9 @@
|
||||
<section>
|
||||
<label>
|
||||
<span>Your message: </span>
|
||||
<textarea type="text" class="nes-textarea" bind:value={reportMessage}></textarea>
|
||||
<textarea type="text" class="nes-textarea" bind:value={reportMessage} />
|
||||
</label>
|
||||
<p hidden="{hiddenError}">Report message cannot to be empty.</p>
|
||||
<p hidden={hiddenError}>Report message cannot to be empty.</p>
|
||||
</section>
|
||||
<section>
|
||||
<button type="submit" class="nes-btn is-error" on:click={submitReport}>Report this user</button>
|
||||
@ -40,16 +40,16 @@
|
||||
|
||||
<style lang="scss">
|
||||
div.report-container-main {
|
||||
text-align: center;
|
||||
text-align: center;
|
||||
|
||||
textarea {
|
||||
height: clamp(100px, 15vh, 300px);
|
||||
}
|
||||
textarea {
|
||||
height: clamp(100px, 15vh, 300px);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-height: 630px) {
|
||||
div.report-container-main textarea {
|
||||
height: 50px;
|
||||
}
|
||||
div.report-container-main textarea {
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="typescript">
|
||||
import type {Game} from "../../Phaser/Game/Game";
|
||||
import {SelectCompanionScene, SelectCompanionSceneName} from "../../Phaser/Login/SelectCompanionScene";
|
||||
import type { Game } from "../../Phaser/Game/Game";
|
||||
import { SelectCompanionScene, SelectCompanionSceneName } from "../../Phaser/Login/SelectCompanionScene";
|
||||
|
||||
export let game: Game;
|
||||
|
||||
@ -26,62 +26,70 @@
|
||||
<form class="selectCompanionScene">
|
||||
<section class="text-center">
|
||||
<h2>Select your companion</h2>
|
||||
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}> < </button>
|
||||
<button class="selectCharacterButton selectCharacterButtonRight nes-btn" on:click|preventDefault={selectRight}> > </button>
|
||||
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}>
|
||||
<
|
||||
</button>
|
||||
<button class="selectCharacterButton selectCharacterButtonRight nes-btn" on:click|preventDefault={selectRight}>
|
||||
>
|
||||
</button>
|
||||
</section>
|
||||
<section class="action">
|
||||
<button href="/" class="selectCompanionSceneFormBack nes-btn" on:click|preventDefault={noCompanion}>No companion</button>
|
||||
<button type="submit" class="selectCompanionSceneFormSubmit nes-btn is-primary" on:click|preventDefault={selectCompanion}>Continue</button>
|
||||
<button href="/" class="selectCompanionSceneFormBack nes-btn" on:click|preventDefault={noCompanion}
|
||||
>No companion</button
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
class="selectCompanionSceneFormSubmit nes-btn is-primary"
|
||||
on:click|preventDefault={selectCompanion}>Continue</button
|
||||
>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<style lang="scss">
|
||||
form.selectCompanionScene {
|
||||
font-family: "Press Start 2P";
|
||||
pointer-events: auto;
|
||||
color: #ebeeee;
|
||||
|
||||
section {
|
||||
margin: 10px;
|
||||
|
||||
&.action {
|
||||
text-align: center;
|
||||
margin-top: 55vh;
|
||||
}
|
||||
|
||||
h2 {
|
||||
form.selectCompanionScene {
|
||||
font-family: "Press Start 2P";
|
||||
margin: 1px;
|
||||
}
|
||||
pointer-events: auto;
|
||||
color: #ebeeee;
|
||||
|
||||
&.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
section {
|
||||
margin: 10px;
|
||||
|
||||
button.selectCharacterButton {
|
||||
position: absolute;
|
||||
top: 33vh;
|
||||
margin: 0;
|
||||
}
|
||||
&.action {
|
||||
text-align: center;
|
||||
margin-top: 55vh;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: "Press Start 2P";
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
&.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button.selectCharacterButton {
|
||||
position: absolute;
|
||||
top: 33vh;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
button.selectCharacterButtonLeft {
|
||||
left: 33vw;
|
||||
}
|
||||
|
||||
button.selectCharacterButtonRight {
|
||||
right: 33vw;
|
||||
}
|
||||
}
|
||||
|
||||
button.selectCharacterButtonLeft {
|
||||
left: 33vw;
|
||||
@media only screen and (max-width: 800px) {
|
||||
form.selectCompanionScene button.selectCharacterButtonLeft {
|
||||
left: 5vw;
|
||||
}
|
||||
form.selectCompanionScene button.selectCharacterButtonRight {
|
||||
right: 5vw;
|
||||
}
|
||||
}
|
||||
|
||||
button.selectCharacterButtonRight {
|
||||
right: 33vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
form.selectCompanionScene button.selectCharacterButtonLeft{
|
||||
left: 5vw;
|
||||
}
|
||||
form.selectCompanionScene button.selectCharacterButtonRight{
|
||||
right: 5vw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
@ -1,14 +1,15 @@
|
||||
<script lang="typescript">
|
||||
import { AudioContext } from 'standardized-audio-context';
|
||||
import {SoundMeter} from "../Phaser/Components/SoundMeter";
|
||||
import {onDestroy} from "svelte";
|
||||
import { AudioContext } from "standardized-audio-context";
|
||||
import { SoundMeter } from "../Phaser/Components/SoundMeter";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
export let stream: MediaStream|null;
|
||||
export let stream: MediaStream | null;
|
||||
let volume = 0;
|
||||
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
const soundMeter = new SoundMeter();
|
||||
let display = false;
|
||||
let error = false;
|
||||
|
||||
$: {
|
||||
if (stream && stream.getAudioTracks().length > 0) {
|
||||
@ -17,17 +18,19 @@
|
||||
|
||||
if (timeout) {
|
||||
clearInterval(timeout);
|
||||
error = false;
|
||||
}
|
||||
|
||||
timeout = setInterval(() => {
|
||||
try{
|
||||
try {
|
||||
volume = soundMeter.getVolume();
|
||||
//console.log(volume);
|
||||
}catch(err){
|
||||
|
||||
} catch (err) {
|
||||
if (!error) {
|
||||
console.error(err);
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
} else {
|
||||
display = false;
|
||||
}
|
||||
@ -38,14 +41,13 @@
|
||||
if (timeout) {
|
||||
clearInterval(timeout);
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<div class="sound-progress" class:active={display}>
|
||||
<span class:active={volume > 5}></span>
|
||||
<span class:active={volume > 10}></span>
|
||||
<span class:active={volume > 15}></span>
|
||||
<span class:active={volume > 40}></span>
|
||||
<span class:active={volume > 70}></span>
|
||||
<span class:active={volume > 5} />
|
||||
<span class:active={volume > 10} />
|
||||
<span class:active={volume > 15} />
|
||||
<span class:active={volume > 40} />
|
||||
<span class:active={volume > 70} />
|
||||
</div>
|
||||
|
@ -1,22 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { fly } from "svelte/transition";
|
||||
import {banMessageVisibleStore, banMessageContentStore} from "../../Stores/TypeMessageStore/BanMessageStore";
|
||||
import {onMount} from "svelte";
|
||||
import { banMessageVisibleStore, banMessageContentStore } from "../../Stores/TypeMessageStore/BanMessageStore";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
const text = $banMessageContentStore;
|
||||
const NAME_BUTTON = 'Ok';
|
||||
let text: string;
|
||||
$: {
|
||||
text = $banMessageContentStore;
|
||||
}
|
||||
const NAME_BUTTON = "Ok";
|
||||
let nbSeconds = 10;
|
||||
let nameButton = '';
|
||||
let nameButton = "";
|
||||
|
||||
onMount(() => {
|
||||
timeToRead()
|
||||
})
|
||||
timeToRead();
|
||||
});
|
||||
|
||||
function timeToRead() {
|
||||
nbSeconds -= 1;
|
||||
nameButton = nbSeconds.toString();
|
||||
if ( nbSeconds > 0 ) {
|
||||
setTimeout( () => {
|
||||
if (nbSeconds > 0) {
|
||||
setTimeout(() => {
|
||||
timeToRead();
|
||||
}, 1000);
|
||||
} else {
|
||||
@ -29,68 +32,76 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="main-ban-message nes-container is-rounded" transition:fly="{{ y: -1000, duration: 500 }}">
|
||||
<h2 class="title-ban-message"><img src="resources/logos/report.svg" alt="***"/> Important message <img src="resources/logos/report.svg" alt="***"/></h2>
|
||||
<div class="main-ban-message nes-container is-rounded" transition:fly={{ y: -1000, duration: 500 }}>
|
||||
<h2 class="title-ban-message">
|
||||
<img src="resources/logos/report.svg" alt="***" /> Important message
|
||||
<img src="resources/logos/report.svg" alt="***" />
|
||||
</h2>
|
||||
<div class="content-ban-message">
|
||||
<p>{text}</p>
|
||||
</div>
|
||||
<div class="footer-ban-message">
|
||||
<button type="button" class="nes-btn {nameButton === NAME_BUTTON ? 'is-primary' : 'is-error'}" disabled="{!(nameButton === NAME_BUTTON)}" on:click|preventDefault={closeBanMessage}>{nameButton}</button>
|
||||
<button
|
||||
type="button"
|
||||
class="nes-btn {nameButton === NAME_BUTTON ? 'is-primary' : 'is-error'}"
|
||||
disabled={!(nameButton === NAME_BUTTON)}
|
||||
on:click|preventDefault={closeBanMessage}>{nameButton}</button
|
||||
>
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-media-has-caption -->
|
||||
<audio id="report-message" autoplay>
|
||||
<source src="/resources/objects/report-message.mp3" type="audio/mp3">
|
||||
<source src="/resources/objects/report-message.mp3" type="audio/mp3" />
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
div.main-ban-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
top: 15vh;
|
||||
div.main-ban-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
top: 15vh;
|
||||
|
||||
height: 70vh;
|
||||
width: 60vw;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-bottom: 0;
|
||||
height: 70vh;
|
||||
width: 60vw;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-bottom: 0;
|
||||
|
||||
pointer-events: auto;
|
||||
background-color: #333333;
|
||||
color: whitesmoke;
|
||||
pointer-events: auto;
|
||||
background-color: #333333;
|
||||
color: whitesmoke;
|
||||
|
||||
h2.title-ban-message {
|
||||
flex: 1 1 auto;
|
||||
max-height: 50px;
|
||||
margin-bottom: 20px;
|
||||
h2.title-ban-message {
|
||||
flex: 1 1 auto;
|
||||
max-height: 50px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
text-align: center;
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
height: 50px;
|
||||
}
|
||||
img {
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
div.content-ban-message {
|
||||
flex: 1 1 auto;
|
||||
max-height: calc(100% - 50px);
|
||||
overflow: auto;
|
||||
|
||||
p {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
div.footer-ban-message {
|
||||
height: 50px;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
|
||||
button {
|
||||
width: 88px;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.content-ban-message {
|
||||
flex: 1 1 auto;
|
||||
max-height: calc(100% - 50px);
|
||||
overflow: auto;
|
||||
|
||||
p {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
div.footer-ban-message {
|
||||
height: 50px;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
|
||||
button {
|
||||
width: 88px;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1,59 +1,64 @@
|
||||
<script lang="ts">
|
||||
import { fly } from "svelte/transition";
|
||||
import {textMessageContentStore, textMessageVisibleStore} from "../../Stores/TypeMessageStore/TextMessageStore";
|
||||
import { textMessageContentStore, textMessageVisibleStore } from "../../Stores/TypeMessageStore/TextMessageStore";
|
||||
import { QuillDeltaToHtmlConverter } from "quill-delta-to-html";
|
||||
|
||||
const content = JSON.parse($textMessageContentStore);
|
||||
const converter = new QuillDeltaToHtmlConverter(content.ops, {inlineStyles: true});
|
||||
const NAME_BUTTON = 'Ok';
|
||||
let converter: QuillDeltaToHtmlConverter;
|
||||
$: {
|
||||
const content = JSON.parse($textMessageContentStore);
|
||||
converter = new QuillDeltaToHtmlConverter(content.ops, { inlineStyles: true });
|
||||
}
|
||||
const NAME_BUTTON = "Ok";
|
||||
|
||||
function closeTextMessage() {
|
||||
textMessageVisibleStore.set(false);
|
||||
}
|
||||
|
||||
function onKeyDown(e:KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
closeTextMessage();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown}/>
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
<div class="main-text-message nes-container is-rounded" transition:fly="{{ x: -1000, duration: 500 }}">
|
||||
<div class="main-text-message nes-container is-rounded" transition:fly={{ x: -1000, duration: 500 }}>
|
||||
<div class="content-text-message">
|
||||
{@html converter.convert()}
|
||||
</div>
|
||||
<div class="footer-text-message">
|
||||
<button type="button" class="nes-btn is-primary" on:click|preventDefault={closeTextMessage}>{NAME_BUTTON}</button>
|
||||
<button type="button" class="nes-btn is-primary" on:click|preventDefault={closeTextMessage}
|
||||
>{NAME_BUTTON}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
div.main-text-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
div.main-text-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
max-height: 25vh;
|
||||
width: 80vw;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
padding-bottom: 0;
|
||||
max-height: 25vh;
|
||||
width: 80vw;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
padding-bottom: 0;
|
||||
|
||||
pointer-events: auto;
|
||||
background-color: #333333;
|
||||
pointer-events: auto;
|
||||
background-color: #333333;
|
||||
|
||||
div.content-text-message {
|
||||
flex: 1 1 auto;
|
||||
max-height: calc(100% - 50px);
|
||||
color: whitesmoke;
|
||||
div.content-text-message {
|
||||
flex: 1 1 auto;
|
||||
max-height: calc(100% - 50px);
|
||||
color: whitesmoke;
|
||||
|
||||
overflow: auto;
|
||||
}
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
div.footer-text-message {
|
||||
height: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
div.footer-text-message {
|
||||
height: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition';
|
||||
import { fly } from "svelte/transition";
|
||||
import megaphoneImg from "./images/megaphone.svg";
|
||||
import {soundPlayingStore} from "../../Stores/SoundPlayingStore";
|
||||
import {afterUpdate} from "svelte";
|
||||
import { soundPlayingStore } from "../../Stores/SoundPlayingStore";
|
||||
import { afterUpdate } from "svelte";
|
||||
|
||||
export let url: string;
|
||||
let audio: HTMLAudioElement;
|
||||
@ -16,37 +16,37 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="audio-playing" transition:fly="{{ x: 210, duration: 500 }}">
|
||||
<div class="audio-playing" transition:fly={{ x: 210, duration: 500 }}>
|
||||
<img src={megaphoneImg} alt="Audio playing" />
|
||||
<p>Audio message</p>
|
||||
<audio bind:this={audio} src={url} on:ended={soundEnded} >
|
||||
<track kind="captions">
|
||||
<audio bind:this={audio} src={url} on:ended={soundEnded}>
|
||||
<track kind="captions" />
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
/*audio html when audio message playing*/
|
||||
.audio-playing {
|
||||
position: absolute;
|
||||
width: 200px;
|
||||
height: 54px;
|
||||
right: 0;
|
||||
top: 40px;
|
||||
transition: all 0.1s ease-out;
|
||||
background-color: black;
|
||||
border-radius: 30px 0 0 30px;
|
||||
display: inline-flex;
|
||||
/*audio html when audio message playing*/
|
||||
.audio-playing {
|
||||
position: absolute;
|
||||
width: 200px;
|
||||
height: 54px;
|
||||
right: 0;
|
||||
top: 40px;
|
||||
transition: all 0.1s ease-out;
|
||||
background-color: black;
|
||||
border-radius: 30px 0 0 30px;
|
||||
display: inline-flex;
|
||||
|
||||
img {
|
||||
border-radius: 50%;
|
||||
background-color: #ffda01;
|
||||
padding: 10px;
|
||||
}
|
||||
img {
|
||||
border-radius: 50%;
|
||||
background-color: #ffda01;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: white;
|
||||
margin-left: 10px;
|
||||
margin-top: 14px;
|
||||
p {
|
||||
color: white;
|
||||
margin-left: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,48 +1,49 @@
|
||||
<script lang="ts">
|
||||
import {errorStore} from "../../Stores/ErrorStore";
|
||||
|
||||
function close(): boolean {
|
||||
errorStore.clearMessages();
|
||||
return false;
|
||||
}
|
||||
import { errorStore, hasClosableMessagesInErrorStore } from "../../Stores/ErrorStore";
|
||||
|
||||
function close(): boolean {
|
||||
errorStore.clearClosableMessages();
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="error-div nes-container is-dark is-rounded" open>
|
||||
<p class="nes-text is-error title">Error</p>
|
||||
<div class="body">
|
||||
{#each $errorStore as error}
|
||||
<p>{error}</p>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
<button class="nes-btn is-error" on:click={close}>Close</button>
|
||||
{#each $errorStore as error}
|
||||
<p>{error.message}</p>
|
||||
{/each}
|
||||
</div>
|
||||
{#if $hasClosableMessagesInErrorStore}
|
||||
<div class="button-bar">
|
||||
<button class="nes-btn is-error" on:click={close}>Close</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
div.error-div {
|
||||
pointer-events: auto;
|
||||
margin-top: 10vh;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
width: max-content;
|
||||
max-width: 80vw;
|
||||
pointer-events: auto;
|
||||
margin-top: 10vh;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
width: max-content;
|
||||
max-width: 80vw;
|
||||
|
||||
.button-bar {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.body {
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: "Press Start 2P";
|
||||
|
||||
&.title {
|
||||
text-align: center;
|
||||
.button-bar {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.body {
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: "Press Start 2P";
|
||||
|
||||
&.title {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,21 +1,21 @@
|
||||
<script lang="ts">
|
||||
import {streamableCollectionStore} from "../../Stores/StreamableCollectionStore";
|
||||
import {afterUpdate, onDestroy} from "svelte";
|
||||
import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore";
|
||||
import { streamableCollectionStore } from "../../Stores/StreamableCollectionStore";
|
||||
import { afterUpdate, onDestroy } from "svelte";
|
||||
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
|
||||
import MediaBox from "./MediaBox.svelte";
|
||||
|
||||
let cssClass = 'one-col';
|
||||
let cssClass = "one-col";
|
||||
|
||||
const unsubscribe = streamableCollectionStore.subscribe((displayableMedias) => {
|
||||
const nbUsers = displayableMedias.size;
|
||||
if (nbUsers <= 1) {
|
||||
cssClass = 'one-col';
|
||||
cssClass = "one-col";
|
||||
} else if (nbUsers <= 4) {
|
||||
cssClass = 'two-col';
|
||||
cssClass = "two-col";
|
||||
} else if (nbUsers <= 9) {
|
||||
cssClass = 'three-col';
|
||||
cssClass = "three-col";
|
||||
} else {
|
||||
cssClass = 'four-col';
|
||||
cssClass = "four-col";
|
||||
}
|
||||
});
|
||||
|
||||
@ -25,11 +25,11 @@
|
||||
|
||||
afterUpdate(() => {
|
||||
biggestAvailableAreaStore.recompute();
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="chat-mode {cssClass}">
|
||||
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
|
||||
<MediaBox streamable={peer}></MediaBox>
|
||||
<MediaBox streamable={peer} />
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -1,16 +1,15 @@
|
||||
<script lang="typescript">
|
||||
import type {ScreenSharingLocalMedia} from "../../Stores/ScreenSharingStore";
|
||||
import {videoFocusStore} from "../../Stores/VideoFocusStore";
|
||||
import {srcObject} from "./utils";
|
||||
import type { ScreenSharingLocalMedia } from "../../Stores/ScreenSharingStore";
|
||||
import { videoFocusStore } from "../../Stores/VideoFocusStore";
|
||||
import { srcObject } from "./utils";
|
||||
|
||||
export let peer : ScreenSharingLocalMedia;
|
||||
export let peer: ScreenSharingLocalMedia;
|
||||
let stream = peer.stream;
|
||||
export let cssClass : string|undefined;
|
||||
export let cssClass: string | undefined;
|
||||
</script>
|
||||
|
||||
|
||||
<div class="video-container {cssClass ? cssClass : ''}" class:hide={!stream}>
|
||||
{#if stream}
|
||||
<video use:srcObject={stream} autoplay muted playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
|
||||
<video use:srcObject={stream} autoplay muted playsinline on:click={() => videoFocusStore.toggleFocus(peer)} />
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -1,20 +1,20 @@
|
||||
<script lang="ts">
|
||||
import {VideoPeer} from "../../WebRtc/VideoPeer";
|
||||
import { VideoPeer } from "../../WebRtc/VideoPeer";
|
||||
import VideoMediaBox from "./VideoMediaBox.svelte";
|
||||
import ScreenSharingMediaBox from "./ScreenSharingMediaBox.svelte";
|
||||
import {ScreenSharingPeer} from "../../WebRtc/ScreenSharingPeer";
|
||||
import { ScreenSharingPeer } from "../../WebRtc/ScreenSharingPeer";
|
||||
import LocalStreamMediaBox from "./LocalStreamMediaBox.svelte";
|
||||
import type {Streamable} from "../../Stores/StreamableCollectionStore";
|
||||
import type { Streamable } from "../../Stores/StreamableCollectionStore";
|
||||
|
||||
export let streamable: Streamable;
|
||||
</script>
|
||||
|
||||
<div class="media-container">
|
||||
{#if streamable instanceof VideoPeer}
|
||||
<VideoMediaBox peer={streamable}/>
|
||||
<VideoMediaBox peer={streamable} />
|
||||
{:else if streamable instanceof ScreenSharingPeer}
|
||||
<ScreenSharingMediaBox peer={streamable}/>
|
||||
<ScreenSharingMediaBox peer={streamable} />
|
||||
{:else}
|
||||
<LocalStreamMediaBox peer={streamable} cssClass=""/>
|
||||
<LocalStreamMediaBox peer={streamable} cssClass="" />
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import {streamableCollectionStore} from "../../Stores/StreamableCollectionStore";
|
||||
import {videoFocusStore} from "../../Stores/VideoFocusStore";
|
||||
import {afterUpdate} from "svelte";
|
||||
import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore";
|
||||
import { streamableCollectionStore } from "../../Stores/StreamableCollectionStore";
|
||||
import { videoFocusStore } from "../../Stores/VideoFocusStore";
|
||||
import { afterUpdate } from "svelte";
|
||||
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
|
||||
import MediaBox from "./MediaBox.svelte";
|
||||
|
||||
afterUpdate(() => {
|
||||
biggestAvailableAreaStore.recompute();
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="main-section">
|
||||
{#if $videoFocusStore }
|
||||
{#if $videoFocusStore}
|
||||
{#key $videoFocusStore.uniqueId}
|
||||
<MediaBox streamable={$videoFocusStore}></MediaBox>
|
||||
<MediaBox streamable={$videoFocusStore} />
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
<aside class="sidebar">
|
||||
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
|
||||
{#if peer !== $videoFocusStore }
|
||||
<MediaBox streamable={peer}></MediaBox>
|
||||
{#if peer !== $videoFocusStore}
|
||||
<MediaBox streamable={peer} />
|
||||
{/if}
|
||||
{/each}
|
||||
</aside>
|
||||
|
@ -1,33 +1,33 @@
|
||||
<script lang="ts">
|
||||
import type {ScreenSharingPeer} from "../../WebRtc/ScreenSharingPeer";
|
||||
import {videoFocusStore} from "../../Stores/VideoFocusStore";
|
||||
import {getColorByString, srcObject} from "./utils";
|
||||
import type { ScreenSharingPeer } from "../../WebRtc/ScreenSharingPeer";
|
||||
import { videoFocusStore } from "../../Stores/VideoFocusStore";
|
||||
import { getColorByString, srcObject } from "./utils";
|
||||
|
||||
export let peer: ScreenSharingPeer;
|
||||
let streamStore = peer.streamStore;
|
||||
let name = peer.userName;
|
||||
let statusStore = peer.statusStore;
|
||||
|
||||
</script>
|
||||
|
||||
<div class="video-container">
|
||||
{#if $statusStore === 'connecting'}
|
||||
<div class="connecting-spinner"></div>
|
||||
{#if $statusStore === "connecting"}
|
||||
<div class="connecting-spinner" />
|
||||
{/if}
|
||||
{#if $statusStore === 'error'}
|
||||
<div class="rtc-error"></div>
|
||||
{#if $statusStore === "error"}
|
||||
<div class="rtc-error" />
|
||||
{/if}
|
||||
{#if $streamStore === null}
|
||||
<i style="background-color: {getColorByString(name)};">{name}</i>
|
||||
{:else}
|
||||
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
|
||||
<!-- svelte-ignore a11y-media-has-caption -->
|
||||
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.video-container {
|
||||
video {
|
||||
width: 100%;
|
||||
.video-container {
|
||||
video {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,12 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type {VideoPeer} from "../../WebRtc/VideoPeer";
|
||||
import type { VideoPeer } from "../../WebRtc/VideoPeer";
|
||||
import SoundMeterWidget from "../SoundMeterWidget.svelte";
|
||||
import microphoneCloseImg from "../images/microphone-close.svg";
|
||||
import reportImg from "./images/report.svg";
|
||||
import blockSignImg from "./images/blockSign.svg";
|
||||
import {videoFocusStore} from "../../Stores/VideoFocusStore";
|
||||
import {showReportScreenStore} from "../../Stores/ShowReportScreenStore";
|
||||
import {getColorByString, srcObject} from "./utils";
|
||||
import { videoFocusStore } from "../../Stores/VideoFocusStore";
|
||||
import { showReportScreenStore } from "../../Stores/ShowReportScreenStore";
|
||||
import { getColorByString, srcObject } from "./utils";
|
||||
|
||||
import Woka from "../Woka/Woka.svelte";
|
||||
|
||||
export let peer: VideoPeer;
|
||||
let streamStore = peer.streamStore;
|
||||
@ -15,32 +17,55 @@
|
||||
let constraintStore = peer.constraintsStore;
|
||||
|
||||
function openReport(peer: VideoPeer): void {
|
||||
showReportScreenStore.set({ userId:peer.userId, userName: peer.userName });
|
||||
showReportScreenStore.set({ userId: peer.userId, userName: peer.userName });
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="video-container">
|
||||
{#if $statusStore === 'connecting'}
|
||||
<div class="connecting-spinner"></div>
|
||||
{#if $statusStore === "connecting"}
|
||||
<div class="connecting-spinner" />
|
||||
{/if}
|
||||
{#if $statusStore === 'error'}
|
||||
<div class="rtc-error"></div>
|
||||
{/if}
|
||||
{#if !$constraintStore || $constraintStore.video === false}
|
||||
<i style="background-color: {getColorByString(name)};">{name}</i>
|
||||
{#if $statusStore === "error"}
|
||||
<div class="rtc-error" />
|
||||
{/if}
|
||||
<!-- {#if !$constraintStore || $constraintStore.video === false} -->
|
||||
<i
|
||||
class="container {!$constraintStore || $constraintStore.video === false ? '' : 'minimized'}"
|
||||
style="background-color: {getColorByString(name)};"
|
||||
>
|
||||
<span>{peer.userName}</span>
|
||||
<div class="woka-icon"><Woka userId={peer.userId} placeholderSrc={""} /></div>
|
||||
</i>
|
||||
<!-- {/if} -->
|
||||
{#if $constraintStore && $constraintStore.audio === false}
|
||||
<img src={microphoneCloseImg} class="active" alt="Muted">
|
||||
<img src={microphoneCloseImg} class="active" alt="Muted" />
|
||||
{/if}
|
||||
<button class="report" on:click={() => openReport(peer)}>
|
||||
<img alt="Report this user" src={reportImg}>
|
||||
<img alt="Report this user" src={reportImg} />
|
||||
<span>Report/Block</span>
|
||||
</button>
|
||||
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
|
||||
<!-- svelte-ignore a11y-media-has-caption -->
|
||||
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)} />
|
||||
<img src={blockSignImg} class="block-logo" alt="Block" />
|
||||
{#if $constraintStore && $constraintStore.audio !== false}
|
||||
<SoundMeterWidget stream={$streamStore}></SoundMeterWidget>
|
||||
<SoundMeterWidget stream={$streamStore} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.minimized {
|
||||
left: auto;
|
||||
transform: scale(0.5);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.woka-icon {
|
||||
margin-right: 3px;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,23 +1,22 @@
|
||||
<script lang="ts">
|
||||
import {LayoutMode} from "../../WebRtc/LayoutManager";
|
||||
import {layoutModeStore} from "../../Stores/StreamableCollectionStore";
|
||||
import { LayoutMode } from "../../WebRtc/LayoutManager";
|
||||
import { layoutModeStore } from "../../Stores/StreamableCollectionStore";
|
||||
import PresentationLayout from "./PresentationLayout.svelte";
|
||||
import ChatLayout from "./ChatLayout.svelte";
|
||||
|
||||
</script>
|
||||
|
||||
<div class="video-overlay">
|
||||
{#if $layoutModeStore === LayoutMode.Presentation }
|
||||
{#if $layoutModeStore === LayoutMode.Presentation}
|
||||
<PresentationLayout />
|
||||
{:else }
|
||||
{:else}
|
||||
<ChatLayout />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.video-overlay {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<script lang="typescript">
|
||||
import { fly } from 'svelte/transition';
|
||||
import {requestVisitCardsStore} from "../../Stores/GameStore";
|
||||
import {onMount} from "svelte";
|
||||
import { fly } from "svelte/transition";
|
||||
import { requestVisitCardsStore } from "../../Stores/GameStore";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let visitCardUrl: string;
|
||||
let w = '500px';
|
||||
let h = '250px';
|
||||
let w = "500px";
|
||||
let h = "250px";
|
||||
let hidden = true;
|
||||
let cvIframe: HTMLIFrameElement;
|
||||
|
||||
@ -13,73 +13,81 @@
|
||||
requestVisitCardsStore.set(null);
|
||||
}
|
||||
|
||||
function handleIframeMessage(message:any) {
|
||||
if (message.data.type === 'cvIframeSize') {
|
||||
w = (message.data.data.w) + 'px';
|
||||
h = (message.data.data.h) + 'px';
|
||||
function handleIframeMessage(message: MessageEvent) {
|
||||
if (message.data.type === "cvIframeSize") {
|
||||
w = message.data.data.w + "px";
|
||||
h = message.data.data.h + "px";
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
cvIframe.onload = () => hidden = false
|
||||
cvIframe.onerror = () => hidden = false
|
||||
})
|
||||
cvIframe.onload = () => (hidden = false);
|
||||
cvIframe.onerror = () => (hidden = false);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.loader {
|
||||
border: 16px solid #f3f3f3; /* Light grey */
|
||||
border-top: 16px solid #3498db; /* Blue */
|
||||
border-radius: 50%;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin:auto;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.visitCard {
|
||||
pointer-events: all;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
margin-top: 200px;
|
||||
max-width: 80vw;
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
max-width: 80vw;
|
||||
overflow: hidden;
|
||||
|
||||
&.hidden {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<section class="visitCard" transition:fly="{{ y: -200, duration: 1000 }}" style="width: {w}">
|
||||
<section class="visitCard" transition:fly={{ y: -200, duration: 1000 }} style="width: {w}">
|
||||
{#if hidden}
|
||||
<div class="loader"></div>
|
||||
<div class="loader" />
|
||||
{/if}
|
||||
<iframe title="visitCard" src={visitCardUrl} allow="clipboard-read; clipboard-write self {visitCardUrl}" style="width: {w}; height: {h}" class:hidden={hidden} bind:this={cvIframe}></iframe>
|
||||
<iframe
|
||||
title="visitCard"
|
||||
src={visitCardUrl}
|
||||
allow="clipboard-read; clipboard-write self {visitCardUrl}"
|
||||
style="width: {w}; height: {h}"
|
||||
class:hidden
|
||||
bind:this={cvIframe}
|
||||
/>
|
||||
{#if !hidden}
|
||||
<div class="buttonContainer">
|
||||
<button class="nes-btn is-popUpElement" on:click={closeCard}>Close</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</section>
|
||||
|
||||
<svelte:window on:message={handleIframeMessage}/>
|
||||
<svelte:window on:message={handleIframeMessage} />
|
||||
|
||||
<style lang="scss">
|
||||
.loader {
|
||||
border: 16px solid #f3f3f3; /* Light grey */
|
||||
border-top: 16px solid #3498db; /* Blue */
|
||||
border-radius: 50%;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: auto;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.visitCard {
|
||||
pointer-events: all;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
margin-top: 200px;
|
||||
max-width: 80vw;
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
max-width: 80vw;
|
||||
overflow: hidden;
|
||||
|
||||
&.hidden {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,37 +1,39 @@
|
||||
<script lang="typescript">
|
||||
import { fly } from 'svelte/transition';
|
||||
import {userIsAdminStore} from "../../Stores/GameStore";
|
||||
import {ADMIN_URL} from "../../Enum/EnvironmentVariable";
|
||||
|
||||
const upgradeLink = ADMIN_URL+'/pricing';
|
||||
|
||||
import { fly } from "svelte/transition";
|
||||
import { userIsAdminStore } from "../../Stores/GameStore";
|
||||
import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
|
||||
|
||||
const upgradeLink = ADMIN_URL + "/pricing";
|
||||
</script>
|
||||
|
||||
<main class="warningMain" transition:fly="{{ y: -200, duration: 500 }}">
|
||||
<main class="warningMain" transition:fly={{ y: -200, duration: 500 }}>
|
||||
<h2>Warning!</h2>
|
||||
{#if $userIsAdminStore}
|
||||
<p>This world is close to its limit!. You can upgrade its capacity <a href="{upgradeLink}" target="_blank">here</a></p>
|
||||
<p>
|
||||
This world is close to its limit!. You can upgrade its capacity <a href={upgradeLink} target="_blank"
|
||||
>here</a
|
||||
>
|
||||
</p>
|
||||
{:else}
|
||||
<p>This world is close to its limit!</p>
|
||||
{/if}
|
||||
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<style lang="scss">
|
||||
main.warningMain {
|
||||
pointer-events: auto;
|
||||
width: 100vw;
|
||||
background-color: red;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
font-family: Lato;
|
||||
min-width: 300px;
|
||||
opacity: 0.9;
|
||||
z-index: 2;
|
||||
main.warningMain {
|
||||
pointer-events: auto;
|
||||
width: 100vw;
|
||||
background-color: red;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
font-family: Lato;
|
||||
min-width: 300px;
|
||||
opacity: 0.9;
|
||||
z-index: 2;
|
||||
h2 {
|
||||
padding: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|