pokemon-showdown-client/play.pokemonshowdown.com/src/panel-popups.tsx
Guangcong Luo fc6d963912
Some checks failed
Node.js CI / build (22.x) (push) Has been cancelled
Strongly type Config
(Also remove a bunch of global types)
2025-05-19 13:40:12 -07:00

1778 lines
59 KiB
TypeScript

import preact from "../js/lib/preact";
import { toID, toRoomid, toUserid, Dex } from "./battle-dex";
import type { ID } from "./battle-dex-data";
import { BattleLog } from "./battle-log";
import { PSLoginServer } from "./client-connection";
import { PSBackground } from "./client-core";
import {
PS, PSRoom, Config, type RoomOptions, type PSLoginState, type RoomID, type TimestampOptions,
} from "./client-main";
import { type BattleRoom } from "./panel-battle";
import { ChatUserList, type ChatRoom } from "./panel-chat";
import { PSRoomPanel, PSPanelWrapper, PSView } from "./panels";
import { PSHeader } from "./panel-topbar";
/**
* User popup
*/
export class UserRoom extends PSRoom {
override readonly classType = 'user';
userid!: ID;
name!: string;
isSelf!: boolean;
constructor(options: RoomOptions) {
super(options);
const userid = (this.id.split('-')[1] || '') as ID;
this.setName(options.args?.username as string || userid);
}
setName(name: string) {
this.name = name;
this.userid = toID(name);
this.isSelf = (this.userid === PS.user.userid);
if (/[a-zA-Z0-9]/.test(this.name.charAt(0))) this.name = ' ' + this.name;
this.update(null);
if (this.userid) PS.send(`|/cmd userdetails ${this.userid}`);
}
}
class UserPanel extends PSRoomPanel<UserRoom> {
static readonly id = 'user';
static readonly routes = ['user-*', 'viewuser-*', 'users'];
static readonly Model = UserRoom;
static readonly location = 'popup';
renderUser() {
const room = this.props.room;
if (!room.userid) return null;
const user = PS.mainmenu.userdetailsCache[room.userid] || {
userid: room.userid, name: room.name.slice(1), avatar: '[loading]',
};
if (!user.avatar) {
// offline; server doesn't know the actual username
user.name = room.name;
}
const hideInteraction = room.id.startsWith('viewuser-');
const group = PS.server.getGroup(room.name);
let groupName: preact.ComponentChild = group.name || null;
if (group.type === 'punishment') {
groupName = <span style="color:#777777">{groupName}</span>;
}
const globalGroup = PS.server.getGroup(user.group);
let globalGroupName: preact.ComponentChild = globalGroup.name && `Global ${globalGroup.name}` || null;
if (globalGroup.type === 'punishment') {
globalGroupName = <span style="color:#777777">{globalGroupName}</span>;
}
if (globalGroup.name === group.name) groupName = null;
let roomsList: preact.ComponentChild = null;
if (user.rooms) {
let battlebuf = [];
let chatbuf = [];
let privatebuf = [];
for (let roomid in user.rooms) {
if (roomid === 'global') continue;
const curRoom = user.rooms[roomid];
let roomrank: preact.ComponentChild = null;
if (!/[A-Za-z0-9]/.test(roomid.charAt(0))) {
roomrank = <small style="color: #888; font-size: 100%">{roomid.charAt(0)}</small>;
}
roomid = toRoomid(roomid);
if (roomid.substr(0, 7) === 'battle-') {
const p1 = curRoom.p1!.substr(1);
const p2 = curRoom.p2!.substr(1);
const ownBattle = (PS.user.userid === toUserid(p1) || PS.user.userid === toUserid(p2));
const roomLink = <a
href={`/${roomid}`} class={'ilink' + (ownBattle || roomid in PS.rooms ? ' yours' : '')}
title={`${p1 || '?'} v. ${p2 || '?'}`}
>{roomrank}{roomid.substr(7)}</a>;
if (curRoom.isPrivate) {
if (privatebuf.length) privatebuf.push(', ');
privatebuf.push(roomLink);
} else {
if (battlebuf.length) battlebuf.push(', ');
battlebuf.push(roomLink);
}
} else {
const roomLink = <a href={`/${roomid}`} class={'ilink' + (roomid in PS.rooms ? ' yours' : '')}>
{roomrank}{roomid}
</a>;
if (curRoom.isPrivate) {
if (privatebuf.length) privatebuf.push(", ");
privatebuf.push(roomLink);
} else {
if (chatbuf.length) chatbuf.push(', ');
chatbuf.push(roomLink);
}
}
}
if (battlebuf.length) battlebuf.unshift(<br />, <em>Battles:</em>, " ");
if (chatbuf.length) chatbuf.unshift(<br />, <em>Chatrooms:</em>, " ");
if (privatebuf.length) privatebuf.unshift(<br />, <em>Private rooms:</em>, " ");
if (battlebuf.length || chatbuf.length || privatebuf.length) {
roomsList = <small class="rooms">{battlebuf}{chatbuf}{privatebuf}</small>;
}
} else if (user.rooms === false) {
roomsList = <strong class="offline">OFFLINE</strong>;
}
const isSelf = user.userid === PS.user.userid;
let away = false;
let status = null;
if (user.status) {
away = user.status.startsWith('!');
status = away ? user.status.slice(1) : user.status;
}
const buttonbar = [];
if (!hideInteraction) {
buttonbar.push(isSelf ? (
<p class="buttonbar">
<button class="button" disabled>Challenge</button> {}
<button class="button" data-href="dm-">Chat Self</button>
</p>
) : !PS.user.named ? (
<p class="buttonbar">
<button class="button" disabled>Challenge</button> {}
<button class="button" disabled>Chat</button> {}
<button class="button" disabled>{'\u2026'}</button>
</p>
) : (
<p class="buttonbar">
<button class="button" data-href={`challenge-${user.userid}`}>Challenge</button> {}
<button class="button" data-href={`dm-${user.userid}`}>Chat</button> {}
<button class="button" data-href={`useroptions-${user.userid}-${room.parentRoomid || ''}`}>{'\u2026'}</button>
</p>
));
if (isSelf) {
buttonbar.push(
<hr />,
<p class="buttonbar" style="text-align: right">
<button class="button" data-href="login"><i class="fa fa-pencil" aria-hidden></i> Change name</button> {}
<button class="button" data-cmd="/logout"><i class="fa fa-power-off" aria-hidden></i> Log out</button>
</p>
);
}
}
const avatar = user.avatar !== '[loading]' ? Dex.resolveAvatar(`${user.avatar || 'unknown'}`) : null;
return [<div class="userdetails">
{avatar && (room.isSelf ? (
<img src={avatar} class="trainersprite yours" data-href="avatars" />
) : (
<img src={avatar} class="trainersprite" />
))}
<strong><a
href={`//${Config.routes.users}/${user.userid}`} target="_blank"
style={`color: ${away ? '#888888' : BattleLog.usernameColor(user.userid)}`}
>
{user.name}
</a></strong><br />
{status && <div class="userstatus">{status}</div>}
{groupName && <div class="usergroup roomgroup">{groupName}</div>}
{globalGroupName && <div class="usergroup globalgroup">{globalGroupName}</div>}
{user.customgroup && <div class="usergroup globalgroup">{user.customgroup}</div>}
{!hideInteraction && roomsList}
</div>, buttonbar];
}
lookup = (ev: Event) => {
ev.preventDefault();
ev.stopImmediatePropagation();
const room = this.props.room;
const username = this.base!.querySelector<HTMLInputElement>('input[name=username]')?.value;
room.setName(username || '');
};
maybeReset = (ev: Event) => {
const room = this.props.room;
const username = this.base!.querySelector<HTMLInputElement>('input[name=username]')?.value;
if (toID(username) !== room.userid) {
room.setName('');
}
};
override render() {
const room = this.props.room;
const showLookup = room.id === 'users';
return <PSPanelWrapper room={room}><div class="pad">
{showLookup && <form onSubmit={this.lookup} style={{ minWidth: '278px' }}>
<label class="label">
Username: {}
<input type="search" name="username" class="textbox autofocus" onInput={this.maybeReset} onChange={this.maybeReset} />
</label>
{!room.userid && <p class="buttonbar">
<button type="submit" class="button"><strong>Look up</strong></button> {}
<button name="closeRoom" class="button">Close</button>
</p>}
{!!room.userid && <hr />}
</form>}
{this.renderUser()}
</div></PSPanelWrapper>;
}
}
class UserOptionsPanel extends PSRoomPanel {
static readonly id = 'useroptions';
static readonly routes = ['useroptions-*'];
static readonly location = 'popup';
static readonly noURL = true;
declare state: {
showMuteInput?: boolean,
showBanInput?: boolean,
showLockInput?: boolean,
showConfirm?: boolean,
requestSent?: boolean,
data?: Record<string, string>,
};
getTargets() {
const [, targetUser, targetRoomid] = this.props.room.id.split('-');
let targetRoom = (PS.rooms[targetRoomid] || null) as ChatRoom | null;
if (targetRoom?.type !== 'chat') targetRoom = targetRoom?.getParent() as ChatRoom;
if (targetRoom?.type !== 'chat') targetRoom = targetRoom?.getParent() as ChatRoom;
if (targetRoom?.type !== 'chat') targetRoom = null;
return { targetUser: targetUser as ID, targetRoomid: targetRoomid as RoomID, targetRoom };
}
handleMute = (ev: Event) => {
this.setState({ showMuteInput: true, showBanInput: false, showLockInput: false });
ev.preventDefault();
ev.stopImmediatePropagation();
};
handleBan = (ev: Event) => {
this.setState({ showBanInput: true, showMuteInput: false, showLockInput: false });
ev.preventDefault();
ev.stopImmediatePropagation();
};
handleLock = (ev: Event) => {
this.setState({ showLockInput: true, showMuteInput: false, showBanInput: false });
ev.preventDefault();
ev.stopImmediatePropagation();
};
handleCancel = (ev: Event) => {
this.setState({ showBanInput: false, showMuteInput: false, showLockInput: false, showConfirm: false });
ev.preventDefault();
ev.stopImmediatePropagation();
};
handleConfirm = (ev: Event) => {
const data = this.state.data;
if (!data) return;
const { targetUser, targetRoom } = this.getTargets();
let cmd = '';
if (data.action === "Mute") {
cmd += data.duration === "1 hour" ? "/hourmute " : "/mute ";
} else if (data.action === "Ban") {
cmd += data.duration === "1 week" ? "/weekban " : "/ban ";
} else if (data.action === "Lock") {
cmd += data.duration === "1 week" ? "/weeklock " : "/lock ";
} else if (data.action === "Namelock") {
cmd += "/namelock ";
} else {
return;
}
cmd += `${targetUser} ${data.reason ? ',' + data.reason : ''}`;
targetRoom?.send(cmd);
this.close();
};
handleAddFriend = (ev: Event) => {
const { targetUser, targetRoom } = this.getTargets();
targetRoom?.send(`/friend add ${targetUser}`);
this.setState({ requestSent: true });
ev.preventDefault();
ev.stopImmediatePropagation();
};
handleIgnore = () => {
const { targetUser, targetRoom } = this.getTargets();
targetRoom?.send(`/ignore ${targetUser}`);
this.close();
};
handleUnignore = () => {
const { targetUser, targetRoom } = this.getTargets();
targetRoom?.send(`/unignore ${targetUser}`);
this.close();
};
muteUser = (ev: Event) => {
this.setState({ showMuteInput: false });
const hrMute = (ev.currentTarget as HTMLButtonElement).value === "1hr";
const reason = this.base?.querySelector<HTMLInputElement>("input[name=mutereason]")?.value;
const data = {
action: 'Mute',
reason,
duration: hrMute ? "1 hour" : "7 minutes",
};
this.setState({ data, showConfirm: true });
ev.preventDefault();
ev.stopImmediatePropagation();
};
banUser = (ev: Event) => {
this.setState({ showBanInput: false });
const weekBan = (ev.currentTarget as HTMLButtonElement).value === "1wk";
const reason = this.base?.querySelector<HTMLInputElement>("input[name=banreason]")?.value;
const data = {
action: 'Ban',
reason,
duration: weekBan ? "1 week" : "2 days",
};
this.setState({ data, showConfirm: true });
ev.preventDefault();
ev.stopImmediatePropagation();
};
lockUser = (ev: Event) => {
this.setState({ showLockInput: false });
const weekLock = (ev.currentTarget as HTMLButtonElement).value === "1wk";
const isNamelock = (ev.currentTarget as HTMLButtonElement).value === "nmlk";
const reason = this.base?.querySelector<HTMLInputElement>("input[name=lockreason]")?.value;
const data = {
action: isNamelock ? 'Namelock' : 'Lock',
reason,
duration: weekLock ? "1 week" : "2 days",
};
this.setState({ data, showConfirm: true });
ev.preventDefault();
ev.stopImmediatePropagation();
};
isIgnoringUser = (userid: string) => {
const ignoring = PS.prefs.ignore || {};
if (ignoring[userid] === 1) return true;
return false;
};
override render() {
const room = this.props.room;
const banPerms = ["@", "#", "~"];
const mutePerms = ["%", ...banPerms];
const { targetUser, targetRoom } = this.getTargets();
const userRoomGroup = targetRoom?.users[PS.user.userid].charAt(0) || '';
const canMute = mutePerms.includes(userRoomGroup);
const canBan = banPerms.includes(userRoomGroup);
const canLock = mutePerms.includes(PS.user.group);
const isVisible = (actionName: string) => {
if (actionName === 'mute') {
return canMute && !this.state.showLockInput && !this.state.showBanInput && !this.state.showConfirm;
}
if (actionName === 'ban') {
return canBan && !this.state.showLockInput && !this.state.showMuteInput && !this.state.showConfirm;
}
if (actionName === 'lock') {
return canLock && !this.state.showBanInput && !this.state.showMuteInput && !this.state.showConfirm;
}
};
return <PSPanelWrapper room={room} width={280}><div class="pad">
<p>
{this.isIgnoringUser(targetUser) ? (
<button onClick={this.handleUnignore} class="button">
Unignore
</button>
) : (
<button onClick={this.handleIgnore} class="button">
Ignore
</button>
)}
</p>
<p>
<button data-href={`view-help-request-report-user-${targetUser}`} class="button">
Report
</button>
</p>
<p>
{this.state.requestSent ? (
<button class="button disabled">
Sent request
</button>
) : (
<button onClick={this.handleAddFriend} class="button">
Add friend
</button>
)}
</p>
{(canMute || canBan || canLock) && <hr />}
{this.state.showConfirm && <p>
<small>
{this.state.data?.action} <b>{targetUser}</b> {}
{!this.state.data?.action.endsWith('ock') ? <>from <b>{targetRoom?.title}</b></> : ''} for {this.state.data?.duration}?
</small>
<p class="buttonbar">
<button class="button" onClick={this.handleConfirm}>
<i class="fa fa-confirm" aria-hidden></i> Confirm
</button> {}
<button class="button" onClick={this.handleCancel}>
Cancel
</button>
</p>
</p>}
<p class="buttonbar">
{isVisible('mute') && (this.state.showMuteInput ? (
<div>
<label class="label">
Reason: {}
<input name="mutereason" class="textbox autofocus" placeholder="Mute reason (optional)" />
</label> {} <br />
<button class="button" onClick={this.muteUser} value="7min">For 7 Mins</button> {}
<button class="button" onClick={this.muteUser} value="1hr">For 1 Hour</button> {}
<button class="button" onClick={this.handleCancel}> Cancel</button>
</div>
) : (
<button class="button" onClick={this.handleMute}>
<i class="fa fa-hourglass-half" aria-hidden></i> Mute
</button>
))} {}
{isVisible('ban') && (this.state.showBanInput ? (
<div>
<label class="label">
Reason: {}
<input name="banreason" class="textbox autofocus" placeholder="Ban reason (optional)" />
</label><br />
<button class="button" onClick={this.banUser} value="2d">For 2 Days</button> {}
<button class="button" onClick={this.banUser} value="1wk">For 1 Week</button> {}
<button class="button" onClick={this.handleCancel}>Cancel</button>
</div>
) : (
<button class="button" onClick={this.handleBan}>
<i class="fa fa-gavel" aria-hidden></i> Ban
</button>
))} {}
{isVisible('lock') && (this.state.showLockInput ? (
<div>
<label class="label">
Reason: {}
<input name="lockreason" class="textbox autofocus" placeholder="Lock reason (optional)" />
</label><br />
<button class="button" onClick={this.lockUser} value="2d">For 2 Days</button> {}
<button class="button" onClick={this.lockUser} value="1wk">For 1 Week</button> {}
<button class="button" onClick={this.lockUser} value="nmlk">Namelock</button> {}
<button class="button" onClick={this.handleCancel}>Cancel</button>
</div>
) : (
<button class="button" onClick={this.handleLock}>
<i class="fa fa-lock" aria-hidden></i> Lock/Namelock
</button>
))}
</p>
</div></PSPanelWrapper>;
}
}
class UserListPanel extends PSRoomPanel {
static readonly id = 'userlist';
static readonly routes = ['userlist'];
static readonly location = 'semimodal-popup';
static readonly noURL = true;
override render() {
const room = this.props.room;
const parentRoom = room.getParent() as ChatRoom;
if (parentRoom.type !== 'chat' && parentRoom.type !== 'battle') {
throw new Error(`UserListPanel: ${room.id} is not a chat room`);
}
return <PSPanelWrapper room={room} width={280}><div class="pad">
<ChatUserList room={parentRoom} static />
</div></PSPanelWrapper>;
}
}
class VolumePanel extends PSRoomPanel {
static readonly id = 'volume';
static readonly routes = ['volume'];
static readonly location = 'popup';
setVolume = (e: Event) => {
const slider = e.currentTarget as HTMLInputElement;
PS.prefs.set(slider.name as 'effectvolume', Number(slider.value));
this.forceUpdate();
};
setMute = (e: Event) => {
const checkbox = e.currentTarget as HTMLInputElement;
PS.prefs.set('mute', !!checkbox.checked);
PS.update();
};
override componentDidMount() {
super.componentDidMount();
this.subscriptions.push(PS.prefs.subscribe(() => {
this.forceUpdate();
}));
}
override render() {
const room = this.props.room;
return <PSPanelWrapper room={room}><div class="pad">
<h3>Volume</h3>
<p class="volume">
<label class="optlabel">
Effects: <span class="value">{!PS.prefs.mute && PS.prefs.effectvolume ? `${PS.prefs.effectvolume}%` : `-`}</span>
</label>
{PS.prefs.mute ?
<em>(muted)</em> :
<input
type="range" min="0" max="100" step="1" name="effectvolume" value={PS.prefs.effectvolume}
onChange={this.setVolume} onInput={this.setVolume} onKeyUp={this.setVolume}
/>}
</p>
<p class="volume">
<label class="optlabel">
Music: <span class="value">{!PS.prefs.mute && PS.prefs.musicvolume ? `${PS.prefs.musicvolume}%` : `-`}</span>
</label>
{PS.prefs.mute ?
<em>(muted)</em> :
<input
type="range" min="0" max="100" step="1" name="musicvolume" value={PS.prefs.musicvolume}
onChange={this.setVolume} onInput={this.setVolume} onKeyUp={this.setVolume}
/>}
</p>
<p class="volume">
<label class="optlabel">
Notifications: {}
<span class="value">{!PS.prefs.mute && PS.prefs.notifvolume ? `${PS.prefs.notifvolume}%` : `-`}</span>
</label>
{PS.prefs.mute ?
<em>(muted)</em> :
<input
type="range" min="0" max="100" step="1" name="notifvolume" value={PS.prefs.notifvolume}
onChange={this.setVolume} onInput={this.setVolume} onKeyUp={this.setVolume}
/>}
</p>
<p>
<label class="checkbox">
<input type="checkbox" name="mute" checked={PS.prefs.mute} onChange={this.setMute} /> Mute all
</label>
</p>
</div></PSPanelWrapper>;
}
}
class OptionsPanel extends PSRoomPanel {
static readonly id = 'options';
static readonly routes = ['options'];
static readonly location = 'semimodal-popup';
declare state: { showStatusInput?: boolean, showStatusUpdated?: boolean };
override componentDidMount() {
super.componentDidMount();
this.subscribeTo(PS.user);
}
setTheme = (e: Event) => {
const theme = (e.currentTarget as HTMLSelectElement).value as 'light' | 'dark' | 'system';
PS.prefs.set('theme', theme);
this.forceUpdate();
};
setLayout = (e: Event) => {
const layout = (e.currentTarget as HTMLSelectElement).value;
switch (layout) {
case '':
PS.prefs.set('onepanel', null);
PS.rightPanel ||= PS.rooms['rooms'] || null;
break;
case 'onepanel':
PS.prefs.set('onepanel', true);
break;
case 'vertical':
PS.prefs.set('onepanel', 'vertical');
break;
}
PS.update();
};
setChatroomTimestamp = (ev: Event) => {
const timestamp = (ev.currentTarget as HTMLSelectElement).value as TimestampOptions;
PS.prefs.set('timestamps', { ...PS.prefs.timestamps, chatrooms: timestamp || undefined });
};
setPMsTimestamp = (ev: Event) => {
const timestamp = (ev.currentTarget as HTMLSelectElement).value as TimestampOptions;
PS.prefs.set('timestamps', { ...PS.prefs.timestamps, pms: timestamp || undefined });
};
handleShowStatusInput = (ev: Event) => {
ev.preventDefault();
ev.stopImmediatePropagation();
this.setState({ showStatusInput: !this.state.showStatusInput });
};
handleOnChange = (ev: Event) => {
let elem = ev.currentTarget as HTMLInputElement;
let setting = elem.name;
let value = elem.checked;
switch (setting) {
case 'blockPMs': {
PS.prefs.set("blockPMs", value);
PS.send(value ? '/blockpms' : '/unblockpms');
break;
}
case 'blockChallenges': {
PS.prefs.set("blockChallenges", value);
PS.send(value ? '/blockchallenges' : '/unblockchallenges');
break;
}
case 'bwgfx': {
PS.prefs.set('bwgfx', value);
Dex.loadSpriteData(value || PS.prefs.noanim ? 'bw' : 'xy');
break;
}
case 'language': {
PS.prefs.set(setting, elem.value);
PS.send('/language ' + elem.value);
break;
}
case 'tournaments': {
if (elem.value === "hide") PS.prefs.set(setting, elem.value);
if (elem.value === "notify") PS.prefs.set(setting, elem.value);
if (!elem.value) PS.prefs.set(setting, null);
break;
}
case 'refreshprompt':
case 'noanim':
case 'nopastgens':
case 'noselfhighlight':
case 'leavePopupRoom':
case 'inchatpm':
PS.prefs.set(setting, value);
break;
}
};
editStatus = (ev: Event) => {
const statusInput = this.base!.querySelector<HTMLInputElement>('input[name=statustext]');
PS.send(statusInput?.value?.length ? `|/status ${statusInput.value}` : `|/clearstatus`);
this.setState({ showStatusUpdated: true, showStatusInput: false });
ev.preventDefault();
ev.stopImmediatePropagation();
};
override render() {
const room = this.props.room;
return <PSPanelWrapper room={room}><div class="pad">
<p>
<img
class="trainersprite yours" width="40" height="40" style={{ verticalAlign: 'middle' }}
src={Dex.resolveAvatar(`${PS.user.avatar}`)} data-href="avatars"
/> {}
<strong>{PS.user.name}</strong>
</p>
<p>
<button class="button" data-href="avatars"> Avatar...</button>
</p>
{this.state.showStatusInput ? (
<p>
<input name="statustext" />
<button class="button" onClick={this.editStatus}><i class="fa fa-pencil" aria-hidden></i></button>
</p>
) : (
<p>
<button class="button" onClick={this.handleShowStatusInput} disabled={this.state.showStatusUpdated}>
{this.state.showStatusUpdated ? 'Status Updated' : 'Status...'}</button>
</p>
)}
{PS.user.named && (PS.user.registered?.userid === PS.user.userid ?
<button className="button" data-href="changepassword">Password...</button> :
<button className="button" data-href="register">Register</button>)}
<hr />
<h3>Graphics</h3>
<p>
<label class="optlabel">Theme: <select name="theme" class="button" onChange={this.setTheme}>
<option value="light" selected={PS.prefs.theme === 'light'}>Light</option>
<option value="dark" selected={PS.prefs.theme === 'dark'}>Dark</option>
<option value="system" selected={PS.prefs.theme === 'system'}>Match system theme</option>
</select></label>
</p>
<p>
<label class="optlabel">Layout: <select name="layout" class="button" onChange={this.setLayout}>
<option value="" selected={!PS.prefs.onepanel}>Two panels (if wide enough)</option>
<option value="onepanel" selected={PS.prefs.onepanel === true}>Single panel</option>
<option value="vertical" selected={PS.prefs.onepanel === 'vertical'}>Vertical tabs</option>
</select></label>
</p>
<p>
<label class="optlabel">
Background: <button class="button" data-href="changebackground">
Change Background
</button>
</label>
</p>
<p>
<label class="checkbox"> <input
name="noanim" checked={PS.prefs.noanim || false} type="checkbox" onChange={this.handleOnChange}
/> Disable animations</label>
</p>
<p>
<label class="checkbox"><input
name="bwgfx" checked={PS.prefs.bwgfx || false} type="checkbox" onChange={this.handleOnChange}
/> Use 2D sprites instead of 3D models</label>
</p>
<p>
<label class="checkbox"><input
name="nopastgens" checked={PS.prefs.nopastgens || false} type="checkbox" onChange={this.handleOnChange}
/> Use modern sprites for past generations</label>
</p>
<hr />
<h3>Chat</h3>
<p>
<label class="checkbox"><input
name="blockPMs" checked={PS.prefs.blockPMs || false} type="checkbox" onChange={this.handleOnChange}
/> Block PMs</label>
</p>
<p>
<label class="checkbox"><input
name="blockChallenges" checked={PS.prefs.blockChallenges || false} type="checkbox" onChange={this.handleOnChange}
/> Block challenges</label>
</p>
<p>
<label class="checkbox"><input
name="inchatpm" checked={PS.prefs.inchatpm || false} type="checkbox" onChange={this.handleOnChange}
/> Show PMs in chatrooms</label>
</p>
<p>
<label class="checkbox"><input
name="noselfhighlight" checked={PS.prefs.noselfhighlight || false} type="checkbox" onChange={this.handleOnChange}
/> Do not highlight when your name is said in chat</label>
</p>
<p>
<label class="checkbox"><input
name="leavePopupRoom" checked={PS.prefs.leavePopupRoom || false} type="checkbox" onChange={this.handleOnChange}
/> Confirm before leaving a room</label>
</p>
<p>
<label class="checkbox"><input
name="refreshprompt" checked={PS.prefs.refreshprompt || false} type="checkbox" onChange={this.handleOnChange}
/> Confirm before refreshing</label>
</p>
<p>
<label class="optlabel">
Language: {}
<select name="language" onChange={this.handleOnChange} class="button">
<option value="german" selected={PS.prefs.language === "german"}>Deutsch</option>
<option value="english" selected={PS.prefs.language === "english"}>English</option>
<option value="spanish" selected={PS.prefs.language === "spanish"}>Español</option>
<option value="french" selected={PS.prefs.language === "french"}>Français</option>
<option value="italian" selected={PS.prefs.language === "italian"}>Italiano</option>
<option value="dutch" selected={PS.prefs.language === "dutch"}>Nederlands</option>
<option value="portuguese" selected={PS.prefs.language === "portuguese"}>Português</option>
<option value="turkish" selected={PS.prefs.language === "turkish"}>Türkçe</option>
<option value="hindi" selected={PS.prefs.language === "hindi"}>ि</option>
<option value="japanese" selected={PS.prefs.language === "japanese"}></option>
<option value="simplifiedchinese" selected={PS.prefs.language === "simplifiedchinese"}></option>
<option value="traditionalchinese" selected={PS.prefs.language === "traditionalchinese"}></option>
</select>
</label>
</p>
<p>
<label class="optlabel">
Tournaments: <select name="tournaments" class="button" onChange={this.handleOnChange}>
<option value="" selected={!PS.prefs.tournaments}>No notifications</option>
<option value="notify" selected={PS.prefs.tournaments === "notify"}>Notifications</option>
<option value="hide" selected={PS.prefs.tournaments === "hide"}>Hide</option>
</select>
</label>
</p>
<p>
<label class="optlabel">Timestamps: <select name="layout" class="button" onChange={this.setChatroomTimestamp}>
<option value="" selected={!PS.prefs.timestamps.chatrooms}>Off</option>
<option value="minutes" selected={PS.prefs.timestamps.chatrooms === "minutes"}>[HH:MM]</option>
<option value="seconds" selected={PS.prefs.timestamps.chatrooms === "seconds"}>[HH:MM:SS]</option>
</select></label>
</p>
<p>
<label class="optlabel">Timestamps in DMs: <select name="layout" class="button" onChange={this.setPMsTimestamp}>
<option value="" selected={!PS.prefs.timestamps.pms}>Off</option>
<option value="minutes" selected={PS.prefs.timestamps.pms === "minutes"}>[HH:MM]</option>
<option value="seconds" selected={PS.prefs.timestamps.pms === "seconds"}>[HH:MM:SS]</option>
</select></label>
</p>
<p>
<label class="optlabel">
Chat preferences: {}
<button class="button" data-href="chatformatting">Text formatting...</button>
</label>
</p>
<hr />
{PS.user.named ? <p class="buttonbar" style="text-align: right">
<button class="button" data-href="login"><i class="fa fa-pencil" aria-hidden></i> Change name</button> {}
<button class="button" data-cmd="/logout"><i class="fa fa-power-off" aria-hidden></i> Log out</button>
</p> : <p class="buttonbar" style="text-align: right">
<button class="button" data-href="login"><i class="fa fa-pencil" aria-hidden></i> Choose name</button>
</p> }
</div></PSPanelWrapper>;
}
}
class GooglePasswordBox extends preact.Component<{ name: string }> {
override componentDidMount() {
window.gapiCallback = (response: any) => {
PS.user.changeNameWithPassword(this.props.name, response.credential, { needsGoogle: true });
};
PS.user.gapiLoaded = true;
const script = document.createElement('script');
script.async = true;
script.src = 'https://accounts.google.com/gsi/client';
document.getElementsByTagName('head')[0].appendChild(script);
}
override render() {
return <div class="google-password-box">
<div
id="g_id_onload" data-client_id="912270888098-jjnre816lsuhc5clj3vbcn4o2q7p4qvk.apps.googleusercontent.com"
data-context="signin" data-ux_mode="popup" data-callback="gapiCallback" data-auto_prompt="false"
></div>
<div
class="g_id_signin" data-type="standard" data-shape="pill" data-theme="filled_blue" data-text="continue_with"
data-size="large" data-logo_alignment="left" data-auto_select="true" data-itp_support="true"
style="width:fit-content;margin:0 auto"
>[loading Google log-in button]</div>
</div>;
}
}
class LoginPanel extends PSRoomPanel {
static readonly id = 'login';
static readonly routes = ['login'];
static readonly location = 'semimodal-popup';
declare state: { passwordShown?: boolean };
override componentDidMount() {
super.componentDidMount();
this.subscriptions.push(PS.user.subscribe(args => {
if (args) {
if (args.success) {
this.close();
return;
}
this.props.room.args = args;
setTimeout(() => this.focus(), 1);
}
this.forceUpdate();
}));
}
getUsername() {
const loginName = PS.user.loggingIn || this.props.room.args?.name as string;
if (loginName) return loginName;
const input = this.base?.querySelector<HTMLInputElement>('input[name=username]');
if (input && !input.disabled) {
return input.value;
}
return PS.user.named ? PS.user.name : '';
}
handleSubmit = (ev: Event) => {
ev.preventDefault();
const passwordBox = this.base!.querySelector<HTMLInputElement>('input[name=password]');
if (passwordBox) {
PS.user.changeNameWithPassword(this.getUsername(), passwordBox.value);
} else {
PS.user.changeName(this.getUsername());
}
};
update = () => {
this.forceUpdate();
};
override focus() {
const passwordBox = this.base!.querySelector<HTMLInputElement>('input[name=password]');
const usernameBox = this.base!.querySelector<HTMLInputElement>('input[name=username]');
(passwordBox || usernameBox)?.select();
}
reset = (ev: Event) => {
ev.preventDefault();
ev.stopImmediatePropagation();
this.props.room.args = null;
this.forceUpdate();
};
handleShowPassword = (ev: Event) => {
ev.preventDefault();
ev.stopImmediatePropagation();
this.setState({ passwordShown: !this.state.passwordShown });
};
override render() {
const room = this.props.room;
const loginState = room.args as PSLoginState;
return <PSPanelWrapper room={room} width={280}><div class="pad">
<h3>Log in</h3>
<form onSubmit={this.handleSubmit}>
{loginState?.error && <p class="error">{loginState.error}</p>}
<p><label class="label">
Username: <small class="preview" style={`color:${BattleLog.usernameColor(toID(this.getUsername()))}`}>(color)</small>
<input
class="textbox" type="text" name="username"
onInput={this.update} onChange={this.update} autocomplete="username"
value={this.getUsername()} disabled={!!PS.user.loggingIn || !!loginState?.name}
/>
</label></p>
{PS.user.named && !loginState && <p>
<small>(Others will be able to see your name change. To change name privately, use "Log out")</small>
</p>}
{loginState?.needsPassword && <p>
<i class="fa fa-level-up fa-rotate-90" aria-hidden></i> <strong>if you registered this name:</strong>
<label class="label">
Password: {}
<input
class="textbox" type={this.state.passwordShown ? 'text' : 'password'} name="password"
autocomplete="current-password" style="width:173px"
/>
<button
type="button" onClick={this.handleShowPassword} aria-label="Show password"
class="button" style="float:right;margin:-21px 0 10px;padding: 2px 6px"
><i class="fa fa-eye" aria-hidden></i></button>
</label>
</p>}
{loginState?.needsGoogle && <>
<p><i class="fa fa-level-up fa-rotate-90" aria-hidden></i> <strong>if you registered this name:</strong></p>
<p><GooglePasswordBox name={this.getUsername()} /></p>
</>}
<p class="buttonbar">
{PS.user.loggingIn ? (
<button disabled class="cur">Logging in...</button>
) : loginState?.needsPassword ? (
<>
<button type="submit" class="button"><strong>Log in</strong></button> {}
<button type="button" onClick={this.reset} class="button">Cancel</button>
</>
) : loginState?.needsGoogle ? (
<button type="button" onClick={this.reset} class="button">Cancel</button>
) : (
<>
<button type="submit" class="button"><strong>Choose name</strong></button> {}
<button type="button" name="closeRoom" class="button">Cancel</button>
</>
)} {}
</p>
{loginState?.name && <div>
<p>
<i class="fa fa-level-up fa-rotate-90" aria-hidden></i> <strong>if not:</strong>
</p>
<p style={{ maxWidth: '210px', margin: '0 auto' }}>
This is someone else's account. Sorry.
</p>
<p class="buttonbar">
<button class="button" onClick={this.reset}>Try another name</button>
</p>
</div>}
</form>
</div></PSPanelWrapper>;
}
}
class AvatarsPanel extends PSRoomPanel {
static readonly id = 'avatars';
static readonly routes = ['avatars'];
static readonly location = 'semimodal-popup';
override render() {
const room = this.props.room;
const avatars: [number, string][] = [];
for (let i = 1; i <= 293; i++) {
if (i === 162 || i === 168) continue;
avatars.push([i, window.BattleAvatarNumbers?.[i] || `${i}`]);
}
return <PSPanelWrapper room={room} width={1210}><div class="pad">
<label class="optlabel"><strong>Choose an avatar or </strong>
<button class="button" data-cmd="/close"> Cancel</button>
</label>
<div class="avatarlist">
{avatars.map(([i, avatar]) => (
<button
data-cmd={`/closeand /avatar ${avatar}`} title={`/avatar ${avatar}`}
class={`option pixelated${avatar === PS.user.avatar ? ' cur' : ''}`}
style={`background-position: -${((i - 1) % 16) * 80 + 1}px -${Math.floor((i - 1) / 16) * 80 + 1}px`}
></button>
))}
</div>
<div style="clear:left"></div>
<p><button class="button" data-cmd="/close">Cancel</button></p>
</div></PSPanelWrapper>;
}
}
class BattleForfeitPanel extends PSRoomPanel {
static readonly id = 'forfeit';
static readonly routes = ['forfeitbattle'];
static readonly location = 'semimodal-popup';
static readonly noURL = true;
override render() {
const room = this.props.room;
const battleRoom = room.getParent() as BattleRoom;
return <PSPanelWrapper room={room} width={480}><div class="pad">
<p>Forfeiting makes you lose the battle. Are you sure?</p>
<p>
<button data-cmd="/closeand /inopener /closeand /forfeit" class="button"><strong>Forfeit and close</strong></button> {}
<button data-cmd="/closeand /inopener /forfeit" class="button">Just forfeit</button> {}
{!battleRoom.battle.rated && <button type="button" data-href="replaceplayer" class="button">
Replace player
</button>} {}
<button type="button" data-cmd="/close" class="button">
Cancel
</button>
</p>
</div></PSPanelWrapper>;
}
}
class ReplacePlayerPanel extends PSRoomPanel {
static readonly id = 'replaceplayer';
static readonly routes = ['replaceplayer'];
static readonly location = 'semimodal-popup';
static readonly noURL = true;
handleReplacePlayer = (ev: Event) => {
const room = this.props.room;
const battleRoom = room.getParent()?.getParent() as BattleRoom;
const newPlayer = this.base?.querySelector<HTMLInputElement>("input[name=newplayer]")?.value;
if (!newPlayer?.length) return battleRoom.add("|error|Enter player's name");
if (battleRoom.battle.ended) return battleRoom.add("|error|Cannot replace player, battle has already ended.");
let playerSlot = battleRoom.battle.p1.id === PS.user.userid ? "p1" : "p2";
battleRoom.send('/leavebattle');
battleRoom.send(`/addplayer ${newPlayer}, ${playerSlot}`);
this.close();
ev.preventDefault();
};
override render() {
const room = this.props.room;
return <PSPanelWrapper room={room} width={480}><div class="pad">
<form onSubmit={this.handleReplacePlayer}>
<p>Replacement player's name:</p>
<p>
<input name="newplayer" class="textbox autofocus" />
</p>
<p>
<button type="submit" class="button">
<strong>Replace</strong>
</button> {}
<button type="button" data-cmd="/close" class="button">
Cancel
</button>
</p>
</form>
</div></PSPanelWrapper>;
}
}
class ChangePasswordPanel extends PSRoomPanel {
static readonly id = "changepassword";
static readonly routes = ["changepassword"];
static readonly location = "semimodal-popup";
static readonly noURL = true;
declare state: { errorMsg: string };
handleChangePassword = (ev: Event) => {
ev.preventDefault();
let oldpassword = this.base?.querySelector<HTMLInputElement>('input[name=oldpassword]')?.value;
let password = this.base?.querySelector<HTMLInputElement>('input[name=password]')?.value;
let cpassword = this.base?.querySelector<HTMLInputElement>('input[name=cpassword]')?.value;
if (!oldpassword?.length ||
!password?.length ||
!cpassword?.length) return this.setState({ errorMsg: "All fields are required" });
if (password !== cpassword) return this.setState({ errorMsg: 'Passwords do not match' });
PSLoginServer.query("changepassword", {
oldpassword,
password,
cpassword,
}).then(data => {
if (data?.actionerror) return this.setState({ errorMsg: data?.actionerror });
PS.alert("Your password was successfully changed!");
}).catch(err => {
console.error(err);
this.setState({ errorMsg: err.message });
});
this.setState({ errorMsg: '' });
};
override render() {
const room = this.props.room;
return <PSPanelWrapper room={room} width={280}><div class="pad">
<form onSubmit={this.handleChangePassword}>
{ !!this.state.errorMsg?.length && <p>
<b class="message-error"> {this.state.errorMsg}</b>
</p> }
<p>Change your password:</p>
<p>
<label class="label">
Username: {}
<input name="username" value={PS.user.name} readOnly={true} autocomplete="username" class="textbox disabled" />
</label>
</p>
<p>
<label class="label">
Old password: {}
<input name="oldpassword" type="password" autocomplete="current-password" class="textbox autofocus" />
</label>
</p>
<p>
<label class="label">
New password: {}
<input name="password" type="password" autocomplete="new-password" class="textbox" />
</label>
</p>
<p>
<label class="label">
New password (confirm): {}
<input name="cpassword" type="password" autocomplete="new-password" class="textbox" />
</label>
</p>
<p class="buttonbar">
<button type="submit" class="button">
<strong>Change password</strong>
</button> {}
<button type="button" data-cmd="/close" class="button">Cancel</button>
</p>
</form>
</div>
</PSPanelWrapper>;
}
}
class RegisterPanel extends PSRoomPanel {
static readonly id = "register";
static readonly routes = ["register"];
static readonly location = "semimodal-popup";
static readonly noURL = true;
static readonly rightPopup = true;
declare state: { errorMsg: string };
handleRegisterUser = (ev: Event) => {
ev.preventDefault();
let captcha = this.base?.querySelector<HTMLInputElement>('input[name=captcha]')?.value;
let password = this.base?.querySelector<HTMLInputElement>('input[name=password]')?.value;
let cpassword = this.base?.querySelector<HTMLInputElement>('input[name=cpassword]')?.value;
if (!captcha?.length ||
!password?.length ||
!cpassword?.length) return this.setState({ errorMsg: "All fields are required" });
if (password !== cpassword) return this.setState({ errorMsg: 'Passwords do not match' });
PSLoginServer.query("register", {
captcha,
password,
cpassword,
username: PS.user.name,
challstr: PS.user.challstr,
}).then(data => {
if (data?.actionerror) this.setState({ errorMsg: data?.actionerror });
if (data?.curuser?.loggedin) {
let name = data.curuser.username;
PS.user.registered = { name, userid: toID(name) };
if (data?.assertion) PS.user.handleAssertion(name, data?.assertion);
this.close();
PS.alert("You have been successfully registered.");
}
}).catch(err => {
console.error(err);
this.setState({ errorMsg: err.message });
});
this.setState({ errorMsg: '' });
};
override render() {
const room = this.props.room;
return <PSPanelWrapper room={room} width={280}><div class="pad">
<form onSubmit={this.handleRegisterUser}>
{ !!this.state.errorMsg?.length && <p>
<b class="message-error"> {this.state.errorMsg}</b>
</p> }
<p>Register your account:</p>
<p>
<label class="label">
Username: {}
<input name="name" value={PS.user.name} readOnly={true} autocomplete="username" class="textbox disabled" />
</label>
</p>
<p>
<label class="label">
Password: {}
<input name="password" type="password" autocomplete="new-password" class="textbox autofocus" />
</label>
</p>
<p>
<label class="label">
Password (confirm): {}
<input name="cpassword" type="password" autocomplete="new-password" class="textbox" />
</label>
</p>
<p>
<label class="label"><img
src="https://play.pokemonshowdown.com/sprites/gen5ani/pikachu.gif"
alt="An Electric-type mouse that is the mascot of the Pok&eacute;mon franchise."
/></label>
</p>
<p>
<label class="label">
What is this pokemon? {}
<input name="captcha" class="textbox" />
</label>
</p>
<p class="buttonbar">
<button type="submit" class="button"><strong>Register</strong></button> {}
<button type="button" data-cmd="/close" class="button">Cancel</button>
</p>
</form>
</div>
</PSPanelWrapper>;
}
}
class BackgroundListPanel extends PSRoomPanel {
static readonly id = 'changebackground';
static readonly routes = ['changebackground'];
static readonly location = 'semimodal-popup';
static readonly noURL = true;
declare state: { status?: string };
setBg = (ev: Event) => {
let curtarget = ev.currentTarget as HTMLButtonElement;
let bg = curtarget.value;
PSBackground.set('', bg);
ev.preventDefault();
ev.stopImmediatePropagation();
this.forceUpdate();
};
uploadBg = (ev: Event) => {
this.setState({ status: undefined });
const input = this.base?.querySelector<HTMLInputElement>('input[name=bgfile]');
if (!input?.files?.[0]) return;
const file = input.files[0];
const reader = new FileReader();
reader.onload = () => {
const base64Image = reader.result as string;
PSBackground.set(base64Image, 'custom');
this.forceUpdate();
};
reader.onerror = () => {
this.setState({ status: "Failed to load background image." });
};
reader.readAsDataURL(file);
ev.preventDefault();
ev.stopImmediatePropagation();
};
override render() {
const room = this.props.room;
const option = (val: string) => val === PSBackground.id ? 'option cur' : 'option';
return <PSPanelWrapper room={room} width={480}><div class="pad">
<p><strong>Default</strong></p>
<div class="bglist">
<button onClick={this.setBg} value="" class={option('')}>
<strong
style="
background: #888888;
color: white;
padding: 16px 18px;
display: block;
font-size: 12pt;
"
>Random</strong>
</button>
</div>
<div style="clear: left"></div>
<p><strong>Official</strong></p>
<div class="bglist">
<button onClick={this.setBg} value="charizards" class={option('charizards')}>
<span class="bg" style="background-position: 0 -0px"></span>{}
Charizards
</button>
<button onClick={this.setBg} value="horizon" class={option('horizon')}>
<span class="bg" style="background-position: 0 -90px"></span>{}
Horizon
</button>
<button onClick={this.setBg} value="waterfall" class={option('waterfall')}>
<span class="bg" style="background-position: 0 -180px"></span>{}
Waterfall
</button>
<button onClick={this.setBg} value="ocean" class={option('ocean')}>
<span class="bg" style="background-position: 0 -270px"></span>{}
Ocean
</button>
<button onClick={this.setBg} value="shaymin" class={option('shaymin')}>
<span class="bg" style="background-position: 0 -360px"></span>{}
Shaymin
</button>
<button onClick={this.setBg} value="solidblue" class={option('solidblue')}>
<span class="bg" style="background: #344b6c"></span>{}
Solid blue
</button>
</div>
<div style="clear: left"></div>
<p><strong>Custom</strong></p>
<p>
Upload:
</p>
<p><input type="file" accept="image/*" name="bgfile" onChange={this.uploadBg} /></p>
{!!this.state.status && <p class="error">{this.state.status}</p>}
<p>
<button data-cmd="/close" class="button"><strong>Done</strong></button>
</p>
</div>
</PSPanelWrapper>;
}
}
class ChatFormattingPanel extends PSRoomPanel {
static readonly id = 'chatformatting';
static readonly routes = ['chatformatting'];
static readonly location = 'semimodal-popup';
static readonly noURL = true;
handleOnChange = (ev: Event) => {
const setting = "hide" + (ev.currentTarget as HTMLInputElement).name;
const value = (ev.currentTarget as HTMLInputElement).checked;
let curPref = PS.prefs.chatformatting;
curPref[setting] = value;
PS.prefs.set("chatformatting", curPref);
ev.preventDefault();
ev.stopImmediatePropagation();
};
override render() {
const room = this.props.room;
const ctrl = PSView.isMac ? 'Cmd' : 'Ctrl';
return <PSPanelWrapper room={room} width={480}><div class="pad">
<p>Usable formatting:</p>
<p>**<strong>bold</strong>** (<kbd>{ctrl}</kbd>+<kbd>B</kbd>)</p>
<p>__<em>italics</em>__ (<kbd>{ctrl}</kbd>+<kbd>I</kbd>)</p>
<p>``<code>code formatting</code>`` (<kbd>Ctrl</kbd>+<kbd>`</kbd>)</p>
<p>~~<s>strikethrough</s>~~</p>
<p>^^<sup>superscript</sup>^^</p>
<p>\\<sub>subscript</sub>\\</p>
<p>
<label class="checkbox">
<input
onChange={this.handleOnChange}
type="checkbox"
name="greentext"
checked={PS.prefs.chatformatting.hidegreentext}
/> Suppress <span class="greentext">&gt;greentext</span>
</label>
</p>
<p>
<label class="checkbox">
<input
onChange={this.handleOnChange}
type="checkbox"
name="me"
checked={PS.prefs.chatformatting.hideme}
/> Suppress <code>/me</code> <em>action formatting</em>
</label>
</p>
<p>
<label class="checkbox">
<input
onChange={this.handleOnChange}
type="checkbox"
name="spoiler"
checked={PS.prefs.chatformatting.hidespoiler}
/> Auto-show spoilers: <span class="spoiler">these things</span>
</label>
</p>
<p>
<label class="checkbox">
<input
onChange={this.handleOnChange}
type="checkbox"
name="links"
checked={PS.prefs.chatformatting.hidelinks}
/> Make [[clickable links]] unclickable
</label>
</p>
<p>
<label class="checkbox">
<input
onChange={this.handleOnChange}
type="checkbox"
name="interstice"
checked={PS.prefs.chatformatting.hideinterstice}
/> Don't warn for untrusted links
</label>
</p>
<p><button data-cmd="/close" class="button">Done</button></p>
</div>
</PSPanelWrapper>;
}
}
class LeaveRoomPanel extends PSRoomPanel {
static readonly id = 'confirmleaveroom';
static readonly routes = ['confirmleaveroom'];
static readonly location = 'semimodal-popup';
static readonly noURL = true;
override render() {
const room = this.props.room;
const parentRoomId = (this.props.room.parentElem as HTMLInputElement).value;
return <PSPanelWrapper room={room} width={480}><div class="pad">
<p>Are you sure you want to exit this room?</p>
<p class="buttonbar">
<button data-cmd={`/closeand /close ${parentRoomId}`} class="button autofocus">
<strong>Close Room</strong>
</button> {}
<button data-cmd="/close" class="button">
<strong>Cancel</strong>
</button>
</p>
</div></PSPanelWrapper>;
}
}
class BattleOptionsPanel extends PSRoomPanel {
static readonly id = 'battleoptions';
static readonly routes = ['battleoptions'];
static readonly location = 'semimodal-popup';
static readonly noURL = true;
handleHardcoreMode = (ev: Event) => {
const mode = (ev.currentTarget as HTMLInputElement).checked;
const room = this.getBattleRoom();
if (!room) return this.close();
room.battle.setHardcoreMode(mode);
if (mode) {
room.add(`||Hardcore mode ON: Information not available in-game is now hidden.`);
} else {
room.add(`||Hardcore mode OFF: Information not available in-game is now shown.`);
}
room.update(null);
};
handleIgnoreSpectators = (ev: Event | boolean) => {
const value = typeof ev === "object" ?
(ev.currentTarget as HTMLInputElement).checked :
ev;
const room = this.getBattleRoom();
if (!room) return this.close();
room.battle.ignoreSpects = value;
room.add(`||Spectators ${room.battle.ignoreSpects ? '' : 'no longer '}ignored.`);
const chats = document.querySelectorAll<HTMLElement>('.battle-log .chat');
const displaySetting = room.battle.ignoreSpects ? 'none' : '';
for (const chat of chats) {
const small = chat.querySelector('small');
if (!small) continue;
const text = small.innerText;
const isPlayerChat = text.includes('\u2606') || text.includes('\u2605');
if (!isPlayerChat) {
chat.style.display = displaySetting;
}
}
room.battle.scene.log.updateScroll();
};
handleIgnoreOpponent = (ev: Event | boolean) => {
const value = typeof ev === "object" ?
(ev.currentTarget as HTMLInputElement).checked :
ev;
const room = this.getBattleRoom();
if (!room) return this.close();
room.battle.ignoreOpponent = value;
room.battle.resetToCurrentTurn();
};
handleIgnoreNicks = (ev: Event | boolean) => {
const value = typeof ev === "object" ?
(ev.currentTarget as HTMLInputElement).checked :
ev;
const room = this.getBattleRoom();
if (!room) return this.close();
room.battle.ignoreNicks = value;
room.battle.resetToCurrentTurn();
};
handleAllSettings = (ev: Event) => {
const setting = (ev.currentTarget as HTMLInputElement).name;
const value = (ev.currentTarget as HTMLInputElement).checked;
const room = this.getBattleRoom();
switch (setting) {
case 'autotimer': {
PS.prefs.set('autotimer', value);
if (value) {
room?.send('/timer on');
}
break;
}
case 'ignoreopp': {
PS.prefs.set('ignoreopp', value);
this.handleIgnoreOpponent(value);
break;
}
case 'ignorespects': {
PS.prefs.set('ignorespects', value);
this.handleIgnoreSpectators(value);
break;
}
case 'ignorenicks': {
PS.prefs.set('ignorenicks', value);
this.handleIgnoreNicks(value);
break;
}
case 'rightpanel': {
PS.prefs.set('rightpanelbattles', value);
break;
}
case 'disallowspectators': {
PS.prefs.set('disallowspectators', value);
PS.mainmenu.disallowSpectators = value;
break;
}
}
};
getBattleRoom() {
const battleRoom = this.props.room.getParent() as BattleRoom | null;
return battleRoom?.battle ? battleRoom : null;
}
override render() {
const room = this.props.room;
const battleRoom = this.getBattleRoom();
const isPlayer = !!battleRoom?.battle.myPokemon;
const canOfferTie = battleRoom && ((battleRoom.battle.turn >= 100 && isPlayer) || PS.user.group === '~');
return <PSPanelWrapper room={room} width={380}><div class="pad">
{battleRoom && <>
<p><strong>In this battle</strong></p>
<p>
<label class="checkbox">
<input
checked={battleRoom.battle.hardcoreMode}
type="checkbox" onChange={this.handleHardcoreMode}
/> Hardcore mode (hide info not shown in-game)
</label>
</p>
<p>
<label class="checkbox">
<input
checked={battleRoom.battle.ignoreSpects}
type="checkbox" onChange={this.handleIgnoreSpectators}
/> Ignore spectators
</label>
</p>
<p>
<label class="checkbox">
<input
checked={battleRoom.battle.ignoreOpponent}
type="checkbox" onChange={this.handleIgnoreOpponent}
/> Ignore opponent
</label>
</p>
<p>
<label class="checkbox">
<input
checked={battleRoom.battle?.ignoreNicks}
type="checkbox" onChange={this.handleIgnoreNicks}
/> Ignore nicknames
</label>
</p>
</>}
<p><strong>All battles</strong></p>
<p>
<label class="checkbox">
<input
name="disallowspectators" checked={PS.prefs.disallowspectators || false}
type="checkbox" onChange={this.handleAllSettings}
/> <abbr title="You can still invite spectators by giving them the URL or using the /invite command">Invite only (hide from Battles list)</abbr>
</label>
</p>
<p>
<label class="checkbox">
<input
name="ignorenicks" checked={PS.prefs.ignorenicks || false}
type="checkbox" onChange={this.handleAllSettings}
/> Ignore Pok&eacute;mon nicknames
</label>
</p>
<p>
<label class="checkbox">
<input
name="ignorespects" checked={PS.prefs.ignorespects || false}
type="checkbox" onChange={this.handleAllSettings}
/> Ignore spectators
</label>
</p>
<p>
<label class="checkbox">
<input
name="ignoreopp" checked={PS.prefs.ignoreopp || false}
type="checkbox" onChange={this.handleAllSettings}
/> Ignore opponent
</label>
</p>
<p>
<label class="checkbox">
<input
name="autotimer" checked={PS.prefs.autotimer || false}
type="checkbox" onChange={this.handleAllSettings}
/> Automatically start timer
</label>
</p>
{!PS.prefs.onepanel && document.body.offsetWidth >= 800 && <p>
<label class="checkbox">
<input
name="rightpanel" checked={PS.prefs.rightpanelbattles || false}
type="checkbox" onChange={this.handleAllSettings}
/> Open new battles in the right-side panel
</label>
</p>}
<p class="buttonbar">
<button data-cmd="/close" class="button">Done</button> {}
{battleRoom && <button data-cmd="/closeand /inopener /offertie" class="button" disabled={!canOfferTie}>
Offer Tie
</button>}
</p>
</div>
</PSPanelWrapper>;
}
}
class PopupRoom extends PSRoom {
returnValue: unknown = this.args?.cancelValue;
override destroy() {
(this.args?.callback as any)?.(this.returnValue);
super.destroy();
}
}
class PopupPanel extends PSRoomPanel<PopupRoom> {
static readonly id = 'popup';
static readonly routes = ['popup-*'];
static readonly location = 'semimodal-popup';
static readonly noURL = true;
static readonly Model = PopupRoom;
handleSubmit = (ev: Event) => {
ev.preventDefault();
ev.stopImmediatePropagation();
const room = this.props.room;
room.returnValue = room.args?.okValue;
const textbox = this.base!.querySelector<HTMLInputElement>('input[name=value]');
if (textbox) {
room.returnValue = textbox.value;
}
this.close();
};
override componentDidMount() {
super.componentDidMount();
const textbox = this.base!.querySelector<HTMLInputElement>('input[name=value]');
if (!textbox) return;
textbox.value = this.props.room.args?.value as string || '';
}
parseMessage(message: string) {
if (message.startsWith('|html|')) {
return BattleLog.sanitizeHTML(message.slice(6));
}
return BattleLog.parseMessage(message);
}
override render() {
const room = this.props.room;
const okButton = room.args?.okButton as string || 'OK';
const cancelButton = room.args?.cancelButton as string | undefined;
const otherButtons = room.args?.otherButtons as preact.ComponentChildren;
const value = room.args?.value as string | undefined;
const type = (room.args?.type || (typeof value === 'string' ? 'text' : null)) as string | null;
const message = room.args?.message;
return <PSPanelWrapper room={room} width={480}><form class="pad" onSubmit={this.handleSubmit}>
{message && <p
style="white-space:pre-wrap;word-wrap:break-word"
dangerouslySetInnerHTML={{ __html: this.parseMessage(message as string || '') }}
></p>}
{!!type && <p><input name="value" type={type} class="textbox autofocus" style="width:100%;box-sizing:border-box" /></p>}
<p class="buttonbar">
<button class={`button${!type ? ' autofocus' : ''}`} type="submit" style="min-width:50px">
<strong>{okButton}</strong>
</button> {}
{otherButtons} {}
{!!cancelButton && <button class="button" data-cmd="/close" type="button">
{cancelButton}
</button>}
</p>
</form></PSPanelWrapper>;
}
}
class RoomTabListPanel extends PSRoomPanel {
static readonly id = 'roomtablist';
static readonly routes = ['roomtablist'];
static readonly location = 'semimodal-popup';
static readonly noURL = true;
startingLayout = PS.prefs.onepanel;
handleLayoutChange = (ev: Event) => {
const checkbox = ev.currentTarget as HTMLInputElement;
PS.prefs.onepanel = checkbox.checked ? 'vertical' : this.startingLayout;
PS.update();
};
override render() {
const verticalTabs = PS.prefs.onepanel === 'vertical';
return <PSPanelWrapper room={this.props.room}><div class="tablist">
<ul>
{PS.leftRoomList.map(roomid => PSHeader.renderRoomTab(roomid, true))}
</ul>
<ul>
{PS.rightRoomList.map(roomid => PSHeader.renderRoomTab(roomid, true))}
</ul>
<div class="pad"><label class="checkbox"><input
type="checkbox" checked={verticalTabs} onChange={this.handleLayoutChange}
/> Try vertical tabs</label></div>
</div></PSPanelWrapper>;
}
}
class BattleTimerPanel extends PSRoomPanel {
static readonly id = 'battletimer';
static readonly routes = ['battletimer'];
static readonly location = 'semimodal-popup';
static readonly noURL = true;
override render() {
const room = this.props.room.getParent() as BattleRoom;
return <PSPanelWrapper room={this.props.room}><div class="pad">
{room.battle.kickingInactive ? (
<button class="button" data-cmd="/closeand /inopener /timer stop">Stop Timer</button>
) : (
<button class="button" data-cmd="/closeand /inopener /timer start">Start Timer</button>
)}
</div>
</PSPanelWrapper>;
}
}
PS.addRoomType(
UserPanel,
UserOptionsPanel,
UserListPanel,
VolumePanel,
OptionsPanel,
LoginPanel,
AvatarsPanel,
ChangePasswordPanel,
RegisterPanel,
BattleForfeitPanel,
ReplacePlayerPanel,
BackgroundListPanel,
LeaveRoomPanel,
ChatFormattingPanel,
PopupPanel,
RoomTabListPanel,
BattleOptionsPanel,
BattleTimerPanel
);