From 2701df1e3b03c7c605ccf212a02987d53fbd0609 Mon Sep 17 00:00:00 2001 From: notoraptor Date: Wed, 17 Jul 2019 15:16:43 -0400 Subject: [web] Make button "Delete all" remove only orders from current se… (#49) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Make button "Delete all" remove only orders from current selected power. - Reorganize code - [web] Remove bugged and useless function gameReloaded() from game page. - This function caused error `engine.getPhaseType is not a function` for games with deadlines. - Move function saveGameToDisk into its own file. - [web] Add documentation to methods involved in orders management to help understand what happens. - Move methods getServerOrders() from game GUI component to game engine object. - Rename method onSetNoOrders to onSetEmptyOrdersSet. - Rename property in PowerActionsForm: onNoOrders => onPass. - [web] Update sending orders to send request clearOrders when local orders list is null. - Renamed local file: - components/power_order => power_orders - forms/power_actions_form => power_order_creation_form - Move power orders buttons bar to a separate file: - components/power_orders_actions_bar - [web] Improve messages about local/server defined orders. --- diplomacy/web/src/gui/pages/page.jsx | 375 +++++++++++++++++++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 diplomacy/web/src/gui/pages/page.jsx (limited to 'diplomacy/web/src/gui/pages/page.jsx') diff --git a/diplomacy/web/src/gui/pages/page.jsx b/diplomacy/web/src/gui/pages/page.jsx new file mode 100644 index 0000000..cd36f6c --- /dev/null +++ b/diplomacy/web/src/gui/pages/page.jsx @@ -0,0 +1,375 @@ +// ============================================================================== +// 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 "./content_connection"; +import {UTILS} from "../../diplomacy/utils/utils"; +import {Diplog} from "../../diplomacy/utils/diplog"; +import {FancyBox} from "../components/fancybox"; +import {DipStorage} from "../utils/dipStorage"; +import {PageContext} from "../components/page_context"; +import {ContentGames} from "./content_games"; +import {loadGameFromDisk} from "../utils/load_game_from_disk"; +import {ContentGame} from "./content_game"; + +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, + // Page content parameters + name: null, + body: null, + // Games. + games: {}, // Games found. + myGames: {} // Games locally stored. + }; + this.error = this.error.bind(this); + this.info = this.info.bind(this); + this.success = this.success.bind(this); + this.logout = this.logout.bind(this); + this.loadGameFromDisk = this.loadGameFromDisk.bind(this); + this.unloadFancyBox = this.unloadFancyBox.bind(this); + this._post_remove = this._post_remove.bind(this); + this._add_to_my_games = this._add_to_my_games.bind(this); + this._remove_from_my_games = this._remove_from_my_games.bind(this); + this._remove_from_games = this._remove_from_games.bind(this); + this.onReconnectionError = this.onReconnectionError.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; + } + + static defaultPage() { + return ; + } + + onReconnectionError(error) { + this.__disconnect(error); + } + + //// 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. + + load(name, body, messages) { + const newState = {}; + if (messages) { + for (let key of ['error', 'info', 'success']) + newState[key] = Page.wrapMessage(messages[key]); + } + Diplog.printMessages(newState); + newState.name = name; + newState.body = body; + this.setState(newState); + } + + loadGames(messages) { + this.load( + 'games', + , + messages + ); + } + + loadGameFromDisk() { + loadGameFromDisk( + (game) => this.load( + `game: ${game.game_id}`, + , + {success: `Game loaded from disk: ${game.game_id}`} + ), + this.error + ); + } + + getName() { + return this.state.name; + } + + //// Methods to sign out channel and go back to connection page. + + __disconnect(error) { + // 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(error ? `${error.toString()}` : `Disconnected from channel and server.`); + Diplog.success(message); + this.setState({ + error: error ? message : null, + info: null, + success: error ? null : message, + name: null, + body: 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. + + 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}); + } + + getGame(gameID) { + if (this.state.myGames.hasOwnProperty(gameID)) + return this.state.myGames[gameID]; + return this.state.games[gameID]; + } + + 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).then(() => { + this.loadGames({info: `Game ${gameID} left.`}); + }); + }) + .catch(error => this.error(`Error when leaving game ${gameID}: ${error.toString()}`)); + } + } else { + this.loadGames({info: `No game to left.`}); + } + } + + _post_remove(gameID) { + this.disconnectGame(gameID) + .then(() => { + const myGames = this._remove_from_my_games(gameID); + const games = this._remove_from_games(gameID); + this.setState( + {games, myGames}, + () => this.loadGames({info: `Game ${gameID} deleted.`})); + }); + } + + removeGame(gameID) { + const game = this.getGame(gameID); + if (game) { + if (game.client) { + game.client.remove() + .then(() => this._post_remove(gameID)) + .catch(error => this.error(`Error when deleting game ${gameID}: ${error.toString()}`)); + } else { + this.channel.joinGame({game_id: gameID}) + .then(networkGame => { + networkGame.remove() + .then(() => this._post_remove(gameID)) + .catch(error => this.error(`Error when deleting game ${gameID}: ${error.toString()}`)); + }) + .catch(error => this.error(`Error when connecting to game to delete (${gameID}): ${error.toString()}`)); + } + } + } + + + disconnectGame(gameID) { + const game = this.getGame(gameID); + if (game) { + if (game.client) + game.client.clearAllCallbacks(); + return this.channel.getGamesInfo({games: [gameID]}) + .then(gamesInfo => { + this.updateMyGames(gamesInfo); + }) + .catch(error => this.error(`Error while leaving game ${gameID}: ${error.toString()}`)); + } + return null; + } + + _add_to_my_games(game) { + 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; + return {myGames: myGames, games: gamesFound}; + } + + _remove_from_my_games(gameID) { + if (this.state.myGames.hasOwnProperty(gameID)) { + const games = Object.assign({}, this.state.myGames); + delete games[gameID]; + DipStorage.removeUserGame(this.channel.username, gameID); + return games; + } else { + return this.state.myGames; + } + } + + _remove_from_games(gameID) { + if (this.state.games.hasOwnProperty(gameID)) { + const games = Object.assign({}, this.state.games); + delete games[gameID]; + return games; + } else { + return this.state.games; + } + } + + addToMyGames(game) { + // Update state myGames with given game **and** update local storage. + DipStorage.addUserGame(this.channel.username, game.game_id); + this.setState(this._add_to_my_games(game), () => this.loadGames()); + } + + removeFromMyGames(gameID) { + const myGames = this._remove_from_my_games(gameID); + if (myGames !== this.state.myGames) + this.setState({myGames}, () => this.loadGames()); + } + + hasMyGame(gameID) { + return this.state.myGames.hasOwnProperty(gameID); + } + + //// Render method. + + render() { + const successMessage = this.state.success || '-'; + const infoMessage = this.state.info || '-'; + const errorMessage = this.state.error || '-'; + return ( + +
+
+
this.success()}> + {successMessage} +
+
this.info()}> + {infoMessage} +
+
this.error()}> + {errorMessage} +
+
+ {this.state.body || Page.defaultPage()} + {this.state.onFancyBox && ( + + {this.state.onFancyBox()} + + )} +
+
+ ); + } +} -- cgit v1.2.3