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:
Guangcong Luo
2026-07-18 23:07:09 +00:00
parent 082b834351
commit 4d88e1df71
14 changed files with 85 additions and 37 deletions

View File

@@ -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>`
);
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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 = '';

View File

@@ -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);

View File

@@ -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')

View File

@@ -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()}

View File

@@ -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>;
}

View File

@@ -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);

View File

@@ -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>;

View File

@@ -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) {

View File

@@ -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); */

View File

@@ -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);

View File

@@ -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 */