From 7c89cf0e479c5846881041112009b0dfb58e4052 Mon Sep 17 00:00:00 2001 From: Oliver Lorenz Date: Tue, 29 Dec 2020 23:17:16 +0100 Subject: [PATCH 01/13] feat: adds property openWebsitePolicy to set allow property in iframe --- front/src/Phaser/Game/GameScene.ts | 2 +- front/src/WebRtc/CoWebsiteManager.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 67380309..8e037eab 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -604,7 +604,7 @@ export class GameScene extends ResizableScene implements CenterListener { coWebsiteManager.closeCoWebsite(); }else{ const openWebsiteFunction = () => { - coWebsiteManager.loadCoWebsite(newValue as string); + coWebsiteManager.loadCoWebsite(newValue as string, allProps.get('openWebsitePolicy') as string | undefined); layoutManager.removeActionButton('openWebsite', this.userInputManager); }; diff --git a/front/src/WebRtc/CoWebsiteManager.ts b/front/src/WebRtc/CoWebsiteManager.ts index e9564222..ef73ac1d 100644 --- a/front/src/WebRtc/CoWebsiteManager.ts +++ b/front/src/WebRtc/CoWebsiteManager.ts @@ -42,7 +42,7 @@ class CoWebsiteManager { this.opened = iframeStates.opened; } - public loadCoWebsite(url: string): void { + public loadCoWebsite(url: string, allowPolicy?: string): void { this.load(); this.cowebsiteDiv.innerHTML = ` + + + diff --git a/front/dist/resources/logos/report.back.svg b/front/dist/resources/logos/report.back.svg new file mode 100644 index 00000000..1cb3b068 --- /dev/null +++ b/front/dist/resources/logos/report.back.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/dist/resources/logos/report.svg b/front/dist/resources/logos/report.svg index 1cb3b068..14753256 100644 --- a/front/dist/resources/logos/report.svg +++ b/front/dist/resources/logos/report.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css index 2e2c6c10..427eff75 100644 --- a/front/dist/resources/style/style.css +++ b/front/dist/resources/style/style.css @@ -55,14 +55,13 @@ body .message-info.warning{ } .video-container img.active{ display: block; + cursor: url('/resources/logos/cursor_pointer.png'), pointer; } .video-container img{ position: absolute; display: none; - width: 15px; - height: 15px; - background: #d93025; - border-radius: 48px; + width: 25px; + height: 25px; left: 5px; bottom: 5px; padding: 10px; diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 405fcb85..2d14babf 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -154,7 +154,7 @@ export class GameScene extends ResizableScene implements CenterListener { private actionableItems: Map = new Map(); // The item that can be selected by pressing the space key. private outlinedItem: ActionableItem|null = null; - private userInputManager!: UserInputManager; + public userInputManager!: UserInputManager; private isReconnecting: boolean = false; private startLayerName!: string | null; private openChatIcon!: OpenChatIcon; diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index 8bd64cd1..6f035446 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -2,7 +2,7 @@ import {LoginScene, LoginSceneName} from "../Login/LoginScene"; import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCharacterScene"; import {gameManager} from "../Game/GameManager"; import {localUserStore} from "../../Connexion/LocalUserStore"; -import {mediaManager} from "../../WebRtc/MediaManager"; +import {mediaManager, ReportCallback, ShowReportCallBack} from "../../WebRtc/MediaManager"; import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager"; export const MenuSceneName = 'MenuScene'; @@ -10,6 +10,7 @@ const gameMenuKey = 'gameMenu'; const gameMenuIconKey = 'gameMenuIcon'; const gameSettingsMenuKey = 'gameSettingsMenu'; const gameShare = 'gameShare'; +const gameReport = 'gameReport'; const closedSideMenuX = -200; const openedSideMenuX = 0; @@ -21,9 +22,11 @@ export class MenuScene extends Phaser.Scene { private menuElement!: Phaser.GameObjects.DOMElement; private gameQualityMenuElement!: Phaser.GameObjects.DOMElement; private gameShareElement!: Phaser.GameObjects.DOMElement; + private gameReportElement!: Phaser.GameObjects.DOMElement; private sideMenuOpened = false; private settingsMenuOpened = false; private gameShareOpened = false; + private gameReportOpened = false; private gameQualityValue: number; private videoQualityValue: number; private menuButton!: Phaser.GameObjects.DOMElement; @@ -40,6 +43,7 @@ export class MenuScene extends Phaser.Scene { this.load.html(gameMenuIconKey, 'resources/html/gameMenuIcon.html'); this.load.html(gameSettingsMenuKey, 'resources/html/gameQualityMenu.html'); this.load.html(gameShare, 'resources/html/gameShare.html'); + this.load.html(gameReport, 'resources/html/gameReport.html'); } create() { @@ -64,6 +68,19 @@ export class MenuScene extends Phaser.Scene { } }); + this.gameReportElement = this.add.dom(middleX, -400).createFromCache(gameReport); + this.revealMenusAfterInit(this.gameReportElement, gameReport); + this.gameReportElement.addListener('click'); + this.gameReportElement.on('click', (event:MouseEvent) => { + event.preventDefault(); + if((event?.target as HTMLInputElement).id === 'gameReportFormSubmit') { + this.submitReport(); + }else if((event?.target as HTMLInputElement).id === 'gameReportFormCancel') { + this.closeGameReport(); + } + }); + mediaManager.setShowReportModalCallBacks(this.openGameReport.bind(this)); + this.input.keyboard.on('keyup-TAB', () => { this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu(); }); @@ -221,6 +238,71 @@ export class MenuScene extends Phaser.Scene { }); } + private openGameReport(userId: string, userName: string|undefined){ + if (this.gameReportOpened) { + this.closeGameReport(); + return; + } + + //close all + this.closeAll(); + + const gameTitleReport = this.gameReportElement.getChildByID('nameReported') as HTMLElement; + gameTitleReport.innerText = userName ? `Report user: ${userName}` : 'Report user'; + const gameIdUserReported = this.gameReportElement.getChildByID('idUserReported') as HTMLInputElement; + gameIdUserReported.value = userId; + + this.gameReportOpened = true; + let middleY = (window.innerHeight / 3) - (257); + if(middleY < 0){ + middleY = 0; + } + let middleX = (window.innerWidth / 3) - 298; + if(middleX < 0){ + middleX = 0; + } + + gameManager.getCurrentGameScene(this).userInputManager.clearAllInputKeyboard(); + + this.tweens.add({ + targets: this.gameReportElement, + y: middleY, + x: middleX, + duration: 1000, + ease: 'Power3' + }); + return; + } + + private closeGameReport(): void{ + this.gameReportOpened = false; + gameManager.getCurrentGameScene(this).userInputManager.initKeyBoardEvent(); + this.tweens.add({ + targets: this.gameReportElement, + y: -400, + duration: 1000, + ease: 'Power3' + }); + } + + private submitReport(): void{ + const gamePError = this.gameReportElement.getChildByID('gameReportErr') as HTMLParagraphElement; + gamePError.innerText = ''; + gamePError.style.display = 'none'; + const gameTextArea = this.gameReportElement.getChildByID('gameReportInput') as HTMLInputElement; + const gameIdUserReported = this.gameReportElement.getChildByID('idUserReported') as HTMLInputElement; + if(!gameTextArea || !gameTextArea.value || !gameIdUserReported || !gameIdUserReported.value){ + gamePError.innerText = 'Report message cannot to be empty.'; + gamePError.style.display = 'block'; + return; + } + gameManager.getCurrentGameScene(this).connection.emitReportPlayerMessage( + parseInt(gameIdUserReported.value), + gameTextArea.value + ); + this.closeGameReport(); + } + private onMenuClick(event:MouseEvent) { event.preventDefault(); @@ -280,5 +362,6 @@ export class MenuScene extends Phaser.Scene { private closeAll(){ this.closeGameQualityMenu(); this.closeGameShare(); + this.closeGameReport(); } } diff --git a/front/src/WebRtc/DiscussionManager.ts b/front/src/WebRtc/DiscussionManager.ts index 7653bc7a..1bd488e9 100644 --- a/front/src/WebRtc/DiscussionManager.ts +++ b/front/src/WebRtc/DiscussionManager.ts @@ -1,5 +1,5 @@ import {HtmlUtils} from "./HtmlUtils"; -import {mediaManager, ReportCallback} from "./MediaManager"; +import {mediaManager, ReportCallback, ShowReportCallBack} from "./MediaManager"; import {UserInputManager} from "../Phaser/UserInput/UserInputManager"; import {connectionManager} from "../Connexion/ConnectionManager"; import {GameConnexionTypes} from "../Url/UrlManager"; @@ -99,7 +99,7 @@ export class DiscussionManager { name: string|undefined, img?: string|undefined, isMe: boolean = false, - reportCallback?: ReportCallback + showReportCallBack?: ShowReportCallBack ) { const divParticipant: HTMLDivElement = document.createElement('div'); divParticipant.classList.add('participant'); @@ -128,8 +128,8 @@ export class DiscussionManager { reportBanUserAction.classList.add('report-btn') reportBanUserAction.innerText = 'Report'; reportBanUserAction.addEventListener('click', () => { - if(reportCallback) { - mediaManager.showReportModal(`${userId}`, name ?? '', reportCallback); + if(showReportCallBack) { + showReportCallBack(`${userId}`, name); }else{ console.info('report feature is not activated!'); } diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index 1a84d4a9..054f0fe2 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -3,6 +3,8 @@ import {HtmlUtils} from "./HtmlUtils"; import {discussionManager, SendMessageCallback} from "./DiscussionManager"; import {UserInputManager} from "../Phaser/UserInput/UserInputManager"; import {VIDEO_QUALITY_SELECT} from "../Administration/ConsoleGlobalMessageManager"; +import {connectionManager} from "../Connexion/ConnectionManager"; +import {GameConnexionTypes} from "../Url/UrlManager"; declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any const localValueVideo = localStorage.getItem(VIDEO_QUALITY_SELECT); @@ -23,6 +25,7 @@ export type UpdatedLocalStreamCallback = (media: MediaStream|null) => void; export type StartScreenSharingCallback = (media: MediaStream) => void; export type StopScreenSharingCallback = (media: MediaStream) => void; export type ReportCallback = (message: string) => void; +export type ShowReportCallBack = (userId: string, userName: string|undefined) => void; // TODO: Split MediaManager in 2 classes: MediaManagerUI (in charge of HTML) and MediaManager (singleton in charge of the camera only) // TODO: verify that microphone event listeners are not triggered plenty of time NOW (since MediaManager is created many times!!!!) @@ -46,6 +49,7 @@ export class MediaManager { updatedLocalStreamCallBacks : Set = new Set(); startScreenSharingCallBacks : Set = new Set(); stopScreenSharingCallBacks : Set = new Set(); + showReportModalCallBacks : Set = new Set(); private microphoneBtn: HTMLDivElement; private cinemaBtn: HTMLDivElement; private monitorBtn: HTMLDivElement; @@ -469,7 +473,7 @@ export class MediaManager { return this.getCamera(); } - addActiveVideo(userId: string, reportCallBack: ReportCallback|undefined, userName: string = ""){ + addActiveVideo(userId: string, userName: string = "", anonymous: boolean = true){ this.webrtcInAudio.play(); userName = userName.toUpperCase(); @@ -482,7 +486,7 @@ export class MediaManager { ${userName} ` + - ((reportCallBack!==undefined)?``:'') + ((anonymous === false)?``:'') + ` @@ -490,18 +494,22 @@ export class MediaManager { layoutManager.add(DivImportance.Normal, userId, html); - if (reportCallBack) { - const reportBtn = HtmlUtils.getElementByIdOrFail(`report-${userId}`); - reportBtn.addEventListener('click', (e: MouseEvent) => { - e.preventDefault(); - this.showReportModal(userId, userName, reportCallBack); - }); - } - this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail(userId)); //permit to create participant in discussion part - this.addNewParticipant(userId, userName, undefined, reportCallBack); + const showReportUser = () => { + for(const callBack of this.showReportModalCallBacks){ + callBack(userId, userName); + } + }; + this.addNewParticipant(userId, userName, undefined, showReportUser); + + if(!anonymous){ + const reportBanUserAction: HTMLImageElement = HtmlUtils.getElementByIdOrFail(`report-${userId}`); + reportBanUserAction.addEventListener('click', () => { + showReportUser(); + }); + } } addScreenSharingActiveVideo(userId: string, divImportance: DivImportance = DivImportance.Important){ @@ -645,65 +653,8 @@ export class MediaManager { return color; } - public showReportModal(userId: string, userName: string, reportCallBack: ReportCallback){ - //create report text area - const mainContainer = HtmlUtils.getElementByIdOrFail('main-container'); - - const divReport = document.createElement('div'); - divReport.classList.add('modal-report-user'); - - const inputHidden = document.createElement('input'); - inputHidden.id = 'input-report-user'; - inputHidden.type = 'hidden'; - inputHidden.value = userId; - divReport.appendChild(inputHidden); - - const titleMessage = document.createElement('p'); - titleMessage.id = 'title-report-user'; - titleMessage.innerText = 'Open a report'; - divReport.appendChild(titleMessage); - - const bodyMessage = document.createElement('p'); - bodyMessage.id = 'body-report-user'; - bodyMessage.innerText = `You are about to open a report regarding an offensive conduct from user ${userName.toUpperCase()}. Please explain to us how you think ${userName.toUpperCase()} breached the code of conduct.`; - divReport.appendChild(bodyMessage); - - const imgReportUser = document.createElement('img'); - imgReportUser.id = 'img-report-user'; - imgReportUser.src = 'resources/logos/report.svg'; - divReport.appendChild(imgReportUser); - - const textareaUser = document.createElement('textarea'); - textareaUser.id = 'textarea-report-user'; - textareaUser.placeholder = 'Write ...'; - divReport.appendChild(textareaUser); - - const buttonReport = document.createElement('button'); - buttonReport.id = 'button-save-report-user'; - buttonReport.innerText = 'Report'; - buttonReport.addEventListener('click', () => { - if(!textareaUser.value){ - textareaUser.style.border = '1px solid red' - return; - } - reportCallBack(textareaUser.value); - divReport.remove(); - }); - divReport.appendChild(buttonReport); - - const buttonCancel = document.createElement('img'); - buttonCancel.id = 'cancel-report-user'; - buttonCancel.src = 'resources/logos/close.svg'; - buttonCancel.addEventListener('click', () => { - divReport.remove(); - }); - divReport.appendChild(buttonCancel); - - mainContainer.appendChild(divReport); - } - - public addNewParticipant(userId: number|string, name: string|undefined, img?: string, reportCallBack?: ReportCallback){ - discussionManager.addParticipant(userId, name, img, false, reportCallBack); + public addNewParticipant(userId: number|string, name: string|undefined, img?: string, showReportUserCallBack?: ShowReportCallBack){ + discussionManager.addParticipant(userId, name, img, false, showReportUserCallBack); } public removeParticipant(userId: number|string){ @@ -769,6 +720,10 @@ export class MediaManager { this.checkActiveUser(); }, this.focused ? 10000 : 1000); } + + public setShowReportModalCallBacks(callback: ShowReportCallBack){ + this.showReportModalCallBacks.add(callback); + } } export const mediaManager = new MediaManager(); diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index 90d260ee..bc2590d7 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -11,6 +11,8 @@ import { import {ScreenSharingPeer} from "./ScreenSharingPeer"; import {MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer} from "./VideoPeer"; import {RoomConnection} from "../Connexion/RoomConnection"; +import {connectionManager} from "../Connexion/ConnectionManager"; +import {GameConnexionTypes} from "../Url/UrlManager"; export interface UserSimplePeerInterface{ userId: number; @@ -134,11 +136,7 @@ export class SimplePeer { mediaManager.removeActiveVideo("" + user.userId); - const reportCallback = this.enableReporting ? (comment: string) => { - this.reportUser(user.userId, comment); - } : undefined; - - mediaManager.addActiveVideo("" + user.userId, reportCallback, name); + mediaManager.addActiveVideo("" + user.userId, name, connectionManager.getConnexionType === GameConnexionTypes.anonymous); const peer = new VideoPeer(user.userId, user.initiator ? user.initiator : false, this.Connection); @@ -391,13 +389,6 @@ export class SimplePeer { } } - /** - * Triggered locally when clicking on the report button - */ - public reportUser(userId: number, message: string) { - this.Connection.emitReportPlayerMessage(userId, message) - } - private sendLocalScreenSharingStreamToUser(userId: number): void { // If a connection already exists with user (because it is already sharing a screen with us... let's use this connection) if (this.PeerScreenSharingConnectionArray.has(userId)) { diff --git a/pusher/src/Services/AdminApi.ts b/pusher/src/Services/AdminApi.ts index 48e8a1a4..e9bccef8 100644 --- a/pusher/src/Services/AdminApi.ts +++ b/pusher/src/Services/AdminApi.ts @@ -105,11 +105,12 @@ class AdminApi { return res.data; } - reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string) { + reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string, reportWorldSlug: string) { return Axios.post(`${ADMIN_API_URL}/api/report`, { reportedUserUuid, reportedUserComment, reporterUserUuid, + reportWorldSlug }, { headers: {"Authorization": `${ADMIN_API_TOKEN}`} diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index 2f86ae19..7bd50b32 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -304,7 +304,7 @@ export class SocketManager implements ZoneEventListener { throw 'reported socket user not found'; } //TODO report user on admin application - await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid) + await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid, client.roomId.split('/')[2]) } catch (e) { console.error('An error occurred on "handleReportMessage"'); console.error(e); From ac2bc76239a3b0fa4cc6b3ffc46dc9f666e8e375 Mon Sep 17 00:00:00 2001 From: Gregoire Parant Date: Sat, 30 Jan 2021 14:08:11 +0100 Subject: [PATCH 06/13] change style report button --- front/dist/resources/style/style.css | 51 ++++++++++++++++++++++++---- front/src/WebRtc/MediaManager.ts | 11 ++++-- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css index 427eff75..1eb8440d 100644 --- a/front/dist/resources/style/style.css +++ b/front/dist/resources/style/style.css @@ -39,6 +39,7 @@ body .message-info.warning{ position: relative; transition: all 0.2s ease; background-color: #00000099; + cursor: url('/resources/logos/cursor_pointer.png'), pointer; } .video-container i{ position: absolute; @@ -53,10 +54,7 @@ body .message-info.warning{ font-size: 28px; color: white; } -.video-container img.active{ - display: block; - cursor: url('/resources/logos/cursor_pointer.png'), pointer; -} + .video-container img{ position: absolute; display: none; @@ -68,9 +66,50 @@ body .message-info.warning{ z-index: 2; } -.video-container img.report{ +.video-container button.report{ + display: block; + cursor: url('/resources/logos/cursor_pointer.png'), pointer; + background: none; + background-color: rgba(0, 0, 0, 0); + border: none; + background-color: black; + border-radius: 15px; + position: absolute; + width: 0px; + height: 35px; right: 5px; - left: auto; + bottom: 5px; + padding: 0px; + overflow: hidden; + z-index: 2; + transition: all .5s ease; +} + +.video-container:hover button.report{ + width: 35px; + padding: 10px; +} + +.video-container button.report:hover { + width: 94px; +} + +.video-container button.report img{ + position: absolute; + display: block; + bottom: 5px; + left: 5px; + margin: 0; + padding: 0; + cursor: url('/resources/logos/cursor_pointer.png'), pointer; +} +.video-container button.report span{ + position: absolute; + bottom: 8px; + left: 36px; + color: white; + font-size: 16px; + cursor: url('/resources/logos/cursor_pointer.png'), pointer; } .video-container video{ diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index 054f0fe2..78749491 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -486,7 +486,13 @@ export class MediaManager { ${userName} ` + - ((anonymous === false)?``:'') + ((anonymous === false)?` + + `:'' + ) + ` @@ -506,7 +512,8 @@ export class MediaManager { if(!anonymous){ const reportBanUserAction: HTMLImageElement = HtmlUtils.getElementByIdOrFail(`report-${userId}`); - reportBanUserAction.addEventListener('click', () => { + reportBanUserAction.addEventListener('click', (e) => { + e.preventDefault(); showReportUser(); }); } From f99db4856fbc4754f0413111fd752b9f71816776 Mon Sep 17 00:00:00 2001 From: Gregoire Parant Date: Sat, 30 Jan 2021 20:33:31 +0100 Subject: [PATCH 07/13] Add WorkAdventure logo --- front/dist/resources/logos/logo.png | Bin 0 -> 15822 bytes front/src/Phaser/Components/Loader.ts | 20 +++++++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 front/dist/resources/logos/logo.png diff --git a/front/dist/resources/logos/logo.png b/front/dist/resources/logos/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f4440ad51079b55755f5b1f510ed6da89907fcee GIT binary patch literal 15822 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 literal 0 HcmV?d00001 diff --git a/front/src/Phaser/Components/Loader.ts b/front/src/Phaser/Components/Loader.ts index ab9c0d95..d9dde0a7 100644 --- a/front/src/Phaser/Components/Loader.ts +++ b/front/src/Phaser/Components/Loader.ts @@ -1,6 +1,18 @@ +const LogoNameIndex: string = 'logo'; +const LogoResource: string = 'resources/logos/logo.png'; + +export const addLoader = (scene: Phaser.Scene): void => { + const loaderPlugin = scene.load.image(LogoNameIndex, LogoResource); + loaderPlugin.spritesheet(LogoNameIndex, LogoResource); + const promiseLoadLogoTexture = new Promise((res) => { + if (loaderPlugin.textureManager.exists(LogoNameIndex)) { + return res(scene.add.image(scene.game.renderer.width / 2, 100, LogoNameIndex)); + } + loaderPlugin.once(`filecomplete-spritesheet-${LogoNameIndex}`, () => { + res(scene.add.image(scene.game.renderer.width / 2, 100, LogoNameIndex)) + }); + }); -export const addLoader = (scene:Phaser.Scene): void => { - const loadingText = scene.add.text(scene.game.renderer.width / 2, 200, 'Loading'); const progress = scene.add.graphics(); scene.load.on('progress', (value: number) => { progress.clear(); @@ -8,7 +20,9 @@ export const addLoader = (scene:Phaser.Scene): void => { progress.fillRect(0, 270, 800 * value, 60); }); scene.load.on('complete', () => { - loadingText.destroy(); + promiseLoadLogoTexture.then((resLoadingImage: Phaser.GameObjects.Image) => { + resLoadingImage.destroy(); + }); progress.destroy(); }); } \ No newline at end of file From 788e22e8b04c80f4952578c9adc1449d88d608ed Mon Sep 17 00:00:00 2001 From: Gregoire Parant Date: Mon, 1 Feb 2021 16:52:28 +0100 Subject: [PATCH 08/13] Fix loading logo of WorkAdventure --- front/src/Phaser/Components/Loader.ts | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/front/src/Phaser/Components/Loader.ts b/front/src/Phaser/Components/Loader.ts index d9dde0a7..a81b7f25 100644 --- a/front/src/Phaser/Components/Loader.ts +++ b/front/src/Phaser/Components/Loader.ts @@ -1,15 +1,26 @@ -const LogoNameIndex: string = 'logo'; +import ImageFrameConfig = Phaser.Types.Loader.FileTypes.ImageFrameConfig; + +const LogoNameIndex: string = 'logoLoading'; +const TextName: string = 'Loading...'; const LogoResource: string = 'resources/logos/logo.png'; +const LogoFrame: ImageFrameConfig = {frameWidth: 307, frameHeight: 59}; export const addLoader = (scene: Phaser.Scene): void => { - const loaderPlugin = scene.load.image(LogoNameIndex, LogoResource); - loaderPlugin.spritesheet(LogoNameIndex, LogoResource); + let loadingText: Phaser.GameObjects.Text|null = null; + const promiseLoadLogoTexture = new Promise((res) => { - if (loaderPlugin.textureManager.exists(LogoNameIndex)) { + if(scene.load.textureManager.exists(LogoNameIndex)){ return res(scene.add.image(scene.game.renderer.width / 2, 100, LogoNameIndex)); + }else{ + //add loading if logo image is not ready + loadingText = scene.add.text(scene.game.renderer.width / 2, 200, TextName); } - loaderPlugin.once(`filecomplete-spritesheet-${LogoNameIndex}`, () => { - res(scene.add.image(scene.game.renderer.width / 2, 100, LogoNameIndex)) + scene.load.spritesheet(LogoNameIndex, LogoResource, LogoFrame); + scene.load.once(`filecomplete-spritesheet-${LogoNameIndex}`, () => { + if(loadingText){ + loadingText.destroy(); + } + return res(scene.add.image(scene.game.renderer.width / 2, 100, LogoNameIndex)); }); }); @@ -20,6 +31,9 @@ export const addLoader = (scene: Phaser.Scene): void => { progress.fillRect(0, 270, 800 * value, 60); }); scene.load.on('complete', () => { + if(loadingText){ + loadingText.destroy(); + } promiseLoadLogoTexture.then((resLoadingImage: Phaser.GameObjects.Image) => { resLoadingImage.destroy(); }); From 0afbb5412ebb2fc5280189f7aa7d5ddc0aa036d5 Mon Sep 17 00:00:00 2001 From: Manfred Stock Date: Mon, 1 Feb 2021 22:12:32 +0100 Subject: [PATCH 09/13] Improve display of websites not specifying background color The `index.html` of the front-end specifies `#000` as background color, however, it seems like various websites found 'in the wild' don't specify their own background color and thus default to `transparent`. As a result of this, they tend to become mostly unreadable when displayed using `openWebsite` since they often assume that they get displayed on a white background. Setting white as background color on the container element of the `