Preact minor updates batch 36

- Fix oldclient textbox size
- Highlight changed lines in "Compare" popup
- Update README to prioritize Preact client
- Fix importing uploaded teams
- Fix room width calculation
  - Resizing incorrectly calculated width sometimes
  - Now hardcodes specific breakpoints for re-rendering everything
- Fix scrolling on older WebKit
- Fix radio button keyboard accessibility
  - Left/Right now change radio buttons
- Fix support for bad server code around bad room IDs
  - At this point, `view-[not a room ID]` needs to be treated as a
    valid room ID for anything to work correctly :(
- Support `|cantleave|` and `|allowleave|` in custom roomgames
- Work around Showdex using the deprecated `semimodal-popup`
  room-location type
- Teambuilder: Improve form readonly mode
- Teambuilder: Fix styling on Firefox
- Teambuilder: EV slider on mobile
- Teambuilder: Add +/- nature buttons on mobile
This commit is contained in:
Guangcong Luo
2026-06-29 14:50:03 +00:00
parent 1780e87291
commit 1d6489fce6
16 changed files with 287 additions and 64 deletions

View File

@@ -45,18 +45,15 @@ New client
Development is proceeding on the client rewrite! The live version is
available at https://play.pokemonshowdown.com/beta
You can contribute to it yourself using the same process as before, just
use `testclient-new.html` rather than `testclient-old.html`.
Testing (the old client)
------------------------------------------------------------------------
Client testing requires a build step! Install the latest Node.js (we
require v20 or later) and Git, and run `node build` (on Windows) or `./build`
(on other OSes) to build.
(on other OSes) to build after each change.
(This will do a fast build with no checks. For tests, linting, and
typechecking, you'll want to do `npm test` separately.)
You can make and test client changes simply by building after each change,
and opening `play.pokemonshowdown.com/testclient-old.html`. This will allow you
and opening `play.pokemonshowdown.com/testclient-new.html`. This will allow you
to test changes to the client without setting up your own login server.
### Test keys
@@ -65,9 +62,8 @@ For security reasons, browsers [don't let other websites control PS][5], so
they can't screw with your account, but it does make it harder to log in on
the test client.
The default hack makes you copy/paste the data instead, but if you're
refreshing a lot, just add a `config/testclient-key.js` file, with the
contents:
If you need a logged-in account, add a `config/testclient-key.js` file, with
the contents:
const POKEMON_SHOWDOWN_TESTCLIENT_KEY = 'sid';
@@ -78,21 +74,19 @@ grab it from:
Make sure to put it in `config/` and not `play.pokemonshowdown.com/config/`.
(This is the only supported method of logging in on the beta testclient.)
[5]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
### Other servers
You can connect to an arbitrary server by navigating to
`testclient-old.html?~~host:port`. For example, to connect to a server running
locally on port 8000, you can navigate to `testclient-old.html?~~localhost:8000`.
`testclient-new.html?~~host:port`. For example, to connect to a server running
locally on port 8000, you can navigate to `testclient-new.html?~~localhost:8000`.
**NOTE**: Certain browsers will convert `'?'` to `'%3F'` when reading files off
of the local filesystem. As a workaround, try using a different browser or
serving the files locally first (ie. run `npx http-server` from the
directory this README is in, then navigate in your browser to
`http://localhost:8080/testclient-old.html?~~localhost:8000`).
`http://localhost:8080/testclient-new.html?~~localhost:8000`).
### Limitations

View File

@@ -2501,7 +2501,7 @@ class TeamWizard extends preact.Component<{
<i class="fa fa-undo" aria-hidden></i> Undo delete
</button>
</p> : null;
return <div class="teameditor">
return <div class={`teameditor${editor.readonly ? ' readonly' : ''}`}>
{editor.sets.map((set, i) => [
pasteControls(i),
this.renderSet(set, i),
@@ -3156,7 +3156,7 @@ class TeamEditorForm extends TeamWizard {
}
cur(type: InnerFocusType, setIndex: number, typeIndex = -1) {
const focus = this.props.editor.innerFocus;
return this.props.editor.readonly || (
return (
focus?.type === type && focus.setIndex === setIndex && focus.typeIndex === typeIndex
) ? ' cur' : '';
}
@@ -3233,12 +3233,12 @@ class TeamEditorForm extends TeamWizard {
"Copy/Move"
}
</button> {}
<button
{!editor.readonly && <button
class="option" name="import" onClick={this.clickPanelButton}
value={`set-${i}-import`}
>
<i class="fa fa-upload" aria-hidden></i> Import/Export
</button> {}
</button>} {}
{!(TeamEditorState.clipboard || editor.readonly) && <button
class="option" name="delete" onClick={this.deleteSet} value={i}
>
@@ -3494,6 +3494,7 @@ class SetImportForm extends preact.Component<{
<textarea
ref={this.setTextbox} class="textbox set-import-textbox" rows={14}
readOnly={editor.readonly} onInput={this.inputText}
style="min-height:3em"
></textarea>
<SetSourceButtons
editor={editor} set={this.props.set}
@@ -3753,6 +3754,11 @@ class StatForm extends preact.Component<{
const evInput = this.base!.querySelector<HTMLInputElement>(`input[name="${name}"]`);
if (evInput) evInput.value = value;
}
getEVText(statID: Dex.StatName) {
const ev = `${this.props.set.evs?.[statID] || ''}`;
const plusMinus = this.plus === statID ? '+' : this.minus === statID ? '-' : '';
return ev + plusMinus;
}
update(init?: boolean) {
const { set } = this.props;
const nature = BattleNatures[set.nature!];
@@ -3766,10 +3772,8 @@ class StatForm extends preact.Component<{
this.minus = null;
}
for (const statID of Dex.statNames) {
const ev = `${set.evs?.[statID] || ''}`;
const plusMinus = this.plus === statID ? '+' : this.minus === statID ? '-' : '';
const iv = this.ivToDv(set.ivs?.[statID]);
if (skipID !== `ev-${statID}`) this.setInput(`ev-${statID}`, ev + plusMinus);
if (skipID !== `ev-${statID}`) this.setInput(`ev-${statID}`, this.getEVText(statID));
if (skipID !== `iv-${statID}`) this.setInput(`iv-${statID}`, iv);
}
}
@@ -3818,6 +3822,10 @@ class StatForm extends preact.Component<{
for (const curEv of Object.values(set.evs || {})) totalEv += curEv;
if (totalEv > maxEv && totalEv - value <= maxEv) {
set.evs![statID] = usableMaxEv - (totalEv - value);
// in mobile, you can drag the slider while the textbox is still focused,
// so onChange won't update it, so we manually update it here too
const textbox = this.base!.querySelector<HTMLInputElement>(`input.stat-input[name="ev-${statID}"]`);
if (textbox) textbox.value = this.getEVText(statID);
}
}
} else {
@@ -3863,10 +3871,40 @@ class StatForm extends preact.Component<{
nextInput.select();
ev.preventDefault();
};
changeNatureModifier = (ev: Event) => {
const target = ev.currentTarget as HTMLButtonElement;
const statID = target.value.slice(0, -1) as Dex.StatNameExceptHP;
const modifier = target.value.slice(-1);
if (modifier === '+') {
this.plus = statID;
if (this.minus === statID) this.minus = null;
} else {
this.minus = statID;
if (this.plus === statID) this.plus = null;
}
this.updateNatureFromPlusMinus();
this.props.onChange();
};
updateNatureFromPlusMinus = () => {
const { set } = this.props;
set.nature = Teams.getNatureFromPlusMinus(this.plus, this.minus) || undefined;
};
renderNatureButtons(statID: Dex.StatName) {
if (statID === 'hp' || this.props.editor.gen < 3) return null;
const statName = BattleStatNames[statID];
return <span class="stat-nature-buttons">
<button
class={`button button-first${this.minus === statID ? ' cur' : ''}`}
value={`${statID}-`} onClick={this.changeNatureModifier}
tabIndex={-1} aria-label={`Minus ${statName} Nature`}
>&ndash;</button>
<button
class={`button button-last${this.plus === statID ? ' cur' : ''}`}
value={`${statID}+`} onClick={this.changeNatureModifier}
tabIndex={-1} aria-label={`Plus ${statName} Nature`}
>+</button>
</span>;
}
/** Converts DV/IV in a textbox to the value in set. */
dvToIv(dvOrIvString?: string): number | null {
const dvOrIv = Number(dvOrIvString);
@@ -3941,7 +3979,14 @@ class StatForm extends preact.Component<{
const useIVs = editor.gen > 2;
// label column
const statNames = {
const statNames = editor.narrow ? {
hp: 'HP',
atk: 'Atk',
def: 'Def',
spa: 'SpA',
spd: 'SpD',
spe: 'Spe',
} : {
hp: 'HP',
atk: 'Attack',
def: 'Defense',
@@ -3990,17 +4035,18 @@ class StatForm extends preact.Component<{
<td class="setstatbar">{this.renderStatbar(stat, statID)}</td>
<td><input
name={`ev-${statID}`} placeholder={`${defaultEV || ''}`}
type="text" inputMode="numeric" class="textbox default-placeholder stat-input" style="width:40px"
type="text" inputMode="numeric" class="textbox default-placeholder stat-input" style="width:40px;vertical-align:middle"
onInput={this.changeEV} onChange={this.changeEV} onKeyDown={this.keyDownStatInput}
/></td>
/>{this.renderNatureButtons(statID)}</td>
<td><input
name={`evslider-${statID}`} value={set.evs?.[statID] ?? defaultEV} min="0" max={maxEV} step={stepEV}
type="range" class="evslider" tabIndex={-1} aria-hidden
onInput={this.changeEV} onChange={this.changeEV}
/></td>
{!editor.isChampions && <td><input
name={`iv-${statID}`} min={0} max={useIVs ? 31 : 15} placeholder={`${defaultIVs[statID]}`} style="width:40px"
type="number" inputMode="numeric" class="textbox default-placeholder stat-input" onInput={this.changeIV}
name={`iv-${statID}`} min={0} max={useIVs ? 31 : 15} placeholder={`${defaultIVs[statID]}`}
style={editor.narrow ? "width:22px" : "width:40px"} type={editor.narrow ? 'text' : 'number'} inputMode="numeric"
class="textbox default-placeholder stat-input" onInput={this.changeIV}
onChange={this.changeIV} onKeyDown={this.keyDownStatInput}
/></td>}
<td style="text-align:right"><strong>{stat}</strong></td>
@@ -4022,7 +4068,7 @@ class StatForm extends preact.Component<{
))}
</select>
</p>}
{editor.gen >= 3 && <p>
{editor.gen >= 3 && !editor.narrow && <p>
<small><em>Protip:</em> You can also set natures by typing <kbd>+</kbd> and <kbd>-</kbd> in the EV box.</small>
</p>}
{editor.gen >= 3 && this.renderStatOptimizer()}

View File

@@ -23,6 +23,9 @@ import type preact from '../js/lib/preact';
declare const BattleTextAFD: any;
declare const BattleTextNotAFD: any;
export const VERTICAL_HEADER_WIDTH = 240;
export const NARROW_MODE_HEADER_WIDTH = 280;
/**********************************************************************
* Config
*********************************************************************/
@@ -1966,11 +1969,18 @@ export const PS = new class extends PSModel {
/**
* * 0 = only one panel visible
* * null = vertical nav layout
* n.b. PS will only update if the left room width changes. Resizes
* that don't change the left room width will not trigger an update.
* n.b. Resizes only trigger a re-render if the panel layout or a
* width-dependent layout breakpoint changes.
*/
leftPanelWidth: number | null = 0;
mainmenu: MainMenuRoom = null!;
layoutViewportWidth = 0;
roomWidthBreakpointPassed(oldWidth: number, newWidth: number) {
return (oldWidth < 550) !== (newWidth < 550) || // chat-room userlists, teambuilder
(oldWidth < 620) !== (newWidth < 620) || // main menu and teambuilder tiny-layout class
(oldWidth <= 700) !== (newWidth <= 700); // battle tiny-layout
}
/**
* The drag-and-drop API is incredibly dumb and doesn't let us know
@@ -2101,27 +2111,37 @@ export const PS = new class extends PSModel {
/** @returns changed */
updateLayout(): boolean {
const leftPanelWidth = this.calculateLeftPanelWidth();
const viewportWidth = document.documentElement.clientWidth;
const totalWidth = document.body.offsetWidth;
const totalHeight = document.body.offsetHeight;
const roomHeight = totalHeight - 56;
let needsUpdate = this.leftPanelWidth !== leftPanelWidth;
if (leftPanelWidth === null) {
this.panel.width = totalWidth - 200;
this.panel.height = totalHeight;
const headerWidth = viewportWidth <= 700 ?
NARROW_MODE_HEADER_WIDTH : VERTICAL_HEADER_WIDTH;
const roomWidth = totalWidth + 1 - headerWidth;
needsUpdate ||= this.roomWidthBreakpointPassed(this.panel.width, roomWidth);
this.panel.width = roomWidth;
this.panel.height = totalHeight - 30;
} else if (leftPanelWidth) {
const rightPanelWidth = totalWidth + 1 - leftPanelWidth;
needsUpdate ||= this.roomWidthBreakpointPassed(this.leftPanel.width, leftPanelWidth);
needsUpdate ||= this.roomWidthBreakpointPassed(this.rightPanel!.width, rightPanelWidth);
this.leftPanel.width = leftPanelWidth;
this.leftPanel.height = roomHeight;
this.rightPanel!.width = totalWidth + 1 - leftPanelWidth;
this.rightPanel!.width = rightPanelWidth;
this.rightPanel!.height = roomHeight;
} else {
needsUpdate ||= this.roomWidthBreakpointPassed(this.panel.width, totalWidth);
this.panel.width = totalWidth;
this.panel.height = roomHeight;
}
if (this.leftPanelWidth !== leftPanelWidth) {
this.leftPanelWidth = leftPanelWidth;
return true;
}
return false;
this.layoutViewportWidth = viewportWidth;
return needsUpdate;
}
getRoom(elem: HTMLElement | EventTarget | null | undefined, skipClickable?: boolean): PSRoom | null {
let curElem: HTMLElement | null = elem as HTMLElement;
@@ -2286,6 +2306,9 @@ export const PS = new class extends PSModel {
return room === this.rightPanel || room === this.leftPanel || room === this.room;
}
}
/**
* @see {@link leftPanelWidth} for return value meaning
*/
calculateLeftPanelWidth() {
const available = document.body.offsetWidth;
if (document.documentElement.clientWidth < 800 || this.prefs.onepanel === 'vertical') {
@@ -2328,12 +2351,26 @@ export const PS = new class extends PSModel {
}
createRoom(options: RoomOptions) {
options.location ||= this.getRouteLocation(options.id);
options.location = this.normalizeRoomLocation(options.location);
options.type ||= this.getRoute(options.id) || '';
const RoomType = this.roomTypes[options.type];
options.noURL ??= RoomType?.noURL;
if (RoomType?.title) options.title = RoomType.title;
const Model = RoomType ? (RoomType.Model || PSRoom) : PlaceholderRoom;
return new Model(options);
const room = new Model(options);
room.location = this.normalizeRoomLocation(room.location);
return room;
}
normalizeRoomLocation(location: string): PSRoomLocation;
normalizeRoomLocation(location: string | undefined): PSRoomLocation | undefined;
normalizeRoomLocation(location: any): any {
if (!location) return location;
switch (location) {
case 'left': case 'right': case 'popup': case 'mini-window': case 'modal-popup':
return location;
default:
return 'modal-popup';
}
}
getRouteInfo(roomid: RoomID) {
if (this.routes[roomid]) return this.routes[roomid];
@@ -2353,8 +2390,10 @@ export const PS = new class extends PSModel {
}
const routeInfo = this.getRouteInfo(roomid);
if (!routeInfo) return 'left';
if (routeInfo.startsWith('*')) return routeInfo.slice(1) as PSRoomLocation;
return PS.roomTypes[routeInfo]!.location || 'left';
if (routeInfo.startsWith('*')) {
return this.normalizeRoomLocation(routeInfo.slice(1));
}
return this.normalizeRoomLocation(PS.roomTypes[routeInfo]!.location) || 'left';
}
getRoute(roomid: RoomID) {
const routeInfo = this.getRouteInfo(roomid);
@@ -2671,6 +2710,7 @@ export const PS = new class extends PSModel {
return room.location === 'left' || room.location === 'right';
}
moveRoom(room: PSRoom, location: PSRoomLocation, background?: boolean, index?: number) {
location = this.normalizeRoomLocation(location);
if (room.location === location && index === undefined) {
if (background === true) {
if (room === this.leftPanel) {

View File

@@ -147,13 +147,15 @@ export class BattleRoom extends ChatRoom {
request: BattleRequest | null = null;
choices: BattleChoiceBuilder | null = null;
autoTimerActivated: boolean | null = null;
requireForfeit = false;
/** should be false if we joined right after accepting or challenging a battle,
* and true if we refreshed and rejoined a battle.
* null = initializing, we don't know yet */
rejoining: boolean | null = null;
override interruptClose(explicit?: boolean, elem?: HTMLElement | null) {
if (!this.battle.ended && this.users[PS.user.userid]?.startsWith('☆') && !this.battle.isReplay) {
const battle = this.battle;
if ((battle && !battle.ended && this.side && !battle.isReplay) || this.requireForfeit) {
PS.join('forfeitbattle' as RoomID, { parentElem: elem, parentRoomid: this.id });
return `You are still in ${this.title}`;
}
@@ -404,6 +406,12 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
override receiveLine(args: Args) {
const room = this.props.room;
switch (args[0]) {
case 'cantleave':
room.requireForfeit = true;
return;
case 'allowleave':
room.requireForfeit = false;
return;
case 'initdone':
if (!PS.prefs.spectatefromstart) room.battle.seekTurn(Infinity);
return;
@@ -1095,7 +1103,7 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
dangerouslySetInnerHTML={{ __html: `#${id} .battle .turn, #${id} .battle-history { display: none !important; }` }}
></style> : null;
if (room.width < 700) {
if (room.width <= 700) {
return <PSPanelWrapper room={room} focusClick noScroll="hidden">
{hardcoreStyle}
<BattleDiv room={room} />

View File

@@ -1422,7 +1422,7 @@ class ChatPanel extends PSRoomPanel<ChatRoom> {
override render() {
const room = this.props.room;
const tinyLayout = room.width < 450;
const tinyLayout = room.width < 550;
const challengeOpen = room.challengeMenuOpen || room.challenging || room.challenged;
return <PSPanelWrapper room={room} focusClick noScroll fullSize>

View File

@@ -1082,6 +1082,7 @@ export class TeamForm extends preact.Component<{
<textarea
name="customrules" class="textbox" rows={3} placeholder="Rules separated by commas or lines"
value={this.customRuleText} onInput={this.changeCustomRules}
style="min-height:3em"
/>
<small><a
href="https://github.com/smogon/pokemon-showdown/blob/master/config/CUSTOM-RULES.md" target="_blank"

View File

@@ -1024,7 +1024,7 @@ class BattleForfeitPanel extends PSRoomPanel {
<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">
{battleRoom.battle && !battleRoom.battle.rated && <button type="button" data-href="replaceplayer" class="button">
Replace player
</button>} {}
<button type="button" data-cmd="/close" class="button">

View File

@@ -129,6 +129,87 @@ class TeamPanel extends PSRoomPanel<TeamRoom> {
});
}
static diffLines(localLines: string[], uploadedLines: string[]) {
// https://en.wikipedia.org/wiki/Longest_common_subsequence
const lcs: number[][] = [];
for (let i = 0; i <= localLines.length; i++) {
lcs[i] = [];
for (let j = 0; j <= uploadedLines.length; j++) lcs[i][j] = 0;
}
for (let i = localLines.length - 1; i >= 0; i--) {
for (let j = uploadedLines.length - 1; j >= 0; j--) {
lcs[i][j] = localLines[i] === uploadedLines[j] ?
lcs[i + 1][j + 1] + 1 :
Math.max(lcs[i + 1][j], lcs[i][j + 1]);
}
}
const rows: { local?: string, uploaded?: string, changed: boolean }[] = [];
const addChangedRows = (fromI: number, toI: number, fromJ: number, toJ: number) => {
const count = Math.max(toI - fromI, toJ - fromJ);
for (let k = 0; k < count; k++) rows.push({
local: k < toI - fromI ? localLines[fromI + k] : undefined,
uploaded: k < toJ - fromJ ? uploadedLines[fromJ + k] : undefined,
changed: true,
});
};
const anchors: [number, number][] = [];
let i = 0;
let j = 0;
while (i < localLines.length && j < uploadedLines.length) {
if (localLines[i] === uploadedLines[j]) {
anchors.push([i, j]);
i++;
j++;
} else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
i++;
} else {
j++;
}
}
let lastI = 0;
let lastJ = 0;
for (const [nextI, nextJ] of anchors) {
addChangedRows(lastI, nextI, lastJ, nextJ);
rows.push({ local: localLines[nextI], uploaded: uploadedLines[nextJ], changed: false });
lastI = nextI + 1;
lastJ = nextJ + 1;
}
addChangedRows(lastI, localLines.length, lastJ, uploadedLines.length);
return rows;
}
static renderDiffLine(line: string | undefined) {
return line ? BattleLog.escapeHTML(line) : '&nbsp;';
}
static renderTeamDiff(localTeam: string, uploadedTeam: string) {
const trimmedLocalTeam = localTeam.replace(/\n+$/, '');
const trimmedUploadedTeam = uploadedTeam.replace(/\n+$/, '');
const localSets = trimmedLocalTeam ? trimmedLocalTeam.split(/\n\n+/) : [];
const uploadedSets = trimmedUploadedTeam ? trimmedUploadedTeam.split(/\n\n+/) : [];
const setCount = Math.max(localSets.length, uploadedSets.length);
let buf = `|html|<table class="table" style="width:100%;font-size:14px">` +
`<tr><th>Local</th>` +
`<th>Uploaded</th></tr>`;
for (let i = 0; i < setCount; i++) {
if (i) {
buf += `<tr><td style="border-top:0;border-bottom:0;padding:0 5px">&nbsp;</td>` +
`<td style="border-top:0;border-bottom:0;padding:0 5px">&nbsp;</td></tr>`;
}
const rows = this.diffLines(
localSets[i]?.split('\n') || [],
uploadedSets[i]?.split('\n') || []
);
for (const row of rows) {
const className = row.changed ? ` class="highlighted"` : ``;
buf += `<tr><td${className} style="border-top:0;border-bottom:0;padding:0 5px">` +
`${this.renderDiffLine(row.local)}</td>` +
`<td${className} style="border-top:0;border-bottom:0;padding:0 5px">` +
`${this.renderDiffLine(row.uploaded)}</td></tr>`;
}
}
return buf + `</table>`;
}
handleRename = (ev: Event) => {
const textbox = ev.currentTarget as HTMLInputElement;
const room = this.props.room;
@@ -161,9 +242,9 @@ class TeamPanel extends PSRoomPanel<TeamRoom> {
PS.alert(`Must use on an uploaded team.`);
return;
}
const uploadedTeam = Teams.export(Teams.unpack(team.uploadedPackedTeam));
const localTeam = Teams.export(Teams.unpack(team.packedTeam));
PS.alert(BattleLog.html`|html|<table class="table" style="width:100%;font-size:14px"><tr><th>Local</th><th>Uploaded</th></tr><tr><td>${localTeam}</td><td>${uploadedTeam}</td></tr></table>`, { width: 720 });
const uploadedTeam = Teams.export(Teams.unpack(team.uploadedPackedTeam), undefined, true);
const localTeam = Teams.export(Teams.unpack(team.packedTeam), undefined, true);
PS.alert(TeamPanel.renderTeamDiff(localTeam, uploadedTeam), { width: 720 });
ev.preventDefault();
ev.stopImmediatePropagation();
};
@@ -269,7 +350,8 @@ class TeamPanel extends PSRoomPanel<TeamRoom> {
</label>
<TeamEditor
team={team} onChange={this.save} readOnly={!!team.teamid && !team.uploadedPackedTeam} resources={this.renderResources()}
editorRef={(editor: TeamEditorState) => { this.props.room.editor = editor; }}
narrow={room.width < 550}
editorRef={(editor: TeamEditorState) => { room.editor = editor; }}
>
{!!(team.packedTeam && team.format.length > 4) && <p>
<button data-cmd="/validate" class="button"><i class="fa fa-check"></i> Validate</button>

View File

@@ -42,6 +42,7 @@ class PSTextarea extends preact.Component<{ initialValue?: string, name?: string
return <div style="position:relative">
<textarea
name={this.props.name} class="textbox" onInput={this.updateSize} onKeyUp={this.updateSize}
style="min-height:3em"
/>
{!this.cssAutosize && <div><textarea
class="textbox heighttester"
@@ -503,11 +504,26 @@ class TeambuilderPanel extends PSRoomPanel<TeambuilderRoom> {
if (this.props.room.exportMode !== true) return alert('Wrong export mode');
const teams = PSTeambuilder.importTeamBackup(value);
const uploadedTeams: { [teamid: number]: Team | undefined } = {};
for (const team of PS.teams.list) {
if (team.teamid) uploadedTeams[team.teamid] = team;
}
const notLoadedTeamRegex = /^[^|]*\|\|\|\|\|\|\|\|\|\|\|(?:\][^|]*\|\|\|\|\|\|\|\|\|\|\|)*$/;
// const visibleTeams = this.visibleTeams();
// alert(`${teams.length} teams imported, ${visibleTeams.length} teams visible now`);
PS.teams.list = [];
PS.teams.byKey = {};
for (const team of teams) PS.teams.push(team);
for (const team of teams) {
const uploadedTeam = team.teamid ? uploadedTeams[team.teamid] : null;
if (uploadedTeam?.uploaded) {
team.uploaded = uploadedTeam.uploaded;
team.uploaded.notLoaded = notLoadedTeamRegex.test(team.packedTeam);
}
if (uploadedTeam?.uploadedPackedTeam !== undefined) {
team.uploadedPackedTeam = uploadedTeam.uploadedPackedTeam;
}
PS.teams.push(team);
}
// TODO: say what changed
const room = this.props.room;

View File

@@ -10,8 +10,10 @@
*/
import preact from "../js/lib/preact";
import { Config, PS, type PSRoom, type RoomID } from "./client-main";
import { NARROW_MODE_HEADER_WIDTH, PSView, VERTICAL_HEADER_WIDTH } from "./panels";
import {
Config, NARROW_MODE_HEADER_WIDTH, PS, type PSRoom, type RoomID, VERTICAL_HEADER_WIDTH,
} from "./client-main";
import { PSView } from "./panels";
import type { Battle } from "./battle";
import { BattleLog } from "./battle-log"; // optional

View File

@@ -16,12 +16,10 @@ import type { Args } from "./battle-text-parser";
import { BattleTooltips } from "./battle-tooltips";
import { Net } from "./client-connection";
import type { PSModel, PSStreamModel, PSSubscription } from "./client-core";
import { PS, type PSRoom, type RoomID } from "./client-main";
import { NARROW_MODE_HEADER_WIDTH, PS, type PSRoom, type RoomID, VERTICAL_HEADER_WIDTH } from "./client-main";
import type { ChatRoom } from "./panel-chat";
import { PSHeader, PSMiniHeader } from "./panel-topbar";
export const VERTICAL_HEADER_WIDTH = 240;
export const NARROW_MODE_HEADER_WIDTH = 280;
export const EXTERNAL_REDIRECTS = /^(appeals?|rooms?suggestions?|suggestions?|adminrequests?|bugs?|bugreports?|rules?|faq|credits?|privacy|contact|dex|insecure)$/;
export class PSRouter {
@@ -70,7 +68,8 @@ export class PSRouter {
if (url.startsWith('/')) url = url.slice(1);
if (url === '.') url = '';
if (!/^[a-z0-9-]*$/.test(url)) return null;
// (exaggerated sigh) PLEASE STOP PUTTING RANDOM CHARACTERS IN ROOM IDS
if (!/^[a-z0-9-]*$/.test(url) && !url.startsWith('view-')) return null;
if (EXTERNAL_REDIRECTS.test(url)) return null;
@@ -807,10 +806,10 @@ export class PSView extends preact.Component {
if (modifierKey) return;
if (kc === 37) { // left
if (kc === 37 && elem?.type !== 'radio') { // left
PS.arrowKeysUsed = true;
PS.focusLeftRoom();
} else if (kc === 39) { // right
} else if (kc === 39 && elem?.type !== 'radio') { // right
PS.arrowKeysUsed = true;
PS.focusRightRoom();
} else if (kc === 191 && !isTextInput && PS.room === PS.mainmenu) { // forward slash

View File

@@ -448,7 +448,6 @@ textarea.textbox {
box-sizing: border-box;
resize: none;
field-sizing: content;
min-height: 3em;
}
pre.textbox {
white-space: pre-wrap;
@@ -724,6 +723,12 @@ input[type=range]:active::-webkit-slider-thumb {
padding: 3px 0 3px 0;
font-size: 9pt;
}
.highlighted {
background: rgba(255,200,80,0.33);
}
.dark .highlighted {
background: rgba(120,220,255,0.28);
}
.chat.highlighted {
margin-left: -8px;
margin-right: -8px;

View File

@@ -5,7 +5,7 @@ License: GPLv2
*/
@import url(./battle-log.css?v12.4);
@import url(./battle-log.css?v12.6);
.battle {
position: absolute;

View File

@@ -1144,6 +1144,7 @@ p.or:after {
}
.chatbox textarea {
display: block;
box-sizing: content-box;
height: 1.1em;
width: 100%;
resize: none;

View File

@@ -1223,12 +1223,6 @@ form.menugroup {
.debug {
display: none;
}
.highlighted {
background: rgba(255,200,80,0.33);
}
.dark .highlighted {
background: rgba(120,220,255,0.28);
}
.chat.highlighted {
margin-left: -8px;
margin-right: -8px;

View File

@@ -11,6 +11,7 @@
border-left: 5px solid #d7e3ec;
width: 154px;
overflow: auto;
-webkit-overflow-scrolling: touch;
overflow-x: hidden;
}
.teampane {
@@ -301,6 +302,7 @@
right: -14px;
min-height: 150px;
overflow: auto;
-webkit-overflow-scrolling: touch;
max-height: 80vh;
}
.searchresults, .team-focus-editor {
@@ -532,6 +534,15 @@
.evslider {
width: 170px;
}
.stat-nature-buttons {
display: none;
margin-left: 2px;
white-space: nowrap;
vertical-align: middle;
}
.tiny-layout .stat-nature-buttons {
display: inline-block;
}
.tiny-layout .evslider {
width: 120px;
}
@@ -686,6 +697,9 @@ you can't delete it by pressing Backspace */
.set-form .set-moves .label:hover .button {
display: inline-block;
}
.readonly.readonly .set-form .set-moves .button {
display: none;
}
.set-button.set-form .label .textbox {
margin-top: 0;
}
@@ -706,6 +720,16 @@ you can't delete it by pressing Backspace */
margin-top: -12px;
margin-right: 2px;
}
.readonly .set-button .textbox {
border: 1px solid #CCC;
box-shadow: none;
}
.dark .readonly .set-button .textbox {
border: 1px solid #444;
}
.readonly .set-button button.textbox {
cursor: not-allowed;
}
/* .set-button.set-form .set-pokemon .label {
text-shadow: #c8d3e9 1px 1px 0, #c8d3e9 1px -1px 0, #c8d3e9 -1px 1px 0, #c8d3e9 -1px -1px 0;
}
@@ -826,9 +850,16 @@ you can't delete it by pressing Backspace */
.team-focus-editor .set-button {
z-index: 1;
}
.team-focus-top {
height: 230px;
overflow: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
}
.team-focus-editor .tabbar {
/* note to self: make this scrollable later */
overflow: auto;
-webkit-overflow-scrolling: touch;
white-space: nowrap;
min-height: 59px;
}
@@ -858,6 +889,10 @@ you can't delete it by pressing Backspace */
border-left: 0;
padding: 6px 3px;
}
.searchboxwrapper {
padding-top: 1px;
padding-bottom: 0;
}
.searchboxwrapper .textbox {
width: 100%;
font-size: 14px;