(function($) {
if (window.nodewebkit) {
window.gui = require('nw.gui');
$('body').on('click', 'a', function(e) {
if (this.target === '_blank') {
gui.Shell.openExternal(this.href);
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
});
window.nwWindow = gui.Window.get();
}
Config.version = '0.9.2';
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, '');
};
// 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 = 'http://' + 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) {
if (this.get('userid') !== toUserid(name)) {
var query = this.getActionPHP() + '?act=getassertion&userid=' +
encodeURIComponent(toUserid(name)) +
'&challengekeyid=' + encodeURIComponent(this.challengekeyid) +
'&challenge=' + encodeURIComponent(this.challenge);
var self = this;
$.get(query, 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,
challengekeyid: this.challengekeyid,
challenge: this.challenge
}, 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');
},
challengekeyid: -1,
challenge: '',
receiveChallenge: function(attrs) {
if (attrs.challenge) {
/**
* 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.
*/
var query = this.getActionPHP() + '?act=upkeep' +
'&challengekeyid=' + encodeURIComponent(attrs.challengekeyid) +
'&challenge=' + encodeURIComponent(attrs.challenge);
var self = this;
$.get(query, Tools.safeJSON(function(data) {
if (!data.username) return;
if (data.loggedin) {
self.set('registered', {
username: data.username,
userid: toUserid(data.username)
});
}
self.finishRename(data.username, data.assertion);
}), 'text');
}
this.challengekeyid = attrs.challengekeyid;
this.challenge = attrs.challenge;
},
/**
* 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 = {};
// down
// if (document.location.hostname === 'play.pokemonshowdown.com') this.down = 'dos';
if (document.location.hostname === 'play.pokemonshowdown.com') {
app.supportsRooms = 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');
} else {
this.addRoom('lobby');
}
}
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('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.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.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.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.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: $.toJSON(Storage.teams),
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, callback, type) {
if (type === 'html') {
uri += '&testclient';
}
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) {
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, callback, type) {
var idx = callbackIdx++;
callbacks[idx] = callback;
postCrossDomainMessage({get: [uri, 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.teams = $.parseJSON(data.teams) || [];
} else {
Storage.teams = [];
}
self.trigger('init:loadteams');
Storage.saveTeams = function() {
postCrossDomainMessage({teams: $.toJSON(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 self = this;
var constructSocket = function() {
var protocol = (Config.server.port === 443) ? 'https' : 'http';
return new SockJS(protocol + '://' + Config.server.host + ':' +
Config.server.port + Config.sockjsprefix);
};
this.socket = constructSocket();
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 '+Tools.parseMessage(data.message)+'
');
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
updateTabbar: function() {
var curId = (app.curRoom ? app.curRoom.id : '');
var curSideId = (app.curSideRoom ? app.curSideRoom.id : '');
var buf = '
';
var atLeastOne = false;
var sideBuf = '';
this.curRoomLeft = '';
this.curRoomRight = '';
this.curSideRoomLeft = '';
this.curSideRoomRight = '';
var passedCurRoom = false;
var passedCurSideRoom = false;
for (var id in app.rooms) {
if (!id || id === 'teambuilder' || id === 'ladder') continue;
var room = app.rooms[id];
var name = ' '+(Tools.escapeHTML(room.title)||(id==='lobby'?'Lobby':id))+'';
if (id.substr(0,7) === 'battle-') {
name = Tools.escapeHTML(room.title);
var formatid = id.substr(7).split('-')[0];
if (!name) {
var p1 = (room && room.battle && room.battle.p1 && room.battle.p1.name) || '';
var p2 = (room && room.battle && room.battle.p2 && room.battle.p2.name) || '';
if (p1 && p2) {
name = ''+Tools.escapeHTML(p1)+' v. '+Tools.escapeHTML(p2);
} else if (p1 || p2) {
name = ''+Tools.escapeHTML(p1)+Tools.escapeHTML(p2);
} else {
name = '(empty room)';
}
}
name = ''+formatid+''+name+'';
}
if (room.isSideRoom) {
if (id !== 'rooms') {
sideBuf += '';
atLeastOne = true;
}
buf += '
';
if (sideBuf) {
if (app.curSideRoom) {
buf += ''+sideBuf+'
';
} else {
buf += ''+sideBuf+'
';
}
}
this.$tabbar.html(buf);
var $lastLi = this.$tabbar.children().last().children().last();
var offset = $lastLi.offset();
var width = $lastLi.outerWidth();
if (offset.top >= 37 || offset.left + width > $(window).width() - 165) {
this.$tabbar.append('');
}
if (app.rooms['']) app.rooms[''].updateRightMenu();
},
dispatchClickButton: function(e) {
var target = e.currentTarget;
if (target.name) {
app.dismissingSource = app.dismissPopups();
app.dispatchingButton = target;
e.preventDefault();
e.stopImmediatePropagation();
this[target.name].call(this, target.value, target);
delete app.dismissingSource;
delete app.dispatchingButton;
}
},
click: function(e) {
if (e.cmdKey || e.metaKey || e.ctrlKey) return;
e.preventDefault();
var $target = $(e.currentTarget);
var id = $target.attr('href');
if (id.substr(0, app.root.length) === app.root) {
id = id.substr(app.root.length);
}
if ($target.hasClass('closebutton')) {
app.leaveRoom(id);
} else {
app.joinRoom(id);
}
},
tablist: function() {
app.addPopup(TabListPopup);
}
});
var Room = this.Room = Backbone.View.extend({
className: 'ps-room',
constructor: function(options) {
if (!this.events) this.events = {};
if (!this.events['click button']) this.events['click button'] = 'dispatchClickButton';
if (!this.events['click']) this.events['click'] = 'dispatchClickBackground';
Backbone.View.apply(this, arguments);
if (!(options && options.nojoin)) this.join();
},
dispatchClickButton: function(e) {
var target = e.currentTarget;
if (target.name) {
app.dismissingSource = app.dismissPopups();
app.dispatchingButton = target;
e.preventDefault();
e.stopImmediatePropagation();
this[target.name].call(this, target.value, target);
delete app.dismissingSource;
delete app.dispatchingButton;
}
},
dispatchClickBackground: function(e) {
app.dismissPopups();
if (e.shiftKey || (window.getSelection && !window.getSelection().isCollapsed)) {
return;
}
this.focus();
},
// communication
/**
* Send to sim server
*/
send: function(data) {
app.send(data, this.id);
},
/**
* Receive from sim server
*/
receive: function(data) {
//
},
// layout
bestWidth: 659,
show: function(position, leftWidth) {
switch (position) {
case 'left':
this.$el.css({left: 0, width: leftWidth, right: 'auto'});
break;
case 'right':
this.$el.css({left: leftWidth+1, width: 'auto', right: 0});
this.leftWidth = leftWidth;
break;
case 'full':
this.$el.css({left: 0, width: 'auto', right: 0});
break;
}
this.$el.show();
this.dismissNotification();
},
hide: function() {
this.blur();
this.$el.hide();
},
focus: function() {},
blur: function() {},
join: function() {},
leave: function() {},
// notifications
requestNotifications: function() {
try {
if (window.webkitNotifications && webkitNotifications.requestPermission) {
// Notification.requestPermission crashes Chrome 23:
// https://code.google.com/p/chromium/issues/detail?id=139594
// In lieu of a way to detect Chrome 23, we'll just use the old
// requestPermission API, which works to request permissions for
// the new Notification spec anyway.
webkitNotifications.requestPermission();
} else if (window.Notification && Notification.requestPermission) {
Notification.requestPermission(function(permission) {});
}
} catch (e) {}
},
notifications: null,
notify: function(title, body, tag, once) {
if (once && app.focused && (this === app.curRoom || this == app.curSideRoom)) return;
if (!tag) tag = 'message';
if (!this.notifications) this.notifications = {};
if (app.focused && (this === app.curRoom || this == app.curSideRoom)) {
this.notifications[tag] = {};
} else if (window.nodewebkit) {
nwWindow.requestAttention(true);
} else if (window.Notification) {
// old one doesn't need to be closed; sending the tag should
// automatically replace the old notification
var notification = this.notifications[tag] = new Notification(title, {
lang: 'en',
body: body,
tag: this.id+':'+tag,
});
var self = this;
notification.onclose = function() {
self.dismissNotification(tag);
};
notification.onclick = function() {
self.clickNotification(tag);
};
if (Tools.prefs('temporarynotifications')) {
if (notification.cancel) {
setTimeout(function() {notification.cancel();}, 5000);
} else if (notification.close) {
setTimeout(function() {notification.close();}, 5000);
}
}
if (once) notification.psAutoclose = true;
} else if (window.macgap) {
macgap.growl.notify({
title: title,
content: body
});
var notification = {};
this.notifications[tag] = notification;
if (once) notification.psAutoclose = true;
} else {
var notification = {};
this.notifications[tag] = notification;
if (once) notification.psAutoclose = true;
}
app.topbar.updateTabbar();
},
notifyOnce: function(title, body, tag) {
return this.notify(title, body, tag, true);
},
closeNotification: function(tag, alreadyClosed) {
if (window.nodewebkit) nwWindow.requestAttention(false);
if (!this.notifications) return;
if (!tag) {
for (tag in this.notifications) {
if (this.notifications[tag].close) this.notifications[tag].close();
}
this.notifications = null;
app.topbar.updateTabbar();
return;
}
if (!this.notifications[tag]) return;
if (!alreadyClosed && this.notifications[tag].close) this.notifications[tag].close();
delete this.notifications[tag];
if (_.isEmpty(this.notifications)) {
this.notifications = null;
app.topbar.updateTabbar();
}
},
dismissNotification: function(tag) {
if (window.nodewebkit) nwWindow.requestAttention(false);
if (!this.notifications) return;
if (!tag) {
for (tag in this.notifications) {
if (!this.notifications[tag].psAutoclose) continue;
if (this.notifications[tag].close) this.notifications[tag].close();
delete this.notifications[tag];
}
if (_.isEmpty(this.notifications)) {
this.notifications = null;
app.topbar.updateTabbar();
}
return;
}
if (!this.notifications[tag]) return;
if (this.notifications[tag].close) this.notifications[tag].close();
if (this.notifications[tag].psAutoclose) {
delete this.notifications[tag];
if (_.isEmpty(this.notifications)) {
this.notifications = null;
app.topbar.updateTabbar();
}
} else {
this.notifications[tag] = {};
}
},
clickNotification: function(tag) {
this.dismissNotification(tag);
app.focusRoom(this.id);
},
close: function() {
app.leaveRoom(this.id);
},
// allocation
destroy: function() {
this.closeNotification();
this.leave();
this.remove();
delete this.app;
}
});
var Popup = this.Popup = Backbone.View.extend({
// If type is 'modal', background will turn gray and popup won't be
// dismissible except by interacting with it.
// If type is 'semimodal', background will turn gray, but clicking
// the background will dismiss it.
// Otherwise, background won't change, and interacting with anything
// other than the popup will still be possible (and will dismiss
// the popup).
type: 'normal',
className: 'ps-popup',
constructor: function(data) {
if (!this.events) this.events = {};
if (!this.events['click button']) this.events['click button'] = 'dispatchClickButton';
if (!this.events['submit form']) this.events['submit form'] = 'dispatchSubmit';
if (data && data.sourceEl) {
this.sourceEl = data.sourceEl = $(data.sourceEl);
}
if (data.type) this.type = data.type;
if (data.position) this.position = data.position;
Backbone.View.apply(this, arguments);
// if we have no source, we can't attach to anything
if (this.type === 'normal' && !this.sourceEl) this.type = 'semimodal';
if (this.type === 'normal') {
// nonmodal popup: should be positioned near source element
var $el = this.$el;
var $measurer = $('').appendTo('body').append($el);
$el.css('width', this.width - 22);
var offset = this.sourceEl.offset();
var room = $(window).height();
var height = $el.outerHeight();
var width = $el.outerWidth();
var sourceHeight = this.sourceEl.outerHeight();
if (this.position === 'right') {
if (room > offset.top + height + 5 &&
(offset.top < room * 2/3 || offset.top + 200 < room)) {
$el.css('top', offset.top);
} else {
$el.css('bottom', Math.max(room - offset.top - sourceHeight, 0));
}
$el.css('left', offset.left + this.sourceEl.outerWidth());
} else {
if (room > offset.top + sourceHeight + height + 5 &&
(offset.top + sourceHeight < room * 2/3 || offset.top + sourceHeight + 200 < room)) {
$el.css('top', offset.top + sourceHeight);
} else if (height + 5 <= offset.top) {
$el.css('bottom', room - offset.top);
} else if (height + 10 < room) {
$el.css('bottom', 5);
} else {
$el.css('top', 0);
}
room = $(window).width() - offset.left;
if (room < width + 10) {
$el.css('right', 10);
} else {
$el.css('left', offset.left);
}
}
$el.detach();
$measurer.remove();
}
},
initialize: function(data) {
this.type = 'semimodal';
this.$el.html('
'+(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=notournaments]': 'setNotournaments', 'change input[name=nolobbypm]': 'setNolobbypm', 'change input[name=temporarynotifications]': 'setTemporaryNotifications', 'change input[name=ignorespects]': 'setIgnoreSpects', '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://pokemonshowdown.com/replay/battle-'+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 += '