From 6187faf20384b0c5a4966343b2d4ca47f8b11e45 Mon Sep 17 00:00:00 2001 From: Philip Paquette Date: Wed, 26 Sep 2018 07:48:55 -0400 Subject: Release v1.0.0 - Diplomacy Game Engine - AGPL v3+ License --- diplomacy/web/src/gui/core/page.jsx | 434 ++++++++++++++++++++++++++++++++++++ 1 file changed, 434 insertions(+) create mode 100644 diplomacy/web/src/gui/core/page.jsx (limited to 'diplomacy/web/src/gui/core/page.jsx') diff --git a/diplomacy/web/src/gui/core/page.jsx b/diplomacy/web/src/gui/core/page.jsx new file mode 100644 index 0000000..5ca09fd --- /dev/null +++ b/diplomacy/web/src/gui/core/page.jsx @@ -0,0 +1,434 @@ +// ============================================================================== +// Copyright (C) 2019 - Philip Paquette, Steven Bocco +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU Affero General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) any +// later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +// details. +// +// You should have received a copy of the GNU Affero General Public License along +// with this program. If not, see . +// ============================================================================== +/** Main class to use to create app GUI. **/ + +import React from "react"; +import {ContentConnection} from "../diplomacy/contents/content_connection"; +import {ContentGames} from "../diplomacy/contents/content_games"; +import {ContentGame} from "../diplomacy/contents/content_game"; +import {UTILS} from "../../diplomacy/utils/utils"; +import {Diplog} from "../../diplomacy/utils/diplog"; +import {STRINGS} from "../../diplomacy/utils/strings"; +import {Game} from "../../diplomacy/engine/game"; +import Octicon, {Person} from '@githubprimer/octicons-react'; +import $ from "jquery"; +import {FancyBox} from "./fancybox"; +import {DipStorage} from "../diplomacy/utils/dipStorage"; + +const CONTENTS = { + connection: ContentConnection, + games: ContentGames, + game: ContentGame +}; + +export class Page extends React.Component { + + constructor(props) { + super(props); + this.connection = null; + this.channel = null; + this.availableMaps = null; + this.state = { + // fancybox, + fancyTitle: null, + onFancyBox: null, + // Page messages + error: null, + info: null, + success: null, + title: null, + // Page content parameters + contentName: 'connection', + contentData: null, + // Games. + games: {}, // Games found. + myGames: {} // Games locally stored. + }; + this.loadPage = this.loadPage.bind(this); + this.loadConnection = this.loadConnection.bind(this); + this.loadGames = this.loadGames.bind(this); + this.loadGame = this.loadGame.bind(this); + this.loadGameFromDisk = this.loadGameFromDisk.bind(this); + this.logout = this.logout.bind(this); + this.error = this.error.bind(this); + this.info = this.info.bind(this); + this.success = this.success.bind(this); + this.unloadFancyBox = this.unloadFancyBox.bind(this); + } + + static wrapMessage(message) { + return message ? `(${UTILS.date()}) ${message}` : ''; + } + + static __sort_games(games) { + // Sort games with not-joined games first, else compare game ID. + games.sort((a, b) => (((a.role ? 1 : 0) - (b.role ? 1 : 0)) || a.game_id.localeCompare(b.game_id))); + return games; + } + + copyState(updatedFields) { + return Object.assign({}, this.state, updatedFields || {}); + } + + //// Methods to check page type. + + __page_is(contentName, contentData) { + return this.state.contentName === contentName && (!contentData || this.state.contentData === contentData); + } + + pageIsConnection(contentData) { + return this.__page_is('connection', contentData); + } + + pageIsGames(contentData) { + return this.__page_is('games', contentData); + } + + pageIsGame(contentData) { + return this.__page_is('game', contentData); + } + + //// Methods to load a global fancybox. + + loadFancyBox(title, callback) { + this.setState({fancyTitle: title, onFancyBox: callback}); + } + + unloadFancyBox() { + this.setState({fancyTitle: null, onFancyBox: null}); + } + + //// Methods to load a page. + + loadPage(contentName, contentData, messages) { + messages = messages || {}; + messages.error = Page.wrapMessage(messages.error); + messages.info = Page.wrapMessage(messages.info); + messages.success = Page.wrapMessage(messages.success); + Diplog.printMessages(messages); + this.setState(this.copyState({ + error: messages.error, + info: messages.info, + success: messages.success, + contentName: contentName, + contentData: contentData, + title: null, + fancyTitle: null, + onFancyBox: null + })); + } + + loadConnection(contentData, messages) { + this.loadPage('connection', contentData, messages); + } + + loadGames(contentData, messages) { + this.loadPage('games', contentData, messages); + } + + loadGame(gameInfo, messages) { + this.loadPage('game', gameInfo, messages); + } + + loadGameFromDisk() { + const input = $(document.createElement('input')); + input.attr("type", "file"); + input.trigger('click'); + input.change(event => { + const file = event.target.files[0]; + if (!file.name.match(/\.json$/i)) { + this.error(`Invalid JSON filename ${file.name}`); + } else { + const reader = new FileReader(); + reader.onload = () => { + const savedData = JSON.parse(reader.result); + const gameObject = {}; + gameObject.game_id = `(local) ${savedData.id}`; + gameObject.map_name = savedData.map; + gameObject.rules = savedData.rules; + const state_history = {}; + const message_history = {}; + const order_history = {}; + const result_history = {}; + for (let savedPhase of savedData.phases) { + const gameState = savedPhase.state; + const phaseOrders = savedPhase.orders || {}; + const phaseResults = savedPhase.results || {}; + const phaseMessages = {}; + if (savedPhase.messages) { + for (let message of savedPhase.messages) { + phaseMessages[message.time_sent] = message; + } + } + if (!gameState.name) + gameState.name = savedPhase.name; + state_history[gameState.name] = gameState; + order_history[gameState.name] = phaseOrders; + message_history[gameState.name] = phaseMessages; + result_history[gameState.name] = phaseResults; + } + gameObject.state_history = state_history; + gameObject.message_history = message_history; + gameObject.order_history = order_history; + gameObject.state_history = state_history; + gameObject.result_history = result_history; + gameObject.messages = []; + gameObject.role = STRINGS.OBSERVER_TYPE; + gameObject.status = STRINGS.COMPLETED; + gameObject.timestamp_created = 0; + gameObject.deadline = 0; + gameObject.n_controls = 0; + gameObject.registration_password = ''; + const game = new Game(gameObject); + this.loadGame(game); + }; + reader.readAsText(file); + } + }); + } + + //// Methods to sign out channel and go back to connection page. + + __disconnect() { + // Clear local data and go back to connection page. + this.connection.close(); + this.connection = null; + this.channel = null; + this.availableMaps = null; + const message = Page.wrapMessage(`Disconnected from channel and server.`); + Diplog.success(message); + this.setState(this.copyState({ + error: null, + info: null, + success: message, + contentName: 'connection', + contentData: null, + // When disconnected, remove all games previously loaded. + games: {}, + myGames: {} + })); + } + + logout() { + // Disconnect channel and go back to connection page. + if (this.channel) { + this.channel.logout() + .then(() => this.__disconnect()) + .catch(error => this.error(`Error while disconnecting: ${error.toString()}.`)); + } else { + this.__disconnect(); + } + } + + //// Methods to be used to set page title and messages. + + setTitle(title) { + this.setState({title: title}); + } + + error(message) { + message = Page.wrapMessage(message); + Diplog.error(message); + this.setState({error: message}); + } + + info(message) { + message = Page.wrapMessage(message); + Diplog.info(message); + this.setState({info: message}); + } + + success(message) { + message = Page.wrapMessage(message); + Diplog.success(message); + this.setState({success: message}); + } + + warn(message) { + this.info(message); + } + + //// Methods to manage games. + + updateMyGames(gamesToAdd) { + // Update state myGames with given games. This method does not update local storage. + const myGames = Object.assign({}, this.state.myGames); + let gamesFound = null; + for (let gameToAdd of gamesToAdd) { + myGames[gameToAdd.game_id] = gameToAdd; + if (this.state.games.hasOwnProperty(gameToAdd.game_id)) { + if (!gamesFound) + gamesFound = Object.assign({}, this.state.games); + gamesFound[gameToAdd.game_id] = gameToAdd; + } + } + if (!gamesFound) + gamesFound = this.state.games; + this.setState({myGames: myGames, games: gamesFound}); + } + + getMyGames() { + return Page.__sort_games(Object.values(this.state.myGames)); + } + + getGamesFound() { + return Page.__sort_games(Object.values(this.state.games)); + } + + addGamesFound(gamesToAdd) { + const gamesFound = {}; + for (let game of gamesToAdd) { + gamesFound[game.game_id] = ( + this.state.myGames.hasOwnProperty(game.game_id) ? + this.state.myGames[game.game_id] : game + ); + } + this.setState({games: gamesFound}); + } + + leaveGame(gameID) { + if (this.state.myGames.hasOwnProperty(gameID)) { + const game = this.state.myGames[gameID]; + if (game.client) { + game.client.leave() + .then(() => { + this.disconnectGame(gameID); + this.loadGames(null, {info: `Game ${gameID} left.`}); + }) + .catch(error => this.error(`Error when leaving game ${gameID}: ${error.toString()}`)); + } + } + } + + disconnectGame(gameID) { + if (this.state.myGames.hasOwnProperty(gameID)) { + const game = this.state.myGames[gameID]; + if (game.client) + game.client.clearAllCallbacks(); + this.channel.getGamesInfo({games: [gameID]}) + .then(gamesInfo => { + this.updateMyGames(gamesInfo); + }) + .catch(error => this.error(`Error while leaving game ${gameID}: ${error.toString()}`)); + } + } + + addToMyGames(game) { + // Update state myGames with given game **and** update local storage. + const myGames = Object.assign({}, this.state.myGames); + const gamesFound = this.state.games.hasOwnProperty(game.game_id) ? Object.assign({}, this.state.games) : this.state.games; + myGames[game.game_id] = game; + if (gamesFound.hasOwnProperty(game.game_id)) + gamesFound[game.game_id] = game; + DipStorage.addUserGame(this.channel.username, game.game_id); + this.setState({myGames: myGames, games: gamesFound}); + } + + removeFromMyGames(gameID) { + if (this.state.myGames.hasOwnProperty(gameID)) { + const games = Object.assign({}, this.state.myGames); + delete games[gameID]; + DipStorage.removeUserGame(this.channel.username, gameID); + this.setState({myGames: games}); + } + } + + hasMyGame(gameID) { + return this.state.myGames.hasOwnProperty(gameID); + } + + //// Render method. + + render() { + const content = CONTENTS[this.state.contentName].builder(this, this.state.contentData); + const hasNavigation = UTILS.javascript.hasArray(content.navigation); + + // NB: I currently don't find a better way to update document title from content details. + const successMessage = this.state.success || '-'; + const infoMessage = this.state.info || '-'; + const errorMessage = this.state.error || '-'; + const title = this.state.title || content.title; + document.title = title + ' | Diplomacy'; + + return ( +
+
+
this.success()}> + {successMessage} +
+
this.info()}> + {infoMessage} +
+
this.error()}> + {errorMessage} +
+
+ {((hasNavigation || this.channel) && ( +
+
{title}
+
+ {(!hasNavigation && ( +
+ + {this.channel.username} + + +
+ )) || ( +
+ +
+ {content.navigation.map((nav, index) => { + const navTitle = nav[0]; + const navAction = nav[1]; + return {navTitle}; + })} +
+
+ )} +
+
+ )) || ( +
{title}
+ )} + {content.component} + {this.state.onFancyBox && ( + + {this.state.onFancyBox()} + + )} +
+ ); + } +} -- cgit v1.2.3