Stub Preact rewrite

HIGHLY UNFINISHED CODE
This commit is contained in:
Guangcong Luo
2018-10-16 15:18:35 -05:00
parent 137dc317b7
commit f8d251c8a1
9 changed files with 867 additions and 1 deletions

View File

@@ -10,6 +10,11 @@ node_modules/
/js/battle-dex.js
/js/battle-dex-data.js
/js/battle-animations-moves.js
/js/battle-scene-stub.js
/js/battle-animations.js
/js/battle-tooltips.js
/js/battle-scene-stub.js
/js/client-core.js
/js/client-main.js
/js/client-frame.js
/js/panel-mainmenu.js
/js/panel-rooms.js

4
.gitignore vendored
View File

@@ -23,5 +23,9 @@ package-lock.json
/js/battle-animations.js
/js/battle-tooltips.js
/js/battle-scene-stub.js
/js/client-core.js
/js/client-frame.js
/js/client-main.js
/js/panel-mainmenu.js
.vscode

View File

@@ -14,6 +14,9 @@
* @license MIT
*/
/**
* String that contains only lowercase alphanumeric characters.
*/
type ID = string & {__isID: true};
const BattleNatures: {[k in NatureName]: {plus?: StatName, minus?: StatName}} = {

View File

@@ -12,6 +12,8 @@
* - The client as a whole is AGPLv3
* - The battle replay/animation engine (battle-*.ts) by itself is MIT
*
* Compiled into battledata.js which includes all dependencies
*
* @author Guangcong Luo <guangcongluo@gmail.com>
* @license MIT
*/

356
src/client-core.ts Normal file
View File

@@ -0,0 +1,356 @@
/**
* Client core
*
* No dependencies.
* Does three unrelated things:
* 1. sets up polyfills where necessary
* 2. sets up PS's model base classes
* 3. sets up the model and view for PS's backgrounds
*
* The background is mostly here so the new background can be loaded ASAP.
*
* @author Guangcong Luo <guancongluo@gmail.com>
* @license AGPLv3
*/
/**********************************************************************
* Polyfills
*********************************************************************/
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function (searchElement, fromIndex) {
for (var i = (fromIndex || 0); i < this.length; i++) {
if (this[i] === searchElement) return i;
}
return -1;
};
}
if (!Array.prototype.includes) {
Array.prototype.includes = function (thing) {
return this.indexOf(thing) !== -1;
};
}
if (!String.prototype.includes) {
String.prototype.includes = function (thing) {
return this.indexOf(thing) !== -1;
};
}
if (!String.prototype.startsWith) {
String.prototype.startsWith = function (thing) {
return this.slice(0, thing.length) === thing;
};
}
if (!String.prototype.endsWith) {
String.prototype.endsWith = function (thing) {
return this.slice(-thing.length) === thing;
};
}
if (!Object.assign) {
Object.assign = function (thing: any, rest: any) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var k in source) {
thing[k] = source[k];
}
}
return thing;
};
}
if (!Object.create) {
Object.create = function (proto: any) {
function F() {}
F.prototype = proto;
return new (F as any)();
};
}
/**********************************************************************
* PS Models
*********************************************************************/
// PS's model classes are defined here
const PSURL = (() => {
let prefix = '';
if (document.location.protocol !== 'http:') prefix = 'https:';
return prefix + '//play.pokemonshowdown.com/';
})();
class PSSubscription {
observable: PSModel | PSStreamModel<any>;
listener: Function;
constructor(observable: PSModel | PSStreamModel<any>, listener: Function) {
this.observable = observable;
this.listener = listener;
}
unsubscribe() {
const index = this.observable.subscriptions.indexOf(this);
if (index >= 0) this.observable.subscriptions.splice(index, 1);
}
}
/**
* PS Models roughly implement the Observable spec. Not the entire
* spec - just the parts we use. PSModel just notifies subscribers of
* updates - a simple model for React.
*/
class PSModel {
subscriptions = [] as PSSubscription[];
subscribe(listener: () => void) {
const subscription = new PSSubscription(this, listener);
this.subscriptions.push(subscription);
return subscription;
}
subscribeAndRun(listener: () => void) {
const subscription = this.subscribe(listener);
subscription.listener();
return subscription;
}
update() {
for (const subscription of this.subscriptions) {
subscription.listener();
}
}
}
/**
* PS Models roughly implement the Observable spec. PSStreamModel
* streams some data out. This is very not-React, which generally
* expects the DOM to be a pure function of state. Instead PSModels
* which hold state, PSStreamModels give state directly to views,
* so that the model doesn't need to hold a redundant copy of state.
*/
class PSStreamModel<T = string> {
subscriptions = [] as PSSubscription[];
updates = [] as T[];
subscribe(listener: (value: T) => void) {
// TypeScript bug
const subscription: PSSubscription = new PSSubscription(this, listener);
this.subscriptions.push(subscription);
if (this.updates.length) {
for (const update of this.updates) {
subscription.listener(update);
}
this.updates = [];
}
return subscription;
}
update(value: T) {
if (!this.subscriptions.length) {
// save updates for later
this.updates.push(value);
}
for (const subscription of this.subscriptions) {
subscription.listener(value);
}
}
}
// type JSONObject = {[k: string]: JSONValue};
// type JSONArray = JSONValue[];
// type JSONValue = number | string | boolean | null | JSONObject | JSONArray;
/**********************************************************************
* Background Model
*********************************************************************/
declare const ColorThief: any;
/**
* PS background model. Separate from PSPrefs because unlike prefs,
* backgrounds can be set separately per server, instead of being
* shared among all servers.
*
* Streams the current URL
*/
const PSBackground = new class extends PSStreamModel<string> {
id = '';
curId = '';
attrib: {url: string, title: string, artist: string} | null = null;
changeCount = 0;
menuColors: string[] | null = null;
constructor() {
super();
try {
let bg = localStorage.getItem('showdown_bg')!.split('\n');
if (bg.length === 1) {
this.set('', bg[0]);
} else if (bg.length === 2) {
this.set(bg[0], bg[1]);
} else if (bg.length >= 7) {
this.set(bg[0], bg[1], bg.slice(2));
}
} catch (e) {}
}
save(bgUrl: string) {
if (this.id !== 'custom') {
localStorage.setItem('showdown_bg', this.id);
} else if (this.menuColors) {
localStorage.setItem('showdown_bg', bgUrl + '\n' + this.id + '\n' + this.menuColors.join('\n'));
}
}
set(bgUrl: string, bgid: string, menuColors: string[] | null = null) {
// id
this.id = bgid;
// curid
if (!bgid) {
if (location.host === 'smogtours.psim.us') {
bgid = 'shaymin';
} else if (location.host === 'play.pokemonshowdown.com') {
const bgs = ['horizon', 'ocean', 'waterfall', 'shaymin', 'charizards'];
bgid = bgs[Math.floor(Math.random() * 5)];
if (bgid === this.curId) bgid = bgs[Math.floor(Math.random() * 5)];
}
}
this.curId = bgid;
if (!bgUrl) {
bgUrl = (bgid === 'solidblue' ? '#344b6c' : PSURL + 'fx/client-bg-' + bgid + '.jpg');
}
// April Fool's 2016 - Digimon theme
// bgid = 'digimon';
// bgUrl = PSURL + 'sprites/afd/digimonbg.jpg';
this.changeCount++;
// menuColors, attrib
let attrib = null;
switch (bgid) {
case 'horizon':
menuColors = ["318.87640449438203,35.177865612648226%", "216,46.2962962962963%", "221.25,32.25806451612904%", "197.8021978021978,52.60115606936417%", "232.00000000000003,19.480519480519483%", "228.38709677419354,60.7843137254902%"];
attrib = {url: 'https://vtas.deviantart.com/art/Pokemon-Horizon-312267168', title: 'Horizon', artist: 'Vivian Zou'};
break;
case 'ocean':
menuColors = ["82.8169014084507,34.63414634146342%", "216.16438356164383,29.55465587044534%", "212.92682926829266,59.42028985507245%", "209.18918918918916,57.51295336787566%", "199.2857142857143,48.275862068965495%", "213.11999999999998,55.06607929515419%"];
attrib = {url: 'https://quanyails.deviantart.com/art/Sunrise-Ocean-402667154', title: 'Sunrise Ocean', artist: 'Yijing Chen'};
break;
case 'waterfall':
menuColors = ["119.31034482758622,37.66233766233767%", "184.36363636363635,23.012552301255226%", "108.92307692307692,37.14285714285714%", "70.34482758620689,20.567375886524818%", "98.39999999999998,36.76470588235296%", "140,38.18181818181818%"];
attrib = {url: 'https://yilx.deviantart.com/art/Irie-372292729', title: 'Irie', artist: 'Samuel Teo'};
break;
case 'shaymin':
menuColors = ["39.000000000000064,21.7391304347826%", "170.00000000000003,2.380952380952378%", "157.5,11.88118811881188%", "174.78260869565216,12.041884816753928%", "185.00000000000003,12.76595744680851%", "20,5.660377358490567%"];
attrib = {url: 'http://cargocollective.com/bluep', title: 'Shaymin', artist: 'Daniel Kong'};
break;
case 'charizards':
menuColors = ["37.159090909090914,74.57627118644066%", "10.874999999999998,70.79646017699115%", "179.51612903225808,52.10084033613446%", "20.833333333333336,36.73469387755102%", "192.3076923076923,80.41237113402063%", "210,29.629629629629633%"];
attrib = {url: 'https://seiryuuden.deviantart.com/art/The-Ultimate-Mega-Showdown-Charizards-414587079', title: 'Charizards', artist: 'Jessica Valencia'};
break;
case 'digimon':
menuColors = ["170.45454545454544,27.500000000000004%", "84.70588235294119,13.821138211382115%", "112.50000000000001,7.8431372549019605%", "217.82608695652175,54.761904761904766%", "0,1.6949152542372816%", ""];
}
if (!menuColors && bgUrl.charAt(0) === '#') {
const r = parseInt(bgUrl.slice(1, 3), 16) / 255;
const g = parseInt(bgUrl.slice(3, 5), 16) / 255;
const b = parseInt(bgUrl.slice(5, 7), 16) / 255;
const hs = this.getHueSat(r, g, b);
menuColors = [hs, hs, hs, hs, hs, hs];
}
this.attrib = attrib;
this.menuColors = menuColors;
if (!menuColors) {
this.extractMenuColors(bgUrl);
} else {
this.save(bgUrl);
}
}
extractMenuColors(bgUrl: string) {
const changeCount = this.changeCount;
// We need the image object to load it on a canvas to detect the main color.
const img = new Image();
img.onload = () => {
if (changeCount === PSBackground.changeCount) return;
// in case ColorThief throws from canvas,
// or localStorage throws
try {
const colorThief = new ColorThief();
const colors = colorThief.getPalette(img, 5);
let menuColors = [];
if (!colors) {
menuColors = ['0, 0%', '0, 0%', '0, 0%', '0, 0%', '0, 0%'];
} else {
for (let i = 0; i < 5; i++) {
const color = colors[i];
const hs = PSBackground.getHueSat(color[0] / 255, color[1] / 255, color[2] / 255);
menuColors.unshift(hs);
}
}
this.menuColors = menuColors;
PSBackground.save(bgUrl);
} catch (e) {}
};
img.src = bgUrl;
}
getHueSat(r: number, g: number, b: number) {
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
if (max === min) {
return `0,0%`;
}
const l = (max + min) / 2;
const d = max - min;
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
let h = 0;
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
return `${h * 360},${s * 100}%`;
}
};
/**********************************************************************
* Core Views
*********************************************************************/
PSBackground.subscribe(bgUrl => {
if (!PSBackground.curId) {
document.body.style.background = '';
document.body.style.backgroundSize = '';
const buttonStyleElem = document.getElementById('mainmenubuttoncolors');
if (buttonStyleElem) buttonStyleElem.textContent = ``;
return;
}
if (bgUrl !== null) {
let background;
if (bgUrl.charAt(0) === '#') {
background = bgUrl;
} else if (PSBackground.curId !== 'custom') {
background = `#546bac url(${bgUrl}) no-repeat left center fixed`;
} else {
background = `#546bac url(${bgUrl}) no-repeat center center fixed`;
}
document.body.style.background = background;
document.body.style.backgroundSize = 'cover';
}
// main menu button colors
let cssBuf = ``;
let n = 0;
if (PSBackground.menuColors) for (const hs of PSBackground.menuColors) {
n++;
cssBuf += `body .button.mainmenu${n} { background: linear-gradient(to bottom, hsl(${hs},72%), hsl(${hs},52%)); border-color: hsl(${hs},40%); }\n`;
cssBuf += `body .button.mainmenu${n}:hover { background: linear-gradient(to bottom, hsl(${hs},62%), hsl(${hs},42%)); border-color: hsl(${hs},21%); }\n`;
cssBuf += `body .button.mainmenu${n}:active { background: linear-gradient(to bottom, hsl(${hs},42%), hsl(${hs},58%)); border-color: hsl(${hs},21%); }\n`;
}
let buttonStyleElem = document.getElementById('mainmenubuttoncolors');
if (!buttonStyleElem) {
if (cssBuf) {
buttonStyleElem = new HTMLStyleElement();
buttonStyleElem.id = 'mainmenubuttoncolors';
buttonStyleElem.textContent = cssBuf;
document.head.appendChild(buttonStyleElem);
}
} else {
buttonStyleElem.textContent = cssBuf;
}
});
// '<a href="https://vtas.deviantart.com/art/Pokemon-Horizon-312267168" target="_blank" class="subtle">"Horizon" <small>background by Vivian Zou</small></a>';
// if (attrib) attrib = '<small style="display:block;padding-bottom:4px">' + attrib + '</small>';

256
src/client-frame.tsx Normal file
View File

@@ -0,0 +1,256 @@
class PSHeader extends preact.Component<{style: {}}> {
renderRoomTab(id: RoomID) {
const room = PS.rooms[id];
const closable = (id === '' || id === 'rooms' ? '' : ' closable');
const cur = (room === PS.leftRoom || room === PS.rightRoom ? ' cur' : '');
let className = `roomtab button${room.notifying}${closable}${cur}`;
let icon = null;
let title = room.title;
let closeButton = null;
switch (room.type) {
case '':
case 'mainmenu':
icon = <i class="fa fa-home"></i>;
break;
case 'teambuilder':
icon = <i class="fa fa-pencil-square-o"></i>;
break;
case 'ladder':
icon = <i class="fa fa-list-ol"></i>;
break;
case 'battles':
icon = <i class="fa fa-caret-square-o-right"></i>;
break;
case 'rooms':
icon = <i class="fa fa-plus" style="margin:7px auto -6px auto"></i>;
title = '';
break;
case 'battle':
let idChunks = id.substr(7).split('-');
let formatid;
// TODO: relocate to room implementation
if (idChunks.length <= 1) {
if (idChunks[0] === 'uploadedreplay') formatid = 'Uploaded Replay';
} else {
formatid = idChunks[idChunks.length - 2];
}
if (!title) {
let battle = (room as any).battle;
let p1 = (battle && battle.p1 && battle.p1.name) || '';
let p2 = (battle && battle.p2 && battle.p2.name) || '';
if (p1 && p2) {
title = '' + p1 + ' v. ' + p2;
} else if (p1 || p2) {
title = '' + p1 + p2;
} else {
title = '(empty room)';
}
}
icon = <i class="text">{formatid}</i>;
break;
case 'chat':
icon = <i class="fa fa-comment-o"></i>;
break;
case 'html':
default:
if (title.charAt(0) === '[') {
let closeBracketIndex = title.indexOf(']');
if (closeBracketIndex > 0) {
icon = <i class="text">{title.slice(1, closeBracketIndex)}</i>;
title = title.slice(closeBracketIndex + 1);
break;
}
}
icon = <i class="fa fa-file-text-o"></i>;
break;
}
if (closable) {
closeButton = <button class="closebutton" name="closeRoom" value={id} aria-label="Close"><i class="fa fa-times-circle"></i></button>;
}
return <li><a class={className} href={`/${id}`} draggable={true}>{icon} <span>{title}</span></a>{closeButton}</li>;
}
render() {
return <div id="header" class="header" style={this.props.style}>
<img class="logo" src="https://play.pokemonshowdown.com/pokemonshowdownbeta.png" srcset="https://play.pokemonshowdown.com/pokemonshowdownbeta@2x.png 2x" alt="Pokémon Showdown! (beta)" width="146" height="44" />
<div class="maintabbarbottom"></div>
<div class="tabbar maintabbar"><div class="inner">
<ul>
{PS.leftRoomList.map(roomid => this.renderRoomTab(roomid))}
</ul>
<ul class="siderooms" style={{float: 'none', marginLeft: PS.leftRoomWidth}}>
{PS.rightRoomList.map(roomid => this.renderRoomTab(roomid))}
</ul>
</div></div>
<div class="userbar">
<span class="username" data-name=" Guest" style="color:hsl(96,67%,36%);"><i class="fa fa-user" style="color:#779EC5"></i> Guest</span>
<button class="icon button" name="openSounds" title="Sound" aria-label="Sound"><i class="fa fa-volume-up"></i></button>
<button class="icon button" name="openOptions" title="Options" aria-label="Options"><i class="fa fa-cog"></i></button>
</div>
</div>;
}
}
class PSRoomPanel extends preact.Component<{style: {}, roomid: RoomID}> {
render() {
return <div class="ps-room" id={`room-${this.props.roomid}`} style={this.props.style}>
<p>Loading...</p>
</div>;
}
}
class PSMain extends preact.Component {
constructor() {
super();
this.updateLayout();
}
posStyle(pos: PanelPosition) {
if (!pos) return {display: 'none'};
let top: number | null = (pos.top || 0);
let height: number | null = null;
let bottom: number | null = (pos.bottom || 0);
if (bottom > 0 || top < 0) {
height = bottom - top;
if (height < 0) throw new RangeError("Invalid pos range");
if (top < 0) top = null;
else bottom = null;
}
let left: number | null = (pos.left || 0);
let width: number | null = null;
let right: number | null = (pos.right || 0);
if (right > 0 || left < 0) {
width = right - left;
if (width < 0) throw new RangeError("Invalid pos range");
if (left < 0) left = null;
else right = null;
}
return {
display: 'block',
top: top === null ? `auto` : `${top}px`,
height: height === null ? `auto` : `${height}px`,
bottom: bottom === null ? `auto` : `${-bottom}px`,
left: left === null ? `auto` : `${left}px`,
width: width === null ? `auto` : `${width}px`,
right: right === null ? `auto` : `${-right}px`,
};
}
/**
* "minWidth" and "maxWidth" are a bit deceptive here - to be clear,
* all PS rooms are expected to responsively support any width from
* 320px up, when in single panel mode. These metrics are used purely
* to calculate the location of the separator in two-panel mode.
*
* - `minWidth` - minimum width as a right-panel
* - `width` - preferred width, minimum width as a left-panel
* - `maxWidth` - maximum width as a left-panel
*
* PS will only show two panels if it can fit `width` in the left, and
* `minWidth` in the right. Extra space will be given to to right panel
* until it reaches `width`, then evenly distributed until both panels
* reach `maxWidth`, and extra space above that will be given to the
* right panel.
*/
getWidthFor(room: PSRoom) {
switch (room.type) {
case 'mainmenu':
return {
minWidth: 340,
width: 628,
maxWidth: 628,
isMainMenu: true,
};
case 'chat':
case 'rooms':
return {
minWidth: 320,
width: 640,
maxWidth: 640,
};
case 'battle':
return {
minWidth: 320,
width: 956,
maxWidth: 1180,
};
}
return {
minWidth: 640,
width: 640,
maxWidth: 640,
};
}
updateLayout() {
const leftRoomWidth = this.calculateLeftRoomWidth();
if (PS.leftRoomWidth !== leftRoomWidth) {
PS.leftRoomWidth = leftRoomWidth;
PS.update();
}
}
calculateLeftRoomWidth() {
// If we don't have both a left room and a right room, obviously
// just show one room
if (!PS.leftRoom || !PS.rightRoom || PS.onePanelMode) {
return 0;
}
// The rest of this code can assume we have both a left room and a
// right room, and also want to show both if they fit
const left = this.getWidthFor(PS.leftRoom);
const right = this.getWidthFor(PS.rightRoom);
const available = window.offsetWidth;
let excess = available - left.width + right.width;
if (excess >= 0) {
// both fit in full size
const leftStretch = left.maxWidth - left.width;
if (!leftStretch) return left.width;
const rightStretch = right.maxWidth - right.width;
if (leftStretch + rightStretch >= excess) return left.maxWidth;
// evenly distribute the excess
return left.width + Math.floor(excess * leftStretch / (leftStretch + rightStretch));
}
if (left.isMainMenu) {
if (available >= left.minWidth + right.width) {
return left.minWidth;
}
return 0;
}
if (available >= left.width + right.minWidth) {
return left.width;
}
return 0;
}
renderRoom(room: PSRoom) {
let pos = null;
if (PS.leftRoomWidth === 0) {
if ((PS.rightRoomFocused && room === PS.leftRoom) ||
(!PS.rightRoomFocused && room === PS.rightRoom)) {
pos = {top: 50};
}
} else {
if (room === PS.leftRoom) pos = {top: 50, right: PS.leftRoomWidth};
if (room === PS.rightRoom) pos = {top: 50, left: PS.leftRoomWidth};
}
return <PSRoomPanel style={this.posStyle(pos)} roomid={room.id} />;
}
render() {
let rooms = [] as preact.VNode[];
for (const k in PS.rooms) {
rooms.push(this.renderRoom(PS.rooms[k]));
}
return <div class="ps-frame">
<PSHeader style={this.posStyle({bottom: 50})} />
{rooms}
</div>;
}
}
type PanelPosition = {top?: number, bottom?: number, left?: number, right?: number} | null;
preact.render(<PSMain />, document.body, document.getElementById('ps-frame')!);

182
src/client-main.ts Normal file
View File

@@ -0,0 +1,182 @@
/**
* Client main
*
* Dependencies: client-core
*
* Sets up the main client models: Prefs, Teams, User, and PS.
*
* @author Guangcong Luo <guancongluo@gmail.com>
* @license AGPLv3
*/
/**
* String that contains only lowercase alphanumeric characters.
*/
type RoomID = string & {__isRoomID: true};
const PSPrefsDefaults = {} as {[key: string]: any};
/**
* Tracks user preferences, stored in localStorage. Contains most local
* data, with the exception of backgrounds, teams, and session data,
* which get their own models.
*/
const PSPrefs = new class extends PSModel {
/**
* Dark mode!
*/
dark = false;
/**
* Disables animated GIFs, but keeps other animations enabled.
* Workaround for a Chrome 64 bug with GIFs.
* true - Disable GIFs, will be automatically re-enabled if you
* switch away from Chrome 64.
* false - Enable GIFs all the time.
* null - Enable GIFs only on Chrome 64.
*/
nogif: boolean | null = null;
/**
* Show "User joined" and "User left" messages. serverid:roomid
* table. Uses 1 and 0 instead of true/false for JSON packing
* reasons.
*/
showjoins: {[serverid: string]: {[roomid: string]: 1 | 0}} | null = null;
/**
* true = one panel, false = two panels, left and right
*/
onepanel = false;
storage = {} as {[k: string]: any};
readonly origin = 'https://play.pokemonshowdown.com';
constructor() {
super();
for (const key in this) {
const value = (this as any)[key];
if (key === 'storage' || key === 'subscriptions' || key === 'origin') continue;
if (typeof value === 'function') continue;
PSPrefsDefaults[key] = value;
}
// set up local loading
try {
if (window.localStorage) {
this.save = () => {
localStorage.setItem('showdown_prefs', JSON.stringify(PSPrefs.storage));
};
this.load(JSON.parse(localStorage.getItem('showdown_prefs')!) || {}, true);
}
} catch (e) {}
}
/**
* Change a preference.
*/
set(key: string, value: any) {
if (value === null) {
delete this.storage[key];
(this as any)[key] = PSPrefsDefaults[key];
} else {
this.storage[key] = value;
(this as any)[key] = value;
}
this.update();
this.save();
}
load(newPrefs: object, noSave?: boolean) {
this.fixPrefs(newPrefs);
Object.assign(this, PSPrefsDefaults);
this.storage = newPrefs;
this.update();
if (!noSave) this.save();
}
save() {
// noop by default
}
fixPrefs(newPrefs: any) {
const oldShowjoins = newPrefs['showjoins'];
if (oldShowjoins !== undefined && typeof oldShowjoins !== 'object') {
const showjoins: {[serverid: string]: {[roomid: string]: 1 | 0}} = {};
const serverShowjoins: {[roomid: string]: 1 | 0} = {global: (oldShowjoins ? 1 : 0)};
const showroomjoins = newPrefs['showroomjoins'] as {[roomid: string]: boolean};
for (const roomid in showroomjoins) {
serverShowjoins[roomid] = (showroomjoins[roomid] ? 1 : 0);
}
delete newPrefs['showroomjoins'];
showjoins[Config.server.id] = serverShowjoins;
newPrefs['showjoins'] = showjoins;
}
const isChrome64 = navigator.userAgent.includes(' Chrome/64.');
if (newPrefs['nogif'] !== undefined) {
if (!isChrome64) {
delete newPrefs['nogif'];
}
} else if (isChrome64) {
newPrefs['nogif'] = true;
alert('Your version of Chrome has a bug that makes animated GIFs freeze games sometimes, so certain animations have been disabled. Only some people have the problem, so you can experiment and enable them in the Options menu setting "Disable GIFs for Chrome 64 bug".');
}
}
}
interface Team {
name: string;
format: ID;
/** In packed format */
team: string;
folder: string;
iconCache: string;
}
const PSTeams = new class extends PSModel {
list = [] as Team[];
save() {
// noop by default
}
}
class PSRoom {
id: RoomID;
title: string;
type = '';
notifying: '' | ' notifying' | ' subtle-notifying' = '';
closable = true;
constructor(roomid: RoomID, title: string) {
this.id = roomid;
this.title = title;
}
}
const PS = new class extends PSModel {
rooms = {} as {[roomid: string]: PSRoom};
leftRoomList = [] as RoomID[];
rightRoomList = [] as RoomID[];
leftRoom: PSRoom | null = null;
rightRoom: PSRoom | null = null;
rightRoomFocused = false;
/**
* Not to be confused with PSPrefs.onepanel, which is permanent.
* PS.onePanelMode will be true if one-panel mode is on, but it will
* also be true if the right panel is temporarily hidden (by opening
* the Rooms panel and clicking "Hide")
*
* Will NOT be true if only one panel fits onto the screen at the
* moment, but resizing will display multiple panels  for that,
* check PS.leftRoomWidth === 0
*/
onePanelMode = false;
/** 0 = no left room */
leftRoomWidth = 0;
constructor() {
super();
const mainmenu = new PSRoom('' as RoomID, "Home");
mainmenu.type = 'mainmenu';
this.rooms[''] = this.leftRoom = mainmenu;
this.leftRoomList.push('' as RoomID);
const rooms = new PSRoom('rooms' as RoomID, "Rooms");
rooms.type = 'rooms';
this.rooms['rooms'] = this.rightRoom = rooms;
this.rightRoomList.push('rooms' as RoomID);
}
};

0
src/panel-mainmenu.tsx Normal file
View File

58
testclient-beta.html Normal file
View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<title>Showdown!</title>
<link rel="shortcut icon" href="favicon.ico" id="dynamic-favicon" />
<link rel="stylesheet" href="style/client.css" />
<meta id="viewport" name="viewport" content="width=640" />
<meta name="robots" content="noindex" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<!--[if lte IE 8]><script>
window.Config = {"oldie": true};
</script><![endif]-->
</head><body>
<div id="ps-frame">
<div id="header" class="header">
<img class="logo" src="pokemonshowdownbeta.png" alt="Pok&eacute;mon Showdown! (beta)" width="146" height="44" /><div class="maintabbarbottom"></div>
</div>
<div class="ps-room scrollable" id="mainmenu"><div class="mainmenuwrapper">
<div class="leftmenu">
<div class="activitymenu">
<div class="pmbox">
<div class="pm-window news-embed">
<h3><button class="closebutton" tabindex="-1" aria-label="Close"><i class="fa fa-times-circle"></i></button><button class="minimizebutton" tabindex="-1" aria-label="Minimize"><i class="fa fa-minus-circle"></i></button>Latest News</h3>
<div class="pm-log" style="max-height:none">
<div class="newsentry"><h4>Test client</h4><p>Welcome to the test client! You can test client changes here!</p><p>&mdash;<strong>Zarel</strong> <small class="date">on Sep 25, 2015</small></p></div>
</div>
</div>
</div>
</div>
<div class="mainmenu">
<div id="loading-message" class="mainmessage">Loading... <noscript>FAILED<br /><br />Pok&eacute;mon Showdown requires JavaScript.</noscript></div>
</div>
</div>
<div class="rightmenu">
</div>
<div class="mainmenufooter">
<small><a href="//pokemonshowdown.com/" target="_blank"><strong>Pok&eacute;mon Showdown</strong></a> | <a href="http://smogon.com/" target="_blank"><strong>Smogon</strong></a><br><a href="//pokemonshowdown.com/dex/" target="_blank">Pokédex</a> | <a href="//pokemonshowdown.com/replay/" target="_blank">Replays</a> | <a href="//pokemonshowdown.com/rules" target="_blank">Rules</a></small> | <small><a href="//pokemonshowdown.com/forums/" target="_blank">Forum</a></small>
</div>
</div></div>
</div>
<script>
function linkStyle(url) {
var linkEl = document.createElement('link');
linkEl.rel = 'stylesheet';
linkEl.href = url;
document.head.appendChild(linkEl);
}
linkStyle("style/sim-types.css");
linkStyle("style/battle.css");
linkStyle("style/utilichart.css");
linkStyle("style/font-awesome.css");
</script>
<script src="js/client-core.js"></script>
<script src="js/client-main.js"></script>
<script src="js/lib/preact.min.js"></script>
<script src="js/client-frame.js"></script>
</body></html>