(function ($) { if (window.nodewebkit) { window.gui = require('nw.gui'); window.nwWindow = gui.Window.get(); } $(document).on('click', 'a', function (e) { if (this.className === 'closebutton') return; // handled elsewhere if (this.className.indexOf('minilogo') >= 0) return; // handled elsewhere if (!this.href) return; // should never happen if (this.host === 'play.pokemonshowdown.com' || this.host === location.host) { var target = this.pathname.substr(1); if (target.indexOf('/') < 0 && target.indexOf('.') < 0) { window.app.tryJoinRoom(target); e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); return; } } if (window.nodewebkit && this.target === '_blank') { gui.Shell.openExternal(this.href); e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); } }); $(window).on('dragover', function (e) { e.preventDefault(); }); $(document).on('dragenter', function (e) { e.preventDefault(); if (!app.dragging && app.curRoom.id === 'teambuilder') { if (e.originalEvent.dataTransfer.files && e.originalEvent.dataTransfer.files[0]) { var file = e.originalEvent.dataTransfer.files[0]; if (file.name.slice(-4) === '.txt') { // Someone dragged in a .txt file, hand it to the teambuilder app.curRoom.defaultDragEnterTeam(e); } } else { // security doesn't let us read the filename :( // we'll just have to assume it's a team app.curRoom.defaultDragEnterTeam(e); } } // dropEffect !== 'none' prevents buggy bounce-back animation in // Chrome/Safari/Opera e.originalEvent.dataTransfer.dropEffect = 'move'; }); $(window).on('drop', function (e) { // The default team drop action for Firefox is to open the team as a // URL, which needs to be prevented. // The default file drop action for most browsers is to open the file // in the tab, which is generally undesirable anyway. e.preventDefault(); if (app.dragging) { app.rooms[app.draggingRoom].defaultDropTeam(e); } else if (e.originalEvent.dataTransfer.files && e.originalEvent.dataTransfer.files[0]) { var file = e.originalEvent.dataTransfer.files[0]; if (file.name.slice(-4) === '.txt' && app.curRoom.id === 'teambuilder') { // Someone dragged in a .txt file, hand it to the teambuilder app.curRoom.defaultDragEnterTeam(e); app.curRoom.defaultDropTeam(e); } else if (file.type && file.type.substr(0, 6) === 'image/') { // It's an image file, try to set it as a background CustomBackgroundPopup.readFile(file); } } }); if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) { // Android mobile-web-app-capable doesn't support it very well, but iOS // does it fine, so we're only going to show this to iOS for now $('head').append(''); } if (window.nodewebkit) { $(document).on("contextmenu", function (e) { e.preventDefault(); var target = e.target; var isEditable = (target.tagName === 'TEXTAREA' || target.tagName === 'INPUT'); var menu = new gui.Menu(); if (isEditable) menu.append(new gui.MenuItem({ label: "Cut", click: function () { document.execCommand("cut"); } })); var link = $(target).closest('a')[0]; if (link) menu.append(new gui.MenuItem({ label: "Copy Link URL", click: function () { gui.Clipboard.get().set(link.href); } })); if (target.tagName === 'IMG') menu.append(new gui.MenuItem({ label: "Copy Image URL", click: function () { gui.Clipboard.get().set(target.src); } })); menu.append(new gui.MenuItem({ label: "Copy", click: function () { document.execCommand("copy"); } })); if (isEditable) menu.append(new gui.MenuItem({ label: "Paste", enabled: !!gui.Clipboard.get().get(), click: function () { document.execCommand("paste"); } })); menu.popup(e.originalEvent.x, e.originalEvent.y); }); } Config.version = '0.10.1'; Config.origindomain = 'play.pokemonshowdown.com'; // `defaultserver` specifies the server to use when the domain name in the // address bar is `Config.origindomain`. Config.defaultserver = { id: 'showdown', host: 'sim.smogon.com', port: 443, httpport: 8000, altport: 80, registered: true }; Config.sockjsprefix = '/showdown'; Config.root = '/'; // sanitize a room ID // shouldn't actually do anything except against a malicious server var toRoomid = this.toRoomid = function (roomid) { return roomid.replace(/[^a-zA-Z0-9-]+/g, '').toLowerCase(); }; // support Safari 6 notifications if (!window.Notification && window.webkitNotification) { window.Notification = window.webkitNotification; } // this is called being lazy window.selectTab = function (tab) { app.tryJoinRoom(tab); return false; }; // placeholder until the real chart loads window.Chart = { pokemonRow: function () {}, itemRow: function () {}, abilityRow: function () {}, moveRow: function () {} }; var User = this.User = Backbone.Model.extend({ defaults: { name: '', userid: '', registered: false, named: false, avatar: 0 }, initialize: function () { app.on('response:userdetails', function (data) { if (data.userid === this.get('userid')) { this.set('avatar', data.avatar); } }, this); var self = this; this.on('change:name', function () { if (!self.get('named')) { self.nameRegExp = null; } else { self.nameRegExp = new RegExp('\\b' + Tools.escapeRegExp(self.get('name')) + '\\b', 'i'); } }); }, /** * Return the path to the login server `action.php` file. AJAX requests * to this file will always be made on the `play.pokemonshowdown.com` * domain in order to have access to the correct cookies. */ getActionPHP: function () { var ret = '/~~' + Config.server.id + '/action.php'; if (Config.testclient) { ret = 'https://' + Config.origindomain + ret; } return (this.getActionPHP = function () { return ret; })(); }, /** * Process a signed assertion returned from the login server. * Emits the following events (arguments in brackets): * * `login:authrequired` (name) * triggered if the user needs to authenticate with this name * * `login:invalidname` (name, error) * triggered if the user's name is invalid * * `login:noresponse` * triggered if the login server did not return a response */ finishRename: function (name, assertion) { if (assertion === ';') { this.trigger('login:authrequired', name); } else if (assertion.substr(0, 2) === ';;') { this.trigger('login:invalidname', name, assertion.substr(2)); } else if (assertion.indexOf('\n') >= 0) { this.trigger('login:noresponse'); } else { app.send('/trn ' + name + ',0,' + assertion); } }, /** * Rename this user to an arbitrary username. If the username is * registered and the user does not currently have a session * associated with that userid, then the user will be required to * authenticate. * * See `finishRename` above for a list of events this can emit. */ rename: function (name) { // | , ; are not valid characters in names name = name.replace(/[\|,;]+/g, ''); var userid = toUserid(name); if (!userid) { app.addPopupMessage("Usernames must contain at least one letter or number."); return; } if (this.get('userid') !== userid) { var self = this; $.get(this.getActionPHP(), { act: 'getassertion', userid: userid, challstr: this.challstr }, function (data) { self.finishRename(name, data); }); } else { app.send('/trn ' + name); } }, passwordRename: function (name, password) { var self = this; $.post(this.getActionPHP(), { act: 'login', name: name, pass: password, challstr: this.challstr }, Tools.safeJSON(function (data) { if (data && data.curuser && data.curuser.loggedin) { // success! self.set('registered', data.curuser); self.finishRename(name, data.assertion); } else { // wrong password app.addPopup(LoginPasswordPopup, { username: name, error: 'Wrong password.' }); } }), 'text'); }, challstr: '', receiveChallstr: function (challstr) { if (challstr) { /** * Rename the user based on the `sid` and `showdown_username` cookies. * Specifically, if the user has a valid session, the user will be * renamed to the username associated with that session. If the user * does not have a valid session but does have a persistent username * (i.e. a `showdown_username` cookie), the user will be renamed to * that name; if that name is registered, the user will be required * to authenticate. * * See `finishRename` above for a list of events this can emit. */ this.challstr = challstr; var self = this; $.get(this.getActionPHP(), { act: 'upkeep', challstr: this.challstr }, Tools.safeJSON(function (data) { if (!data.username) return; // | , ; are not valid characters in names data.username = data.username.replace(/[\|,;]+/g, ''); if (data.loggedin) { self.set('registered', { username: data.username, userid: toUserid(data.username) }); } self.finishRename(data.username, data.assertion); }), 'text'); } }, /** * Log out from the server (but remain connected as a guest). */ logout: function () { $.post(this.getActionPHP(), { act: 'logout', userid: this.get('userid') }); app.send('/logout'); }, setPersistentName: function (name) { $.cookie('showdown_username', (name !== undefined) ? name : this.get('name'), { expires: 14 }); }, }); var App = this.App = Backbone.Router.extend({ root: '/', routes: { '*path': 'dispatchFragment' }, focused: true, initialize: function () { window.app = this; this.initializeRooms(); this.initializePopups(); this.user = new User(); this.ignore = {}; this.supports = {}; // down // if (document.location.hostname === 'play.pokemonshowdown.com') this.down = 'dos'; if (document.location.hostname === 'play.pokemonshowdown.com') { app.supports['rooms'] = true; } this.topbar = new Topbar({el: $('#header')}); this.addRoom(''); if (!this.down && $(window).width() >= 916) { if (document.location.hostname === 'play.pokemonshowdown.com') { this.addRoom('rooms', null, true); var autojoin = (Tools.prefs('autojoin') || ''); var autojoinIds = []; if (autojoin) { var autojoins = autojoin.split(','); var roomid; for (var i = 0; i < autojoins.length; i++) { roomid = toRoomid(autojoins[i]); this.addRoom(roomid, null, true, autojoins[i]); if (roomid !== 'staff' && roomid !== 'upperstaff') autojoinIds.push(roomid); } } this.send('/autojoin ' + autojoinIds.join(',')); } else { this.addRoom('lobby', null, true); this.send('/autojoin'); } } var self = this; this.prefsLoaded = false; this.on('init:loadprefs', function () { self.prefsLoaded = true; var bg = Tools.prefs('bg'); if (bg) { $(document.body).css({ background: bg, 'background-size': 'cover' }); } else if (Config.server.id === 'smogtours') { $(document.body).css({ background: '#546bac url(//play.pokemonshowdown.com/fx/client-bg-shaymin.jpg) no-repeat left center fixed', 'background-size': 'cover' }); } var muted = Tools.prefs('mute'); BattleSound.setMute(muted); var effectVolume = Tools.prefs('effectvolume'); if (effectVolume !== undefined) BattleSound.setEffectVolume(effectVolume); var musicVolume = Tools.prefs('musicvolume'); if (musicVolume !== undefined) BattleSound.setBgmVolume(musicVolume); if (Tools.prefs('logchat')) Storage.startLoggingChat(); if (Tools.prefs('showdebug')) { var debugStyle = $('#debugstyle').get(0); var onCSS = '.debug {display: block;}'; if (!debugStyle) { $('head').append(''); } else { debugStyle.innerHTML = onCSS; } } if (Tools.prefs('bwgfx') || Tools.prefs('noanim')) { // since xy data is loaded by default, only call // loadSpriteData if we want bw sprites or if we need bw // sprite data (if animations are disabled) Tools.loadSpriteData('bw'); } }); this.on('init:unsupported', function () { self.addPopupMessage('Your browser is unsupported.'); }); this.on('init:nothirdparty', function () { self.addPopupMessage('You have third-party cookies disabled in your browser, which is likely to cause problems. You should enable them and then refresh this page.'); }); this.on('init:socketclosed', function () { self.reconnectPending = true; if (!self.popups.length) self.addPopup(ReconnectPopup); }); this.on('init:connectionerror', function () { self.addPopup(ReconnectPopup, {cantconnect: true}); }); this.user.on('login:invalidname', function (name, reason) { self.addPopup(LoginPopup, {name: name, reason: reason}); }); this.user.on('login:authrequired', function (name) { self.addPopup(LoginPasswordPopup, {username: name}); }); this.on('response:savereplay', this.uploadReplay, this); this.on('response:rooms', this.roomsResponse, this); if (window.nodewebkit) { nwWindow.on('focus', function () { if (!self.focused) { self.focused = true; if (self.curRoom) self.curRoom.dismissNotification(); if (self.curSideRoom) self.curSideRoom.dismissNotification(); } }); nwWindow.on('blur', function () { self.focused = false; }); } else { $(window).on('focus click', function () { if (!self.focused) { self.focused = true; if (self.curRoom) self.curRoom.dismissNotification(); if (self.curSideRoom) self.curSideRoom.dismissNotification(); } }); $(window).on('blur', function () { self.focused = false; }); } $(window).on('beforeunload', function (e) { if (Config.server && Config.server.host === 'localhost') return; for (var id in self.rooms) { var room = self.rooms[id]; if (room && room.requestLeave && !room.requestLeave()) return "You have active battles."; } }); $(window).on('keydown', function (e) { var el = e.target; var tagName = el.tagName.toUpperCase(); // keypress happened in an empty textarea or a button var safeLocation = ((tagName === 'TEXTAREA' && !el.value.length) || tagName === 'BUTTON'); if (app.curSideRoom && $(e.target).closest(app.curSideRoom.$el).length) { // keypress happened in sideroom if (e.keyCode === 37 && safeLocation || window.nodewebkit && e.ctrlKey && e.shiftKey && e.keyCode === 9) { // Left or Ctrl+Shift+Tab on desktop client if (app.topbar.curSideRoomLeft) { e.preventDefault(); e.stopImmediatePropagation(); app.arrowKeysUsed = true; app.focusRoom(app.topbar.curSideRoomLeft); } } else if (e.keyCode === 39 && safeLocation || window.nodewebkit && e.ctrlKey && e.keyCode === 9) { // Right or Ctrl+Tab on desktop client if (app.topbar.curSideRoomRight) { e.preventDefault(); e.stopImmediatePropagation(); app.arrowKeysUsed = true; app.focusRoom(app.topbar.curSideRoomRight); } } return; } // keypress happened outside of sideroom if (e.keyCode === 37 && safeLocation || window.nodewebkit && e.ctrlKey && e.shiftKey && e.keyCode === 9) { // Left or Ctrl+Shift+Tab on desktop client if (app.topbar.curRoomLeft) { e.preventDefault(); e.stopImmediatePropagation(); app.arrowKeysUsed = true; app.focusRoom(app.topbar.curRoomLeft); } } else if (e.keyCode === 39 && safeLocation || window.nodewebkit && e.ctrlKey && e.keyCode === 9) { // Right or Ctrl+Tab on desktop client if (app.topbar.curRoomRight) { e.preventDefault(); e.stopImmediatePropagation(); app.arrowKeysUsed = true; app.focusRoom(app.topbar.curRoomRight); } } }); this.initializeConnection(); Backbone.history.start({pushState: true}); }, /** * Start up the client, including loading teams and preferences, * determining which server to connect to, and actually establishing * a connection to that server. * * Triggers the following events (arguments in brackets): * `init:unsupported` * triggered if the user's browser is unsupported * * `init:loadprefs` * triggered when preferences/teams are finished loading and are * safe to read * * `init:nothirdparty` * triggered if the user has third-party cookies disabled and * third-party cookies/storage are necessary for full functioning * (i.e. stuff will probably be broken for the user, so show an * error message) * * `init:socketopened` * triggered once a socket has been opened to the sim server; this * does NOT mean that the user has signed in yet, merely that the * SockJS connection has been established. * * `init:connectionerror` * triggered if a connection to the sim server could not be * established * * `init:socketclosed` * triggered if the SockJS socket closes */ initializeConnection: function () { if ((document.location.hostname !== Config.origindomain) && !Config.testclient) { // Handle *.psim.us. return this.initializeCrossDomainConnection(); } else if (Config.testclient) { this.initializeTestClient(); } else if (document.location.protocol === 'https:') { /* if (!$.cookie('showdown_ssl')) { // Never used HTTPS before, so we have to copy over the // HTTP origin localStorage. We have to redirect to the // HTTP site in order to do this. We set a cookie // indicating that we redirected for the purpose of copying // over the localStorage. $.cookie('showdown_ssl_convert', 1); return document.location.replace('http://' + document.location.hostname + document.location.pathname); } */ // Renew the `showdown_ssl` cookie. $.cookie('showdown_ssl', 1, {expires: 365 * 3}); } else if (!$.cookie('showdown_ssl')) { // localStorage is currently located on the HTTP origin. if (!$.cookie('showdown_ssl_convert') || !('postMessage' in window)) { // This user is not using HTTPS now and has never used // HTTPS before, so her localStorage is still under the // HTTP origin domain: connect on port 8000, not 443. Config.defaultserver.port = Config.defaultserver.httpport; } else { // First time using HTTPS: copy the existing HTTP storage // over to the HTTPS origin. $(window).on('message', function ($e) { var e = $e.originalEvent; var origin = 'https://' + Config.origindomain; if (e.origin !== origin) return; if (e.data === 'init') { Storage.loadTeams(); e.source.postMessage($.toJSON({ teams: Storage.getPackedTeams(), prefs: $.toJSON(Tools.prefs.data) }), origin); } else if (e.data === 'done') { // Set a cookie to indicate that localStorage is now under // the HTTPS origin. $.cookie('showdown_ssl', 1, {expires: 365 * 3}); localStorage.clear(); return document.location.replace('https://' + document.location.hostname + document.location.pathname); } }); var $iframe = $(''); $('body').append($iframe); return; } } else { // The user is using HTTP right now, but has used HTTPS in the // past, so her localStorage is located on the HTTPS origin: // hence we need to use the cross-domain code to load the // localStorage because the HTTPS origin is considered a // different domain for the purpose of localStorage. return this.initializeCrossDomainConnection(); } // Simple connection: no cross-domain logic needed. Config.server = Config.server || Config.defaultserver; // Config.server.afd = true; Storage.loadTeams(); this.trigger('init:loadprefs'); return this.connect(); }, /** * Initialise the client when running on the file:// filesystem. */ initializeTestClient: function () { var self = this; var showUnsupported = function () { self.addPopupMessage('The requested action is not supported by testclient.html. Please complete this action in the official client instead.'); }; $.get = function (uri, data, callback, type) { if (type === 'html') { uri += '&testclient'; } if (data) { uri += '?testclient'; for (var i in data) { uri += '&' + i + '=' + encodeURIComponent(data[i]); } } if (uri[0] === '/') { // relative URI uri = Tools.resourcePrefix + uri.substr(1); } self.addPopup(ProxyPopup, {uri: uri, callback: callback}); }; $.post = function (/*uri, data, callback, type*/) { showUnsupported(); }; }, /** * Handle a cross-domain connection: that is, a connection where the * client is loaded from a different domain from the one where the * user's localStorage is located. */ initializeCrossDomainConnection: function () { if (!('postMessage' in window)) { // browser does not support cross-document messaging return this.trigger('init:unsupported'); } // If the URI in the address bar is not `play.pokemonshowdown.com`, // we receive teams, prefs, and server connection information from // crossdomain.php on play.pokemonshowdown.com. var self = this; $(window).on('message', (function () { var origin; var callbacks = {}; var callbackIdx = 0; return function ($e) { var e = $e.originalEvent; if ((e.origin === 'http://' + Config.origindomain) || (e.origin === 'https://' + Config.origindomain)) { origin = e.origin; } else { return; // unauthorised source origin } var data = $.parseJSON(e.data); if (data.server) { var postCrossDomainMessage = function (data) { return e.source.postMessage($.toJSON(data), origin); }; // server config information Config.server = data.server; // Config.server.afd = true; if (Config.server.registered && Config.server.id !== 'showdown' && Config.server.id !== 'smogtours') { var $link = $(''); $('head').append($link); } // persistent username self.user.setPersistentName = function (name) { postCrossDomainMessage({ username: ((name !== undefined) ? name : this.get('name')) }); }; // ajax requests $.get = function (uri, data, callback, type) { var idx = callbackIdx++; callbacks[idx] = callback; postCrossDomainMessage({get: [uri, data, idx, type]}); }; $.post = function (uri, data, callback, type) { var idx = callbackIdx++; callbacks[idx] = callback; postCrossDomainMessage({post: [uri, data, idx, type]}); }; // teams if (data.teams) { Storage.loadPackedTeams(data.teams); } else { Storage.teams = []; } self.trigger('init:loadteams'); Storage.saveTeams = function () { postCrossDomainMessage({teams: Storage.packAllTeams(Storage.teams)}); }; // prefs if (data.prefs) { Tools.prefs.data = $.parseJSON(data.prefs); } self.trigger('init:loadprefs'); Tools.prefs.save = function () { postCrossDomainMessage({prefs: $.toJSON(this.data)}); }; // check for third-party cookies being disabled if (data.nothirdparty) { self.trigger('init:nothirdparty'); } // connect self.connect(); } else if (data.ajax) { var idx = data.ajax[0]; if (callbacks[idx]) { callbacks[idx](data.ajax[1]); delete callbacks[idx]; } } }; })()); var $iframe = $( '' ); $('body').append($iframe); }, /** * This function establishes the actual connection to the sim server. * This is intended to be called only by `initializeConnection` above. * Don't call this function directly. */ connect: function () { if (this.down) return; var bannedHosts = ['cool.jit.su']; if (Config.server.banned || bannedHosts.indexOf(Config.server.host) >= 0) { this.addPopupMessage("This server has been deleted for breaking US laws or impersonating PS global staff."); return; } var self = this; var constructSocket = function () { var protocol = (Config.server.port === 443) ? 'https' : 'http'; Config.server.host = $.trim(Config.server.host); return new SockJS(protocol + '://' + Config.server.host + ':' + Config.server.port + Config.sockjsprefix); }; this.socket = constructSocket(); setInterval(function () { if (Config.server.host !== $.trim(Config.server.host)) { app.socket.close(); } }, 500); var socketopened = false; var altport = (Config.server.port === Config.server.altport); var altprefix = false; this.socket.onopen = function () { socketopened = true; if (altport && window.ga) { ga('send', 'event', 'Alt port connection', Config.server.id); } self.trigger('init:socketopened'); var avatar = Tools.prefs('avatar'); if (avatar) { // This will be compatible even with servers that don't support // the second argument for /avatar yet. self.send('/avatar ' + avatar + ',1'); } if (self.sendQueue) { var queue = self.sendQueue; delete self.sendQueue; for (var i = 0; i < queue.length; i++) { self.send(queue[i], true); } } }; this.socket.onmessage = function (msg) { if (window.console && console.log) { console.log('<< ' + msg.data); } if (msg.data.charAt(0) !== '{') { self.receive(msg.data); return; } alert("This server is using an outdated version of Pokémon Showdown and needs to be updated."); }; var reconstructSocket = function (socket) { var s = constructSocket(); s.onopen = socket.onopen; s.onmessage = socket.onmessage; s.onclose = socket.onclose; return s; }; this.socket.onclose = function () { if (!socketopened) { if (Config.server.altport && !altport) { if (document.location.protocol === 'https:') { if (confirm("Could not connect with HTTPS. Try HTTP?")) { return document.location.replace('http://' + document.location.host + document.location.pathname); } } altport = true; Config.server.port = Config.server.altport; self.socket = reconstructSocket(self.socket); return; } if (!altprefix) { altprefix = true; Config.sockjsprefix = ''; self.socket = reconstructSocket(self.socket); return; } return self.trigger('init:connectionerror'); } self.trigger('init:socketclosed'); }; }, dispatchFragment: function (fragment) { if (Config.testclient) { // Fragment dispatching doesn't work in testclient.html. // Just open the main menu. fragment = ''; } if (location.search && window.history) { history.replaceState(null, null, '/'); } fragment = toRoomid(fragment || ''); if (this.initialFragment === undefined) this.initialFragment = fragment; this.tryJoinRoom(fragment); }, /** * Send to sim server */ send: function (data, room) { if (room && room !== 'lobby' && room !== true) { data = room + '|' + data; } else if (room !== true) { data = '|' + data; } if (!this.socket || (this.socket.readyState !== SockJS.OPEN)) { if (!this.sendQueue) this.sendQueue = []; this.sendQueue.push(data); return; } if (window.console && console.log) { console.log('>> ' + data); } this.socket.send(data); }, /** * Send team to sim server */ sendTeam: function (team) { this.send('/utm ' + Storage.getPackedTeam(team)); }, /** * Receive from sim server */ receive: function (data) { var roomid = ''; var autojoined = false; if (data.substr(0, 1) === '>') { var nlIndex = data.indexOf('\n'); if (nlIndex < 0) return; roomid = toRoomid(data.substr(1, nlIndex - 1)); data = data.substr(nlIndex + 1); } if (data.substr(0, 6) === '|init|') { if (!roomid) roomid = 'lobby'; var roomType = data.substr(6); var roomTypeLFIndex = roomType.indexOf('\n'); if (roomTypeLFIndex >= 0) roomType = roomType.substr(0, roomTypeLFIndex); roomType = toId(roomType); if (this.rooms[roomid] || roomid === 'staff' || roomid === 'upperstaff') { // autojoin rooms are joined in background this.addRoom(roomid, roomType, true); } else { this.joinRoom(roomid, roomType, true); } if (roomType === 'chat') autojoined = true; } else if ((data + '|').substr(0, 8) === '|expire|') { var room = this.rooms[roomid]; if (room) { room.expired = true; if (room.updateUser) room.updateUser(); } return; } else if ((data + '|').substr(0, 8) === '|deinit|' || (data + '|').substr(0, 8) === '|noinit|') { if (!roomid) roomid = 'lobby'; if (this.rooms[roomid] && this.rooms[roomid].expired) { // expired rooms aren't closed when left return; } var isdeinit = (data.charAt(1) === 'd'); data = data.substr(8); var pipeIndex = data.indexOf('|'); var errormessage; if (pipeIndex >= 0) { errormessage = data.substr(pipeIndex + 1); data = data.substr(0, pipeIndex); } // handle error codes here // data is the error code if (data === 'namerequired') { var self = this; this.once('init:choosename', function () { self.send('/join ' + roomid); }); } else if (data !== 'namepending') { if (isdeinit) { // deinit if (this.rooms[roomid] && this.rooms[roomid].type === 'chat') { this.removeRoom(roomid, true); this.updateAutojoin(); } else { this.removeRoom(roomid, true); } } else { // noinit this.unjoinRoom(roomid); if (roomid === 'lobby') this.joinRoom('rooms'); } if (errormessage) { this.addPopupMessage(errormessage); } } return; } if (roomid) { if (this.rooms[roomid]) { this.rooms[roomid].receive(data); } if (autojoined) this.updateAutojoin(); return; } // Since roomid is blank, it could be either a global message or // a lobby message. (For bandwidth reasons, lobby messages can // have blank roomids.) // If it starts with a messagetype in the global messagetype // list, we'll assume global; otherwise, we'll assume lobby. var parts; if (data.charAt(0) === '|') { parts = data.substr(1).split('|'); } else { parts = []; } switch (parts[0]) { case 'challstr': if (parts[2]) { this.user.receiveChallstr(parts[1] + '|' + parts[2]); } else { this.user.receiveChallstr(parts[1]); } break; case 'formats': this.parseFormats(parts); break; case 'updateuser': var nlIndex = data.indexOf('\n'); if (nlIndex > 0) { this.receive(data.substr(nlIndex + 1)); nlIndex = parts[3].indexOf('\n'); parts[3] = parts[3].substr(0, nlIndex); } var name = parts[1]; var named = !!+parts[2]; this.user.set({ name: name, userid: toUserid(name), named: named, avatar: parts[3] }); this.user.setPersistentName(named ? name : null); if (named) { this.trigger('init:choosename'); } break; case 'nametaken': app.addPopup(LoginPopup, {name: parts[1] || '', error: parts[2] || ''}); break; case 'queryresponse': var responseData = JSON.parse(data.substr(16 + parts[1].length)); app.trigger('response:' + parts[1], responseData); break; case 'updatechallenges': if (this.rooms['']) { this.rooms[''].updateChallenges($.parseJSON(data.substr(18))); } break; case 'updatesearch': if (this.rooms['']) { this.rooms[''].updateSearch($.parseJSON(data.substr(14))); } break; case 'popup': var maxWidth = undefined; var type = 'semimodal'; data = data.substr(7); if (data.substr(0, 6) === '|wide|') { data = data.substr(6); maxWidth = 960; } if (data.substr(0, 7) === '|modal|') { data = data.substr(7); type = 'modal'; } if (data.substr(0, 6) === '|html|') { data = data.substr(6); app.addPopup(Popup, { type: type, maxWidth: maxWidth, htmlMessage: Tools.sanitizeHTML(data) }); } else { app.addPopup(Popup, { type: type, maxWidth: maxWidth, message: data.replace(/\|\|/g, '\n') }); } if (this.rooms['']) this.rooms[''].resetPending(); break; case 'pm': var message = parts.slice(3).join('|'); this.rooms[''].addPM(parts[1], message, parts[2]); break; case 'roomerror': // deprecated; use |deinit| or |noinit| this.unjoinRoom(parts[1]); this.addPopupMessage(parts.slice(2).join('|')); break; case 'refresh': // refresh the page document.location.reload(true); break; case 'c': case 'chat': if (parts[1] === '~') { if (parts[2].substr(0, 6) === '/warn ') { app.addPopup(RulesPopup, {warning: parts[2].substr(6)}); break; } } /* fall through */ default: // the messagetype wasn't in our list of recognized global // messagetypes; so the message is presumed to be for the // lobby. if (this.rooms['lobby']) { this.rooms['lobby'].receive(data); } break; } }, parseFormats: function (formatsList) { var isSection = false; var section = ''; var column = 0; var columnChanged = false; BattleFormats = {}; for (var j = 1; j < formatsList.length; j++) { if (isSection) { section = formatsList[j]; isSection = false; } else if (formatsList[j] === '' || (formatsList[j].substr(0, 1) === ',' && !isNaN(formatsList[j].substr(1)))) { isSection = true; if (formatsList[j]) { var newColumn = parseInt(formatsList[j].substr(1)) || 0; if (column !== newColumn) { column = newColumn; columnChanged = true; } } } else { var name = formatsList[j]; var searchShow = true; var challengeShow = true; var team = null; var lastCommaIndex = name.lastIndexOf(','); var code = lastCommaIndex >= 0 ? parseInt(name.substr(lastCommaIndex + 1), 16) : NaN; if (!isNaN(code)) { name = name.substr(0, lastCommaIndex); if (code & 1) team = 'preset'; if (!(code & 2)) searchShow = false; if (!(code & 4)) challengeShow = false; if (!(code & 8)) tournamentShow = false; } else { // Backwards compatibility: late 0.9.0 -> 0.10.0 if (name.substr(name.length - 2) === ',#') { // preset teams team = 'preset'; name = name.substr(0, name.length - 2); } if (name.substr(name.length - 2) === ',,') { // search-only challengeShow = false; name = name.substr(0, name.length - 2); } else if (name.substr(name.length - 1) === ',') { // challenge-only searchShow = false; name = name.substr(0, name.length - 1); } } var id = toId(name); var isTeambuilderFormat = searchShow && !team; var teambuilderFormat = ''; if (isTeambuilderFormat) { var parenPos = name.indexOf('('); if (parenPos > 0 && name.charAt(name.length - 1) === ')') { // variation of existing tier teambuilderFormat = toId(name.substr(0, parenPos)); if (BattleFormats[teambuilderFormat]) { BattleFormats[teambuilderFormat].isTeambuilderFormat = true; } else { BattleFormats[teambuilderFormat] = { id: teambuilderFormat, name: $.trim(name.substr(0, parenPos)), team: team, section: section, column: column, rated: false, isTeambuilderFormat: true, effectType: 'Format' }; } isTeambuilderFormat = false; } } if (BattleFormats[id] && BattleFormats[id].isTeambuilderFormat) { isTeambuilderFormat = true; } BattleFormats[id] = { id: id, name: name, team: team, section: section, column: column, searchShow: searchShow, challengeShow: challengeShow, rated: searchShow && id.substr(0, 7) !== 'unrated', teambuilderFormat: teambuilderFormat, isTeambuilderFormat: isTeambuilderFormat, effectType: 'Format' }; } } if (columnChanged) app.supports['formatColumns'] = true; this.trigger('init:formats'); }, uploadReplay: function (data) { var id = data.id; var serverid = Config.server.id && toId(Config.server.id.split(':')[0]); if (serverid && serverid !== 'showdown') id = serverid + '-' + id; $.post(app.user.getActionPHP() + '?act=uploadreplay', { log: data.log, id: id }, function (data) { if ((serverid === 'showdown') && (data === 'invalid id')) { data = 'not found'; } if (data === 'success') { app.addPopup(ReplayUploadedPopup, {id: id}); } else if (data === 'hash mismatch') { app.addPopupMessage("Someone else is already uploading a replay of this battle. Try again in five seconds."); } else if (data === 'not found') { app.addPopupMessage("This server isn't registered, and doesn't support uploading replays."); } else if (data === 'invalid id') { app.addPopupMessage("This server is using invalid battle IDs, so this replay can't be uploaded."); } else { app.addPopupMessage("Error while uploading replay: " + data); } }); }, roomsResponse: function (data) { app.supports['rooms'] = true; if (data) { this.roomsData = data; } }, clearGlobalListeners: function () { // jslider doesn't clear these when it should, // so we have to do it for them :/ $(document).off('click touchstart mousedown touchmove mousemove touchend mouseup'); }, /********************************************************* * Rooms *********************************************************/ initializeRooms: function () { this.rooms = Object.create(null); // {} $(window).on('resize', _.bind(this.resize, this)); }, fixedWidth: true, resize: function () { if (window.screen && screen.width && screen.width >= 320) { if (this.fixedWidth) { document.getElementById('viewport').setAttribute('content', 'width=device-width'); this.fixedWidth = false; } } else { if (!this.fixedWidth) { document.getElementById('viewport').setAttribute('content', 'width=320'); this.fixedWidth = true; } } if (!app.roomsFirstOpen && !this.down && $(window).width() >= 916 && document.location.hostname === 'play.pokemonshowdown.com') { this.addRoom('rooms'); } this.updateLayout(); }, // the currently active room curRoom: null, curSideRoom: null, sideRoom: null, joinRoom: function (id, type, nojoin) { if (this.rooms[id]) { this.focusRoom(id); return this.rooms[id]; } if (id.substr(0, 11) === 'battle-gen5' && !Tools.loadedSpriteData['bw']) Tools.loadSpriteData('bw'); var room = this._addRoom(id, type, nojoin); this.focusRoom(id); return room; }, /** * We tried to join a room but it didn't exist */ unjoinRoom: function (id) { if (Config.server.id && this.rooms[id] && this.rooms[id].type === 'battle') { if (id === this.initialFragment) { // you were direct-linked to this nonexistent room var replayid = id.substr(7); if (Config.server.id !== 'showdown') replayid = Config.server.id + '-' + replayid; // document.location.replace('http://replay.pokemonshowdown.com/' + replayid); var replayLink = 'http://replay.pokemonshowdown.com/' + replayid; app.addPopupMessage('This room does not exist. You might want to try the replay: ' + replayLink + ''); return; } } this.removeRoom(id, true); if (this.curRoom) this.navigate(this.curRoom.id, {replace: true}); }, tryJoinRoom: function (id) { this.joinRoom(id); }, addRoom: function (id, type, nojoin, title) { this._addRoom(id, type, nojoin, title); this.updateSideRoom(); this.updateLayout(); }, _addRoom: function (id, type, nojoin, title) { var oldRoom; if (this.rooms[id]) { if (type && this.rooms[id].type !== type) { // this room changed type // (or the type we guessed it would be was wrong) var oldRoom = this.rooms[id]; oldRoom.destroy(); delete this.rooms[id]; } else { return this.rooms[id]; } } var el; if (!id) { el = $('#mainmenu'); } else { el = $('
').appendTo('body'); } var typeName = ''; if (typeof type === 'string') { typeName = type; type = null; } var roomTable = { '': MainMenuRoom, 'teambuilder': TeambuilderRoom, 'rooms': RoomsRoom, 'ladder': LadderRoom, 'lobby': ChatRoom, 'staff': ChatRoom, 'constructor': ChatRoom }; var typeTable = { 'battle': BattleRoom, 'chat': ChatRoom }; // the passed type overrides everything else if (typeName) type = typeTable[typeName]; // otherwise, the room table has precedence if (!type) type = roomTable[id]; // otherwise, infer the room type if (!type) { if (id.substr(0, 7) === 'battle-') { type = BattleRoom; } else { type = ChatRoom; } } var room = this.rooms[id] = new type({ id: id, el: el, nojoin: nojoin, title: title }); if (oldRoom) { if (this.curRoom === oldRoom) this.curRoom = room; if (this.curSideRoom === oldRoom) this.curSideRoom = room; if (this.sideRoom === oldRoom) this.sideRoom = room; } return room; }, focusRoom: function (id) { var room = this.rooms[id]; if (!room) return false; if (this.curRoom === room || this.curSideRoom === room) { room.focus(); return true; } this.updateSideRoom(id); this.updateLayout(); if (this.curSideRoom !== room) { if (this.curRoom) { this.curRoom.hide(); this.curRoom = null; } else if (this.rooms['']) { this.rooms[''].hide(); } this.curRoom = window.room = room; this.updateLayout(); if (this.curRoom.id === id) this.navigate(id); } room.focus(); return; }, updateLayout: function () { if (!this.curRoom) return; // can happen during initialization this.dismissPopups(); if (!this.sideRoom) { this.curRoom.show('full'); if (this.curRoom.id === '') { if ($(window).width() < this.curRoom.bestWidth) { this.curRoom.$el.addClass('tiny-layout'); } else { this.curRoom.$el.removeClass('tiny-layout'); } } this.topbar.updateTabbar(); return; } var leftMin = (this.curRoom.minWidth || this.curRoom.bestWidth); var leftMinMain = (this.curRoom.minMainWidth || leftMin); var rightMin = (this.sideRoom.minWidth || this.sideRoom.bestWidth); var available = $(window).width(); if (this.curRoom.isSideRoom) { // we're trying to focus a side room if (available >= this.rooms[''].tinyWidth + leftMinMain) { // it fits to the right of the main menu, so do that this.curSideRoom = this.sideRoom = this.curRoom; this.curRoom = this.rooms['']; leftMin = this.curRoom.tinyWidth; rightMin = (this.sideRoom.minWidth || this.sideRoom.bestWidth); } else if (this.sideRoom) { // nooo if (this.curSideRoom) { this.curSideRoom.hide(); this.curSideRoom = null; } this.curRoom.show('full'); this.topbar.updateTabbar(); return; } } else if (this.curRoom.id === '') { leftMin = this.curRoom.tinyWidth; } if (available < leftMin + rightMin) { if (this.curSideRoom) { this.curSideRoom.hide(); this.curSideRoom = null; } if ($(window).width() < this.curRoom.bestWidth) { this.curRoom.$el.addClass('tiny-layout'); } else { this.curRoom.$el.removeClass('tiny-layout'); } this.curRoom.show('full'); this.topbar.updateTabbar(); return; } this.curSideRoom = this.sideRoom; if (leftMin === this.curRoom.tinyWidth) { if (available < this.curRoom.bestWidth + 570) { // there's only room for the tiny layout :( this.curRoom.show('left', leftMin); this.curRoom.$el.addClass('tiny-layout'); this.curSideRoom.show('right', leftMin); this.topbar.updateTabbar(); return; } leftMin = (this.curRoom.minWidth || this.curRoom.bestWidth); this.curRoom.$el.removeClass('tiny-layout'); } var leftMax = (this.curRoom.maxWidth || this.curRoom.bestWidth); var rightMax = (this.sideRoom.maxWidth || this.sideRoom.bestWidth); var leftWidth = leftMin; if (leftMax + rightMax <= available) { leftWidth = leftMax; } else { var bufAvailable = available - leftMin - rightMin; var wanted = leftMax - leftMin + rightMax - rightMin; if (wanted) leftWidth = Math.floor(leftMin + (leftMax - leftMin) * bufAvailable / wanted); } this.curRoom.show('left', leftWidth); this.curSideRoom.show('right', leftWidth); this.topbar.updateTabbar(); }, updateSideRoom: function (id) { if (id && this.rooms[id].isSideRoom) { this.sideRoom = this.rooms[id]; if (this.curSideRoom && this.curSideRoom !== this.sideRoom) { this.curSideRoom.hide(); this.curSideRoom = this.sideRoom; } // updateLayout will null curSideRoom if there's // no room for this room } else if (!this.sideRoom) { for (var i in this.rooms) { if (this.rooms[i].isSideRoom) { this.sideRoom = this.rooms[i]; } } } }, leaveRoom: function (id, e) { var room = this.rooms[id]; if (!room) return false; if (room.requestLeave && !room.requestLeave(e)) return false; return this.removeRoom(id); }, removeRoom: function (id, alreadyLeft) { var room = this.rooms[id]; if (room) { if (room === this.curRoom) this.focusRoom(''); delete this.rooms[id]; room.destroy(alreadyLeft); if (room === this.sideRoom) { this.sideRoom = null; this.curSideRoom = null; this.updateSideRoom(); } this.updateLayout(); return true; } return false; }, openInNewWindow: function (url) { if (window.nodewebkit) { gui.Shell.openExternal(url); } else { window.open(url, '_blank'); } }, clickLink: function (e) { if (window.nodewebkit) { gui.Shell.openExternal(e.target.href); return false; } }, updateAutojoin: function () { if (Config.server.id !== 'showdown') return; var autojoins = []; var autojoinCount = 0; for (var i in this.rooms) { if (!this.rooms[i]) continue; if (this.rooms[i].type !== 'chat' || i === 'lobby') { continue; } autojoins.push(this.rooms[i].title || this.rooms[i].id); if (i === 'staff' || i === 'upperstaff') continue; autojoinCount++; if (autojoinCount >= 8) break; } Tools.prefs('autojoin', autojoins.join(',')); }, /********************************************************* * Popups *********************************************************/ popups: null, initializePopups: function () { this.popups = []; }, addPopup: function (type, data) { if (!data) data = {}; if (data.sourceEl === undefined && app.dispatchingButton) { data.sourceEl = app.dispatchingButton; } if (data.sourcePopup === undefined && app.dispatchingPopup) { data.sourcePopup = app.dispatchingPopup; } if (this.dismissingSource && $(data.sourceEl)[0] === this.dismissingSource) return; while (this.popups.length) { var prevPopup = this.popups[this.popups.length - 1]; if (data.sourcePopup === prevPopup) { break; } var sourceEl = prevPopup.sourceEl ? prevPopup.sourceEl[0] : null; this.popups.pop().remove(); if ($(data.sourceEl)[0] === sourceEl) return; } if (!type) type = Popup; var popup = new type(data); var $overlay; if (popup.type === 'normal') { $('body').append(popup.el); } else { $overlay = $('').appendTo('body').append(popup.el); if (popup.type === 'semimodal') { $overlay.on('click', function (e) { if (e.currentTarget === e.target) { popup.close(); } }); } } if (popup.domInitialize) popup.domInitialize(data); popup.$('.autofocus').select().focus(); if ($overlay) $overlay.scrollTop(0); this.popups.push(popup); return popup; }, addPopupMessage: function (message) { // shorthand for adding a popup message // this is the equivalent of alert(message) app.addPopup(Popup, {message: message}); }, addPopupPrompt: function (message, buttonOrCallback, callback) { var button = (callback ? buttonOrCallback : 'OK'); callback = (!callback ? buttonOrCallback : callback); app.addPopup(PromptPopup, {message: message, button: button, callback: callback}); }, closePopup: function (id) { if (this.popups.length) { var popup = this.popups.pop(); popup.remove(); if (this.reconnectPending) this.addPopup(ReconnectPopup); return true; } return false; }, dismissPopups: function () { var source = false; while (this.popups.length) { var popup = this.popups[this.popups.length - 1]; if (popup.type !== 'normal') return source; if (popup.sourceEl) source = popup.sourceEl[0]; if (!source) source = true; this.popups.pop().remove(); } return source; } }); var Topbar = this.Topbar = Backbone.View.extend({ events: { 'click a': 'click', 'click .username': 'clickUsername', 'click button': 'dispatchClickButton' }, initialize: function () { this.$el.html('
');
this.$tabbar = this.$('.maintabbar .inner');
// this.$sidetabbar = this.$('.sidetabbar');
this.$userbar = this.$('.userbar');
this.updateTabbar();
app.user.on('change', this.updateUserbar, this);
this.updateUserbar();
},
// userbar
updateUserbar: function () {
var buf = '';
var name = ' ' + app.user.get('name');
var color = hashColor(app.user.get('userid'));
if (app.user.get('named')) {
buf = ' ' + Tools.escapeHTML(name) + ' ';
} else {
buf = ' ';
}
this.$userbar.html(buf);
},
login: function () {
app.addPopup(LoginPopup);
},
openSounds: function () {
app.addPopup(SoundsPopup);
},
openOptions: function () {
app.addPopup(OptionsPopup);
},
clickUsername: function (e) {
e.stopPropagation();
var name = $(e.currentTarget).data('name');
app.addPopup(UserPopup, {name: name, sourceEl: e.currentTarget});
},
// tabbar
renderRoomTab: function (room) {
if (!room) return '';
var id = room.id;
var buf = '' + (data.htmlMessage || Tools.parseMessage(data.message)) + '
').css('max-width', data.maxWidth || 480); }, dispatchClickButton: function (e) { var target = e.currentTarget; if (target.name) { app.dispatchingButton = target; app.dispatchingPopup = this; e.preventDefault(); e.stopImmediatePropagation(); this[target.name].call(this, target.value, target); delete app.dispatchingButton; delete app.dispatchingPopup; } }, dispatchSubmit: function (e) { e.preventDefault(); e.stopPropagation(); var dataArray = $(e.currentTarget).serializeArray(); var data = {}; for (var i = 0, len = dataArray.length; i < len; i++) { var name = dataArray[i].name, value = dataArray[i].value; if (data[name]) { if (!data[name].push) data[name] = [data[name]]; data[name].push(value || ''); } else { data[name] = (value || ''); } } this.submit(data); }, send: function (data) { app.send(data); }, remove: function () { var $parent = this.$el.parent(); Backbone.View.prototype.remove.apply(this, arguments); if ($parent.hasClass('ps-overlay')) $parent.remove(); }, close: function () { app.closePopup(); } }); var PromptPopup = this.PromptPopup = Popup.extend({ type: 'semimodal', initialize: function (data) { if (!data || !data.message || typeof data.callback !== "function") return; this.callback = data.callback; var buf = ''; this.$el.html(buf); }, submit: function (data) { this.close(); this.callback(data.data); } }); var UserPopup = this.UserPopup = Popup.extend({ initialize: function (data) { data.userid = toId(data.name); var name = data.name; if (/[a-zA-Z0-9]/.test(name.charAt(0))) name = ' ' + name; this.data = data = _.extend(data, UserPopup.dataCache[data.userid]); data.name = name; app.on('response:userdetails', this.update, this); app.send('/cmd userdetails ' + data.userid); this.update(); }, events: { 'click .ilink': 'clickLink', 'click .yours': 'avatars' }, update: function (data) { if (data && data.userid === this.data.userid) { data = _.extend(this.data, data); UserPopup.dataCache[data.userid] = data; } else { data = this.data; } var userid = data.userid; var name = data.name; var avatar = data.avatar || ''; var groupDetails = { '#': "Room Owner (#)", '~': "Administrator (~)", '&': "Leader (&)", '@': "Moderator (@)", '%': "Driver (%)", '\u2605': "Player (\u2605)", '+': "Voiced (+)", '‽': "Locked (‽)", '!': "Muted (!)" }; var group = (groupDetails[name.substr(0, 1)] || ''); if (group || name.charAt(0) === ' ') name = name.substr(1); var buf = '' + (muted ? '(muted)' : '') + '
'; buf += '' + (muted ? '(muted)' : '') + '
'; buf += ''; this.$el.html(buf).css('min-width', 160); }, events: { 'change input[name=muted]': 'setMute' }, domInitialize: function () { var self = this; this.$('.effect-volume input').slider({ from: 0, to: 100, step: 1, dimension: '%', skin: 'round_plastic', onstatechange: function (val) { self.setEffectVolume(val); } }); this.$('.music-volume input').slider({ from: 0, to: 100, step: 1, dimension: '%', skin: 'round_plastic', onstatechange: function (val) { self.setMusicVolume(val); } }); }, setMute: function (e) { var muted = !!e.currentTarget.checked; Tools.prefs('mute', muted); BattleSound.setMute(muted); if (!muted) { this.$('.effect-volume').html(''); this.$('.music-volume').html(''); this.domInitialize(); } else { this.$('.effect-volume').html('(muted)'); this.$('.music-volume').html('(muted)'); } app.topbar.$('button[name=openSounds]').html(''); }, setEffectVolume: function (volume) { BattleSound.setEffectVolume(volume); Tools.prefs('effectvolume', volume); }, setMusicVolume: function (volume) { BattleSound.setBgmVolume(volume); Tools.prefs('musicvolume', volume); } }); var OptionsPopup = this.OptionsPopup = Popup.extend({ initialize: function (data) { app.user.on('change', this.update, this); app.send('/cmd userdetails ' + app.user.get('userid')); this.update(); }, events: { 'change input[name=noanim]': 'setNoanim', 'change input[name=bwgfx]': 'setBwgfx', 'change input[name=nopastgens]': 'setNopastgens', 'change input[name=notournaments]': 'setNotournaments', 'change input[name=inchatpm]': 'setInchatpm', 'change input[name=temporarynotifications]': 'setTemporaryNotifications', 'change select[name=bg]': 'setBg', 'change select[name=timestamps-lobby]': 'setTimestampsLobby', 'change select[name=timestamps-pms]': 'setTimestampsPMs', 'change input[name=logchat]': 'setLogChat', 'change input[name=selfhighlight]': 'setSelfHighlight', 'click img': 'avatars' }, update: function () { var name = app.user.get('name'); var avatar = app.user.get('avatar'); var buf = ''; buf += '' + (avatar ? '' : '') + '' + Tools.escapeHTML(name) + '
You can choose to display formatted text as normal text.
'; buf += ''; buf += ''; buf += ''; buf += ''; buf += ''; buf += ''; buf += ''; buf += ''; this.$el.html(buf); }, setOption: function (e) { var name = $(e.currentTarget).prop('name'); this.chatformatting['hide' + name] = !!e.currentTarget.checked; Tools.prefs('chatformatting', this.chatformatting); } }); var AvatarsPopup = this.AvatarsPopup = Popup.extend({ type: 'semimodal', initialize: function () { var cur = +app.user.get('avatar'); var buf = ''; buf += 'Choose an avatar or
'; buf += 'Your replay has been uploaded! It\'s available at:
'; buf += 'http://replay.pokemonshowdown.com/' + data.id + '
'; buf += ''; this.$el.html(buf).css('max-width', 620); }, clickClose: function () { this.close(); }, submit: function (i) { app.openInNewWindow('http://replay.pokemonshowdown.com/' + this.id); this.close(); } }); var RulesPopup = this.RulesPopup = Popup.extend({ type: 'modal', initialize: function (data) { var warning = ('warning' in data); var buf = ''; if (warning) { buf += '
' + (Tools.escapeHTML(data.warning) || 'You have been warned for breaking the rules.') + '
'; } buf += 'Choose a custom background
'; buf += ''; buf += ''; buf += ''; this.$el.html(buf); }, setBg: function (e) { $('.bgstatus').text('Changing background image.'); var file = e.currentTarget.files[0]; CustomBackgroundPopup.readFile(file, this); } }); CustomBackgroundPopup.readFile = function (file, popup) { var reader = new FileReader(); reader.onload = function (e) { var bg = '#344b6c url(' + e.target.result + ') no-repeat left center fixed'; try { Tools.prefs('bg', bg); } catch (e) { if (popup) { $('.bgstatus').text("Image too large, upload a background whose size is 3.5MB or less."); } else { app.addPopupMessage("Image too large, upload a background whose size is 3.5MB or less."); } return; } $(document.body).css({ background: bg, 'background-size': 'cover' }); if (popup) popup.close(); }; reader.readAsDataURL(file); }; }).call(this, jQuery);