mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-09-12 13:59:09 -05:00
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:
26
README.md
26
README.md
@@ -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
|
||||
|
||||
|
||||
@@ -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`}
|
||||
>–</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()}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) : ' ';
|
||||
}
|
||||
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"> </td>` +
|
||||
`<td style="border-top:0;border-bottom:0;padding:0 5px"> </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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -5,7 +5,7 @@ License: GPLv2
|
||||
|
||||
*/
|
||||
|
||||
@import url(./battle-log.css?v12.4);
|
||||
@import url(./battle-log.css?v12.6);
|
||||
|
||||
.battle {
|
||||
position: absolute;
|
||||
|
||||
@@ -1144,6 +1144,7 @@ p.or:after {
|
||||
}
|
||||
.chatbox textarea {
|
||||
display: block;
|
||||
box-sizing: content-box;
|
||||
height: 1.1em;
|
||||
width: 100%;
|
||||
resize: none;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user