diff --git a/bemani/frontend/app.py b/bemani/frontend/app.py index c1bfaa7..d15ce37 100644 --- a/bemani/frontend/app.py +++ b/bemani/frontend/app.py @@ -498,6 +498,10 @@ def navigation() -> Dict[str, Any]: 'label': 'Game Options', 'uri': url_for('popn_pages.viewsettings'), }, + { + 'label': 'Rivals', + 'uri': url_for('popn_pages.viewrivals'), + }, { 'label': 'Personal Profile', 'uri': url_for('popn_pages.viewplayer', userid=g.userID), diff --git a/bemani/frontend/popn/endpoints.py b/bemani/frontend/popn/endpoints.py index 6874b0d..84fd92f 100644 --- a/bemani/frontend/popn/endpoints.py +++ b/bemani/frontend/popn/endpoints.py @@ -3,7 +3,7 @@ import re from typing import Any, Dict from flask import Blueprint, request, Response, url_for, abort, g # type: ignore -from bemani.common import GameConstants +from bemani.common import ID, GameConstants from bemani.data import UserID from bemani.frontend.app import loginrequired, jsonify, render_react from bemani.frontend.popn.popn import PopnMusicFrontend @@ -354,3 +354,123 @@ def updatename() -> Dict[str, Any]: 'version': version, 'name': name, } + + +@popn_pages.route('/rivals') +@loginrequired +def viewrivals() -> Response: + frontend = PopnMusicFrontend(g.data, g.config, g.cache) + rivals, playerinfo = frontend.get_rivals(g.userID) + return render_react( + 'Pop\'n Music Rivals', + 'popn/rivals.react.js', + { + 'userid': str(g.userID), + 'rivals': rivals, + 'players': playerinfo, + 'versions': {version: name for (game, version, name) in frontend.all_games()}, + }, + { + 'refresh': url_for('popn_pages.listrivals'), + 'search': url_for('popn_pages.searchrivals'), + 'player': url_for('popn_pages.viewplayer', userid=-1), + 'addrival': url_for('popn_pages.addrival'), + 'removerival': url_for('popn_pages.removerival'), + }, + ) + + +@popn_pages.route('/rivals/list') +@jsonify +@loginrequired +def listrivals() -> Dict[str, Any]: + frontend = PopnMusicFrontend(g.data, g.config, g.cache) + rivals, playerinfo = frontend.get_rivals(g.userID) + + return { + 'rivals': rivals, + 'players': playerinfo, + } + + +@popn_pages.route('/rivals/search', methods=['POST']) +@jsonify +@loginrequired +def searchrivals() -> Dict[str, Any]: + frontend = PopnMusicFrontend(g.data, g.config, g.cache) + version = int(request.get_json()['version']) + name = request.get_json()['term'] + print(name) + + # Try to treat the term as an extid + extid = ID.parse_extid(name) + + matches = set() + profiles = g.data.remote.user.get_all_profiles(GameConstants.POPN_MUSIC, version) + for (userid, profile) in profiles: + if profile.get_int('extid') == extid or profile.get_str('name').lower() == name.lower(): + matches.add(userid) + + playerinfo = frontend.get_all_player_info(list(matches), allow_remote=True) + return { + 'results': playerinfo, + } + + +@popn_pages.route('/rivals/add', methods=['POST']) +@jsonify +@loginrequired +def addrival() -> Dict[str, Any]: + frontend = PopnMusicFrontend(g.data, g.config, g.cache) + version = int(request.get_json()['version']) + other_userid = UserID(int(request.get_json()['userid'])) + userid = g.userID + + # Add this rival link + profile = g.data.remote.user.get_profile(GameConstants.POPN_MUSIC, version, other_userid) + if profile is None: + raise Exception('Unable to find profile for rival!') + + g.data.local.user.put_link( + GameConstants.POPN_MUSIC, + version, + userid, + 'rival', + other_userid, + {}, + ) + + # Now return updated rival info + rivals, playerinfo = frontend.get_rivals(userid) + + return { + 'rivals': rivals, + 'players': playerinfo, + } + + +@popn_pages.route('/rivals/remove', methods=['POST']) +@jsonify +@loginrequired +def removerival() -> Dict[str, Any]: + frontend = PopnMusicFrontend(g.data, g.config, g.cache) + version = int(request.get_json()['version']) + other_userid = UserID(int(request.get_json()['userid'])) + userid = g.userID + + # Remove this rival link + g.data.local.user.destroy_link( + GameConstants.POPN_MUSIC, + version, + userid, + 'rival', + other_userid, + ) + + # Now return updated rival info + rivals, playerinfo = frontend.get_rivals(userid) + + return { + 'rivals': rivals, + 'players': playerinfo, + } diff --git a/bemani/frontend/popn/popn.py b/bemani/frontend/popn/popn.py index 2796782..0e43302 100644 --- a/bemani/frontend/popn/popn.py +++ b/bemani/frontend/popn/popn.py @@ -18,6 +18,8 @@ class PopnMusicFrontend(FrontendBase): PopnMusicBase.CHART_TYPE_EX, ] + valid_rival_types = ['rival'] + def all_games(self) -> Iterator[Tuple[str, int, str]]: yield from PopnMusicFactory.all_games() diff --git a/bemani/frontend/static/controllers/popn/rivals.react.js b/bemani/frontend/static/controllers/popn/rivals.react.js new file mode 100644 index 0000000..2ff32c3 --- /dev/null +++ b/bemani/frontend/static/controllers/popn/rivals.react.js @@ -0,0 +1,375 @@ +/*** @jsx React.DOM */ + +var valid_versions = Object.keys(window.rivals); +var pagenav = new History(valid_versions); + +var rivals_view = React.createClass({ + + getInitialState: function(props) { + var profiles = Object.keys(window.rivals); + var version = pagenav.getInitialState(profiles[profiles.length - 1]); + return { + rivals: window.rivals, + players: window.players, + profiles: profiles, + version: version, + term_id: "", + term_name: "", + results: {}, + searching_id: false, + searching_name: false, + offset: 0, + limit: 5, + }; + }, + + componentDidMount: function() { + pagenav.onChange(function(version) { + this.setState({version: version, offset: 0}); + }.bind(this)); + this.refreshRivals(); + }, + + refreshRivals: function() { + AJAX.get( + Link.get('refresh'), + function(response) { + this.setState({ + rivals: response.rivals, + players: response.players, + }); + setTimeout(this.refreshRivals, 5000); + }.bind(this) + ); + }, + + searchForPlayersID: function(event) { + this.setState({searching_id: true}); + AJAX.post( + Link.get('search'), + { + version: this.state.version, + term: this.state.term_id.substring(0, 9), + }, + function(response) { + this.setState({ + results: response.results, + searching_id: false, + }); + }.bind(this) + ); + event.preventDefault(); + }, + + searchForPlayersName: function(event) { + this.setState({searching_name: true}); + AJAX.post( + Link.get('search'), + { + version: this.state.version, + term: this.state.term_name, + }, + function(response) { + this.setState({ + results: response.results, + searching_name: false, + }); + }.bind(this) + ); + event.preventDefault(); + }, + + addRival: function(event, userid) { + AJAX.post( + Link.get('addrival'), + { + version: this.state.version, + userid: userid, + }, + function(response) { + this.setState({ + rivals: response.rivals, + players: response.players, + }); + }.bind(this) + ); + event.preventDefault(); + }, + + removeRival: function(event, userid) { + AJAX.post( + Link.get('removerival'), + { + version: this.state.version, + userid: userid, + }, + function(response) { + this.setState({ + rivals: response.rivals, + players: response.players, + }); + }.bind(this) + ); + event.preventDefault(); + }, + + addRivals: function(userid) { + if (userid == window.userid) { + return null; + } + + var avail = true; + var count = 0; + var current_rivals = this.state.rivals[this.state.version]; + for (var i = 0; i < current_rivals.length; i++) { + count++; + if (current_rivals[i].userid == userid) { + avail = false; + } + } + + if (count >= 4) { avail = false; } + + return ( + + {avail ? + : + null + } + + ); + }, + + render: function() { + if (this.state.rivals[this.state.version]) { + var rivals = this.state.rivals[this.state.version]; + var resultlength = 0; + Object.keys(this.state.results).map(function(userid, index) { + var player = this.state.results[userid][this.state.version]; + if (player) { resultlength++; } + }.bind(this)); + return ( +
+
+

Rivals

+ {this.state.profiles.map(function(version) { + return ( +
+
+
+ +
+ = '0'.charCodeAt(0) && c <= '9'.charCodeAt(0)) { + c = 0xFF10 + (c - '0'.charCodeAt(0)); + } else if(c >= 'A'.charCodeAt(0) && c <= 'Z'.charCodeAt(0)) { + c = 0xFF21 + (c - 'A'.charCodeAt(0)); + } else if(c >= 'a'.charCodeAt(0) && c <= 'z'.charCodeAt(0)) { + c = 0xFF41 + (c - 'a'.charCodeAt(0)); + } else if(c == '@'.charCodeAt(0)) { + c = 0xFF20; + } else if(c == ','.charCodeAt(0)) { + c = 0xFF0C; + } else if(c == '.'.charCodeAt(0)) { + c = 0xFF0E; + } else if(c == '_'.charCodeAt(0)) { + c = 0xFF3F; + } + value = value + String.fromCharCode(c); + } + var nameRegex = new RegExp( + "^[" + + "\uFF20-\uFF3A" + // widetext A-Z and @ + "\uFF41-\uFF5A" + // widetext a-z + "\uFF10-\uFF19" + // widetext 0-9 + "\uFF0C\uFF0E\uFF3F" + // widetext ,._ + "\u3041-\u308D\u308F\u3092\u3093" + // hiragana + "\u30A1-\u30ED\u30EF\u30F2\u30F3\u30FC" + // katakana + "]*$" + ); + if (value.length <= 6 && nameRegex.test(value)) { + this.setState({term_name: value}); + } + }.bind(this)} + name="search" + /> + + { this.state.searching_name ? + : + null + } +
+
+ +
+ + + { this.state.searching_id ? + : + null + } +
+ {resultlength > 0 ? + + + + + + + + + { + Object.keys(this.state.results).map(function(userid, index) { + if (index < this.state.offset || index >= this.state.offset + this.state.limit) { + return null; + } + var player = this.state.results[userid][this.state.version]; + if (!player) { return null; } + + return ( + + + + + + ); + }.bind(this)) + } + + + + + +
NamePop'n Music ID
{ player.extid }{this.addRivals(userid)}
+ { this.state.offset > 0 ? + : null + } + { (this.state.offset + this.state.limit) < resultlength ? + = resultlength) { return; } + this.setState({offset: page}); + }.bind(this)}/> : null + } +
: +
No players match the specified search.
+ } +
+
+

Rivals

+ + + + + + + + + + {this.state.rivals[this.state.version].map(function(rival, index) { + var player = this.state.players[rival.userid][this.state.version]; + return ( + + + + + + ); + }.bind(this))} + +
NamePop'n Music ID
{ player.extid } + +
+
+
+ ); + } else { + return ( +
+
+ You have no profile for {window.versions[this.state.version]}! +
+
+ {this.state.profiles.map(function(version) { + return ( +
+
+ ); + } + }, +}); + +ReactDOM.render( + React.createElement(rivals_view, null), + document.getElementById('content') +);