mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-08-20 01:51:42 -05:00
Preact minor updates batch 42
- Improve type icon readability
- I can't believe `-webkit-text-stroke` has been Baseline since 2019
and no one's bothered to standardize it
- Fix pasting in Firefox
- Fix text box height in older browsers
- Fix favicon notifying
- Fix custom avatars in replays
- Fix unknown avatars being horizontally flipped
- Move "Offer Tie" button to "This battle" section of Battle Options,
and add a "Forfeit" button
- Fix select menu readability in Chrome Windows
- Fix color of "Add status" text box in light mode
- Fix clicking on filters in teambuilder search
- Fix Imprison preventing cancel in FFA
- Fix bandwidth usage when clicking yourself
- Fix uhtml notifying outside of DMs
This commit is contained in:
@@ -740,9 +740,10 @@ export class BattleScene implements BattleSceneStub {
|
||||
}
|
||||
badgehtml += '</span>';
|
||||
}
|
||||
const avatar = Dex.resolveAvatar(side.avatar);
|
||||
return (
|
||||
`<div class="trainer trainer-${posStr}"${faded}><strong>${BattleLog.escapeHTML(side.name)}</strong>` +
|
||||
`<div class="trainersprite"${ratinghtml} style="background-image:url(${Dex.resolveAvatar(side.avatar)})">` +
|
||||
`<div class="trainersprite${avatar.includes('unknown') ? ' noflip' : ''}"${ratinghtml} style="background-image:url(${avatar})">` +
|
||||
`</div>${badgehtml}${pokemonhtml}</div>`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -224,14 +224,15 @@ export class BattleChoiceBuilder {
|
||||
/** only the last choice can be uncancelable */
|
||||
const isLastChoice = this.choices.length + 1 >= this.requestLength();
|
||||
if (choice.choiceType === 'move') {
|
||||
if (!choice.targetLoc && (this.request as BattleMoveRequest).targetable) {
|
||||
const targetable = (this.request as BattleMoveRequest).targetable;
|
||||
if (!choice.targetLoc && targetable) {
|
||||
const choosableTargets: unknown[] = ['normal', 'any', 'adjacentAlly', 'adjacentAllyOrSelf', 'adjacentFoe'];
|
||||
if (choosableTargets.includes(this.currentMove(choice)?.target)) {
|
||||
this.current = choice;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (this.currentMoveRequest()?.maybeDisabled && isLastChoice && this.requestLength() === 1) {
|
||||
if (this.currentMoveRequest()?.maybeDisabled && isLastChoice && !targetable) {
|
||||
this.noCancel = true;
|
||||
}
|
||||
if (choice.mega || choice.megax || choice.megay) this.alreadyMega = true;
|
||||
|
||||
@@ -296,6 +296,9 @@ export const Dex = new class implements ModdedDex {
|
||||
return Dex.resourcePrefix + 'sprites/trainers-custom/' + toID(avatar.substr(1)) + '.png';
|
||||
}
|
||||
if (avatar.includes('.')) {
|
||||
if (!window.Config?.server) {
|
||||
return Dex.resourcePrefix + 'sprites/trainers/unknown.png';
|
||||
}
|
||||
// previously checked `&& window.Config?.server?.registered`
|
||||
// currently doesn't, bc server registration isn't a thing anymore
|
||||
// custom avatar served by the server
|
||||
|
||||
@@ -2094,7 +2094,7 @@ class TeamEditorForm extends preact.Component<{
|
||||
const cur = (i: number) => setIndex === i ? ' cur' : '';
|
||||
const isSearchMode = type !== 'stats' && type !== 'details' && type !== 'import';
|
||||
const SEARCH_PLACEHOLDERS = {
|
||||
'pokemon': 'Search species or filter by type, learnable moves, ability, or egg group',
|
||||
'pokemon': 'Search species or filter by type, learnable moves, ability, tier, or egg group',
|
||||
'ability': 'Search abilities',
|
||||
'item': 'Search items',
|
||||
'move': 'Search moves or filter by type or category',
|
||||
@@ -2708,6 +2708,7 @@ class TeamEditorForm extends preact.Component<{
|
||||
this.resetScroll();
|
||||
this.forceUpdate();
|
||||
} else if (!type) {
|
||||
this.focusFocusedSetField();
|
||||
const searchBox = this.base!.querySelector<HTMLInputElement>('input[name=value]');
|
||||
if (searchBox) {
|
||||
searchBox.value = '';
|
||||
|
||||
@@ -163,6 +163,7 @@ export class BattleTooltips {
|
||||
static parentElem: HTMLElement | null = null;
|
||||
static isLocked = false;
|
||||
static isPressed = false;
|
||||
static outsideClickListenerAdded = false;
|
||||
static allowsTouchScroll(elem: HTMLElement | null) {
|
||||
return elem && (elem.tagName === 'DIV' || elem.tagName === 'SPAN');
|
||||
}
|
||||
@@ -185,6 +186,14 @@ export class BattleTooltips {
|
||||
$('#tooltipwrapper').removeClass('tooltip-locking-click tooltip-locking-tap');
|
||||
}
|
||||
|
||||
static dismissOnOutsideClick(e: MouseEvent) {
|
||||
if (!BattleTooltips.elem) return;
|
||||
const target = e.target;
|
||||
if (target && BattleTooltips.elem.contains(target as Node)) return;
|
||||
if (target && (target as Element).closest?.('.has-tooltip')) return;
|
||||
BattleTooltips.hideTooltip();
|
||||
}
|
||||
|
||||
lockTooltip() {
|
||||
if (BattleTooltips.elem && !BattleTooltips.isLocked) {
|
||||
BattleTooltips.isLocked = true;
|
||||
@@ -204,6 +213,10 @@ export class BattleTooltips {
|
||||
}
|
||||
|
||||
listen(elem: HTMLElement | JQuery) {
|
||||
if (!BattleTooltips.outsideClickListenerAdded) {
|
||||
window.addEventListener('click', BattleTooltips.dismissOnOutsideClick, true);
|
||||
BattleTooltips.outsideClickListenerAdded = true;
|
||||
}
|
||||
const $elem = $(elem);
|
||||
$elem.on('mouseover.battleTooltips', '.has-tooltip', this.mouseOverEvent);
|
||||
$elem.on('click.battleTooltips', '.has-tooltip', this.clickTooltipEvent);
|
||||
|
||||
@@ -203,6 +203,7 @@ export class MiniEditPastePlugin {
|
||||
.replace(/<(script|style)\b[\s\S]*?<\/\1>/gi, '')
|
||||
// handle newlines
|
||||
// .replace(/\n/g, '<br>') // in case they're in <pre>?
|
||||
.replace(/\n/g, '') // Firefox bug: just adds random newlines for fun apparently???
|
||||
.replace(new RegExp(`</?(?:${HTML_BLOCK_TAGS.join('|')})\\b[^>]*>`, 'gi'), '\n')
|
||||
.replace(/\n{2,}/g, '\n')
|
||||
.replace(/<br\b[^>]*>\n?/gi, '\n')
|
||||
|
||||
@@ -157,14 +157,15 @@ export class BattleRoom extends ChatRoom {
|
||||
overlayActive: 'move' | 'switch' | null = null;
|
||||
|
||||
override interruptClose(explicit?: boolean, elem?: HTMLElement | null) {
|
||||
const battle = this.battle;
|
||||
const activeBattle = battle && !battle.ended && this.request && this.connectMode !== 'deleted';
|
||||
if (activeBattle || this.requireForfeit) {
|
||||
if (this.isPlaying() || this.requireForfeit) {
|
||||
PS.join('forfeitbattle' as RoomID, { parentElem: elem, parentRoomid: this.id });
|
||||
return `You are still in ${this.title}`;
|
||||
}
|
||||
return super.interruptClose(explicit, elem);
|
||||
}
|
||||
isPlaying() {
|
||||
return this.battle && !this.battle.ended && this.request && this.connectMode !== 'deleted';
|
||||
}
|
||||
|
||||
override handleReconnect(): boolean | void {
|
||||
if (this.battle) {
|
||||
@@ -1172,9 +1173,7 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
|
||||
return <div class="inline-controls">
|
||||
<div class="whatdo">
|
||||
{this.renderOldChoices(request, choices)}
|
||||
</div>
|
||||
<div class="pad">
|
||||
{choices.noCancel || room.battle.hardcoreMode ?
|
||||
<em>Waiting for opponent...</em> {choices.noCancel || room.battle.hardcoreMode ?
|
||||
null : <button data-cmd="/cancel" class="button">Cancel</button>}
|
||||
</div>
|
||||
{this.renderTeamList()}
|
||||
|
||||
@@ -281,12 +281,15 @@ export class ChatRoom extends PSRoom {
|
||||
this.highlightRegExp[i] = new RegExp('(?:\\b|(?!\\w))(?:' + highlights[i].join('|') + ')(?:\\b|(?!\\w))', 'i');
|
||||
}
|
||||
}
|
||||
static isHighlightableChatMessage(message: string) {
|
||||
static isHighlightableChatMessage(message: string, isDM = false) {
|
||||
if (!message.startsWith('/')) return true;
|
||||
const [cmd] = PSUtils.splitFirst(message.slice(1), ' ');
|
||||
if (['raw', 'nonotify', 'text', 'error'].includes(cmd)) {
|
||||
return false;
|
||||
}
|
||||
if (['uhtml', 'uhtmlchange'].includes(cmd)) {
|
||||
return isDM;
|
||||
}
|
||||
if (cmd === 'subtlenotify') {
|
||||
return 'subtle';
|
||||
}
|
||||
@@ -332,7 +335,8 @@ export class ChatRoom extends PSRoom {
|
||||
if (!message) return false;
|
||||
if (userid === PS.user.userid) return false;
|
||||
|
||||
const highlightType = ChatRoom.isHighlightableChatMessage(message);
|
||||
const isDM = this.id.startsWith("dm-");
|
||||
const highlightType = ChatRoom.isHighlightableChatMessage(message, isDM);
|
||||
const isIgnored = PS.prefs.ignore?.[userid];
|
||||
if (isIgnored || !highlightType) return false;
|
||||
if (highlightType === 'subtle') {
|
||||
@@ -340,7 +344,7 @@ export class ChatRoom extends PSRoom {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.id.startsWith("dm-")) {
|
||||
if (isDM) {
|
||||
this.notify({
|
||||
title: `${this.title}`,
|
||||
body: this.getChatNotificationBody(message),
|
||||
@@ -1630,8 +1634,8 @@ export class PSTextarea extends preact.Component<{
|
||||
const textboxTest = this.base!.querySelector<HTMLTextAreaElement>('textarea.heighttester')!;
|
||||
textboxTest.style.width = `${textbox.offsetWidth}px`;
|
||||
textboxTest.value = textbox.value;
|
||||
const newHeight = Math.max(textboxTest.scrollHeight + 40, 50);
|
||||
textbox.style.height = `${newHeight}px`;
|
||||
// +2 for the borders
|
||||
textbox.style.height = `${textboxTest.scrollHeight + 2}px`;
|
||||
};
|
||||
handleInput = (e: Event) => {
|
||||
if (this.props.singleLine) {
|
||||
@@ -1673,7 +1677,7 @@ export class PSTextarea extends preact.Component<{
|
||||
/>
|
||||
{!this.cssAutosize && <div><textarea
|
||||
class={`${className} heighttester`}
|
||||
style="visibility:hidden;position:absolute;left:-200px"
|
||||
style="visibility:hidden;position:absolute;left:-200px;height:10px;overflow-y:hidden"
|
||||
/></div>}
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -414,10 +414,6 @@ export class MainMenuRoom extends PSRoom {
|
||||
case 'userdetails':
|
||||
let userid = response.userid;
|
||||
fullid += ` ${userid}`;
|
||||
if (userid === PS.user.userid && response.avatar !== undefined) {
|
||||
PS.user.avatar = `${response.avatar}`;
|
||||
PS.user.update(null);
|
||||
}
|
||||
let userdetails = this.userdetailsCache[userid];
|
||||
if (!userdetails) {
|
||||
this.userdetailsCache[userid] = response;
|
||||
@@ -425,6 +421,12 @@ export class MainMenuRoom extends PSRoom {
|
||||
response.status ||= '';
|
||||
Object.assign(userdetails, response);
|
||||
}
|
||||
if (userid === PS.user.userid) {
|
||||
if (response.avatar !== undefined && PS.user.avatar !== `${response.avatar}`) {
|
||||
PS.user.avatar = `${response.avatar}`;
|
||||
PS.user.update(null);
|
||||
}
|
||||
}
|
||||
PS.rooms[`user-${userid}`]?.update(null);
|
||||
PS.rooms[`viewuser-${userid}`]?.update(null);
|
||||
PS.rooms[`users`]?.update(null);
|
||||
|
||||
@@ -49,9 +49,6 @@ export class UserRoom extends PSRoom {
|
||||
|
||||
export class StatusEditor extends preact.Component {
|
||||
declare state: { addingStatus?: boolean, statusChanged?: boolean };
|
||||
override componentDidMount() {
|
||||
this.fetchStatus();
|
||||
}
|
||||
fetchStatus() {
|
||||
if (!PS.user.userid) return;
|
||||
PS.mainmenu.makeQuery('userdetails', PS.user.userid).then(() => this.forceUpdate());
|
||||
@@ -680,6 +677,7 @@ class OptionsPanel extends PSRoomPanel {
|
||||
override componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.subscribeTo(PS.user);
|
||||
PS.mainmenu.makeQuery('userdetails', PS.user.userid).then(() => this.forceUpdate());
|
||||
}
|
||||
setTheme = (e: Event) => {
|
||||
const theme = (e.currentTarget as HTMLSelectElement).value as 'light' | 'dark' | 'system';
|
||||
@@ -1724,8 +1722,8 @@ class BattleOptionsPanel extends PSRoomPanel {
|
||||
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 === '~');
|
||||
const isPlaying = !!battleRoom?.isPlaying();
|
||||
const canOfferTie = battleRoom && ((battleRoom.battle.turn >= 100 && isPlaying) || PS.user.group === '~');
|
||||
const sideBySideDisabled = !!battleRoom && battleRoom.width < 500;
|
||||
let automaticLayout: BattleLayoutPreference | null = null;
|
||||
if (battleRoom) {
|
||||
@@ -1735,6 +1733,14 @@ class BattleOptionsPanel extends PSRoomPanel {
|
||||
return <PSPanelWrapper room={room} width={380}><div class="pad">
|
||||
{battleRoom && <>
|
||||
<p><strong>In this battle</strong></p>
|
||||
<p class="buttonbar">
|
||||
<button data-cmd="/closeand /inopener /forfeit" class="button" disabled={!isPlaying}>
|
||||
Forfeit
|
||||
</button> {}
|
||||
<button data-cmd="/closeand /inopener /offertie" class="button" disabled={!canOfferTie}>
|
||||
Offer tie {!canOfferTie && "(turn 100+)"}
|
||||
</button>
|
||||
</p>
|
||||
<p>
|
||||
<label class="checkbox">
|
||||
<input
|
||||
@@ -1842,7 +1848,7 @@ class BattleOptionsPanel extends PSRoomPanel {
|
||||
<input
|
||||
name="autohardcore" checked={PS.prefs.autohardcore || false}
|
||||
type="checkbox" onChange={this.handleAllSettings}
|
||||
/> Automatically enable hardcore mode
|
||||
/> <abbr title="Disable quality-of-life features not available in official games">Hardcore mode</abbr>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
@@ -1863,9 +1869,6 @@ class BattleOptionsPanel extends PSRoomPanel {
|
||||
</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>;
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "./client-main";
|
||||
import { PSView } from "./panels";
|
||||
import type { Battle } from "./battle";
|
||||
import { Dex } from "./battle-dex";
|
||||
import { BattleLog } from "./battle-log"; // optional
|
||||
|
||||
window.addEventListener('dragover', e => {
|
||||
@@ -23,6 +24,16 @@ window.addEventListener('dragover', e => {
|
||||
});
|
||||
|
||||
export class PSHeader extends preact.Component {
|
||||
faviconNotifying = false;
|
||||
updateFavicon = () => {
|
||||
const notifying = Object.values(PS.rooms).some(room => room?.notifications.length);
|
||||
if (notifying === this.faviconNotifying) return;
|
||||
|
||||
const favicon = document.querySelector<HTMLLinkElement>('#dynamic-favicon');
|
||||
if (!favicon) return;
|
||||
favicon.href = `${Dex.resourcePrefix}${notifying ? 'favicon-notify.ico' : 'favicon.ico'}`;
|
||||
this.faviconNotifying = notifying;
|
||||
};
|
||||
static toggleMute = (e: Event) => {
|
||||
PS.prefs.set('mute', !PS.prefs.mute);
|
||||
PS.update();
|
||||
@@ -205,9 +216,11 @@ export class PSHeader extends preact.Component {
|
||||
});
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
this.handleResize();
|
||||
this.updateFavicon();
|
||||
}
|
||||
override componentDidUpdate() {
|
||||
this.handleResize();
|
||||
this.updateFavicon();
|
||||
}
|
||||
renderUser() {
|
||||
if (!PS.connection?.connected) {
|
||||
|
||||
@@ -177,7 +177,9 @@ select.base-select::picker-icon {
|
||||
}
|
||||
|
||||
.button option,
|
||||
.button optgroup {
|
||||
.button optgroup,
|
||||
.select option,
|
||||
.select optgroup {
|
||||
background: #f6f6f6;
|
||||
color: #222222;
|
||||
}
|
||||
@@ -283,7 +285,9 @@ select.base-select::picker-icon {
|
||||
box-shadow: 0.5px 1px 2px rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
.dark .button option,
|
||||
.dark .button optgroup {
|
||||
.dark .button optgroup,
|
||||
.dark .select option,
|
||||
.dark .select optgroup {
|
||||
/* Chrome/Win does this thing where it takes <select> styling and applies it
|
||||
to options, which would be cool except it of course can't handle gradient
|
||||
backgrounds, and also can't fall back to the last non-gradient background,
|
||||
@@ -487,8 +491,8 @@ pre.textbox {
|
||||
}
|
||||
.dark .textbox,
|
||||
.dark .textbox-subtle[data-changed="1"],
|
||||
textarea.textbox-subtle:empty,
|
||||
input.textbox-subtle[value=""] {
|
||||
.dark textarea.textbox-subtle:empty,
|
||||
.dark input.textbox-subtle[value=""] {
|
||||
border-color: #6b7178;
|
||||
|
||||
/* box-shadow: inset 0px -1px 2px #606060, -1px -1px 0 rgba(255,255,255,.2); */
|
||||
|
||||
@@ -5,7 +5,7 @@ License: GPLv2
|
||||
|
||||
*/
|
||||
|
||||
@import url(./battle-log.css?v14b12);
|
||||
@import url(./battle-log.css?v15.1);
|
||||
|
||||
.battle {
|
||||
position: absolute;
|
||||
@@ -354,7 +354,7 @@ License: GPLv2
|
||||
.teamicons span {
|
||||
float: left;
|
||||
}
|
||||
.leftbar .trainer div.trainersprite,
|
||||
.leftbar .trainer div.trainersprite:not(.noflip),
|
||||
.leftbar .teamicons span {
|
||||
-moz-transform: scaleX(-1);
|
||||
-webkit-transform: scaleX(-1);
|
||||
|
||||
@@ -418,12 +418,15 @@
|
||||
height: 13px;
|
||||
border-radius: 9px;
|
||||
font: 9px Helvetica, Arial, sans-serif;
|
||||
/* text-shadow: none; */
|
||||
text-shadow: 0 1px 0 black;
|
||||
text-align: center;
|
||||
padding: 2px 4px 0 4px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
|
||||
text-shadow: none;
|
||||
/* text-shadow: 0 1px 0 black; */
|
||||
-webkit-text-stroke: 2px rgba(0, 0, 0, 0.4);
|
||||
paint-order: stroke fill;
|
||||
}
|
||||
.typetile.tera, .typeicon.tera {
|
||||
--s: 3px; /* spike depth */
|
||||
|
||||
Reference in New Issue
Block a user