From 0eeed0d7ce52ae6fb97ce2f0b496796a4c2f7ec9 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Thu, 10 Apr 2025 09:15:12 +0000 Subject: [PATCH] Preact: Add vertical header layout for mobile It's finally here and it's beautiful! I've had this design in my mind for around ten years, ever since I first started working on a Preact client. The idea is that on small screens, you'd just scroll left to get to the menu, instead of needing to press the menu button. This turned out to work surprisingly well and I'm pretty happy with it. This also exposed a number of bugs. It turns out most of our popups are not written with horizontal scrolling in mind, so I had to fix popup rooms as well as BattleTooltips. There's still plenty of improvements possible, but I'm already really happy with what I've done so far. --- .../preactalpha.template.html | 6 +- .../src/battle-tooltips.ts | 9 +- play.pokemonshowdown.com/src/client-main.ts | 92 +++++++--- play.pokemonshowdown.com/src/panel-battle.tsx | 59 +++++-- play.pokemonshowdown.com/src/panel-chat.tsx | 6 +- .../src/panel-mainmenu.tsx | 4 +- play.pokemonshowdown.com/src/panel-rooms.tsx | 2 +- play.pokemonshowdown.com/src/panel-topbar.tsx | 92 ++++++++-- play.pokemonshowdown.com/src/panels.tsx | 166 ++++++++++-------- play.pokemonshowdown.com/style/client2.css | 158 +++++++++++++---- play.pokemonshowdown.com/testclient-beta.html | 4 +- 11 files changed, 425 insertions(+), 173 deletions(-) diff --git a/play.pokemonshowdown.com/preactalpha.template.html b/play.pokemonshowdown.com/preactalpha.template.html index ee20a7daa..af1623907 100644 --- a/play.pokemonshowdown.com/preactalpha.template.html +++ b/play.pokemonshowdown.com/preactalpha.template.html @@ -1,4 +1,4 @@ - + - + Showdown! @@ -104,7 +104,7 @@ https://psim.us/dev - + diff --git a/play.pokemonshowdown.com/src/battle-tooltips.ts b/play.pokemonshowdown.com/src/battle-tooltips.ts index 0463fb21c..2663baf94 100644 --- a/play.pokemonshowdown.com/src/battle-tooltips.ts +++ b/play.pokemonshowdown.com/src/battle-tooltips.ts @@ -419,7 +419,7 @@ export class BattleTooltips { $wrapper.removeClass('tooltip-locked'); } $wrapper.css({ - left: x, + left: Math.min(x, document.documentElement.clientWidth - 400), top: y, }); innerHTML = `
${innerHTML}
`; @@ -448,8 +448,11 @@ export class BattleTooltips { } let width = $(BattleTooltips.elem).outerWidth()!; - if (x > document.documentElement.clientWidth - width - 2) { - x = document.documentElement.clientWidth - width - 2; + const availableWidth = document.documentElement.clientWidth + window.scrollX; + if (x > availableWidth - width - 2) { + x = availableWidth - width - 2; + $wrapper.css('left', x); + } else if (x > document.documentElement.clientWidth - 400) { $wrapper.css('left', x); } diff --git a/play.pokemonshowdown.com/src/client-main.ts b/play.pokemonshowdown.com/src/client-main.ts index f2bd91f2c..370ad74a4 100644 --- a/play.pokemonshowdown.com/src/client-main.ts +++ b/play.pokemonshowdown.com/src/client-main.ts @@ -882,21 +882,21 @@ export const PS = new class extends PSModel { panel: PSRoom = null!; /** * Not to be confused with PSPrefs.onepanel, which is permanent. - * PS.onePanelMode will be true if one-panel mode is on, but it will - * also be true if the right panel is temporarily hidden (by opening - * the Rooms panel and clicking "Hide") + * Normally tracks PSPrefs.onepanel, but will become 'vertical' + * at low widths. * * Will NOT be true if only one panel fits onto the screen at the * moment, but resizing will display multiple panels – for that, * check `PS.leftRoomWidth === 0` */ - onePanelMode = false; + onePanelMode: boolean | 'vertical' = false; /** - * 0 = only one panel visible. + * * 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. */ - leftPanelWidth = 0; + leftPanelWidth: number | null = 0; mainmenu: MainMenuRoom = null!; /** @@ -988,21 +988,25 @@ export const PS = new class extends PSModel { }; } updateLayout(alreadyUpdating?: boolean) { - const leftRoomWidth = this.calculateLeftRoomWidth(); - let roomHeight = document.body.offsetHeight - 56; - let totalWidth = document.body.offsetWidth; - if (leftRoomWidth) { - this.leftPanel.width = leftRoomWidth; + const leftPanelWidth = this.calculateLeftPanelWidth(); + const totalWidth = document.body.offsetWidth; + const totalHeight = document.body.offsetHeight; + const roomHeight = totalHeight - 56; + if (leftPanelWidth === null) { + this.panel.width = totalWidth - 200; + this.panel.height = totalHeight; + } else if (leftPanelWidth) { + this.leftPanel.width = leftPanelWidth; this.leftPanel.height = roomHeight; - this.rightPanel!.width = totalWidth + 1 - leftRoomWidth; + this.rightPanel!.width = totalWidth + 1 - leftPanelWidth; this.rightPanel!.height = roomHeight; } else { this.panel.width = totalWidth; this.panel.height = roomHeight; } - if (this.leftPanelWidth !== leftRoomWidth) { - this.leftPanelWidth = leftRoomWidth; + if (this.leftPanelWidth !== leftPanelWidth) { + this.leftPanelWidth = leftPanelWidth; if (!alreadyUpdating) this.update(true); } } @@ -1119,7 +1123,7 @@ export const PS = new class extends PSModel { this.connection.send(fullMsg); } isVisible(room: PSRoom) { - if (this.leftPanelWidth === 0) { + if (!this.leftPanelWidth) { // one panel visible return room === this.panel; } else { @@ -1127,7 +1131,11 @@ export const PS = new class extends PSModel { return room === this.rightPanel || room === this.leftPanel; } } - calculateLeftRoomWidth() { + calculateLeftPanelWidth() { + const available = document.body.offsetWidth; + if (available < 800) { + return null; + } // If we don't have both a left room and a right room, obviously // just show one room if (!this.leftPanel || !this.rightPanel || this.onePanelMode) { @@ -1139,7 +1147,6 @@ export const PS = new class extends PSModel { const left = this.getWidthFor(this.leftPanel); const right = this.getWidthFor(this.rightPanel); - const available = document.body.offsetWidth; let excess = available - (left.width + right.width); if (excess >= 0) { @@ -1252,12 +1259,12 @@ export const PS = new class extends PSModel { if (!this.isVisible(room)) { room.hiddenInit = true; } - if (room.location === 'left') { - this.leftPanel = this.panel = room; - while (this.popups.length) this.leave(this.popups.pop()!); - this.room = room; - } else if (room.location === 'right') { - this.rightPanel = this.panel = room; + if (PS.isNormalRoom(room)) { + if (room.location === 'right') { + this.rightPanel = this.panel = room; + } else { + this.leftPanel = this.panel = room; + } while (this.popups.length) this.leave(this.popups.pop()!); this.room = room; } else { // popup or mini-window @@ -1272,6 +1279,9 @@ export const PS = new class extends PSModel { return true; } horizontalNav(room = this.room) { + if (this.leftPanelWidth === null) { + return { rooms: [], index: -1 }; + } const rooms = this.leftRoomList.concat(this.rightRoomList); const miniRoom = this.miniRoomList[0] !== 'news' ? this.miniRoomList[0] : null; if (miniRoom) rooms.splice(1, 0, miniRoom); @@ -1282,6 +1292,11 @@ export const PS = new class extends PSModel { return { rooms, index }; } verticalNav(room = this.room) { + if (this.leftPanelWidth === null) { + const rooms = ['' as RoomID, ...this.miniRoomList, ...this.leftRoomList.slice(1), ...this.rightRoomList]; + const index = rooms.indexOf(room.id); + return { rooms, index }; + } if (room.location !== 'mini-window') { return { rooms: [], index: -1 }; } @@ -1354,9 +1369,11 @@ export const PS = new class extends PSModel { if (!upRoom && !downRoom) return ''; let buf = ' '; - if (upRoom) buf += `Alt+\u2191 ${upRoom.title}`; + // const altLabel = navigator.platform?.startsWith('Mac') ? '⌥' : 'ᴀʟᴛ'; + const altLabel = navigator.platform?.startsWith('Mac') ? 'ᴏᴘᴛ' : 'ᴀʟᴛ'; + if (upRoom) buf += `${altLabel}\u2191 ${upRoom.title}`; buf += " | "; - if (downRoom) buf += `${downRoom.title} Alt+\u2193`; + if (downRoom) buf += `${altLabel}\u2193 ${downRoom.title}`; return buf; } @@ -1441,11 +1458,11 @@ export const PS = new class extends PSModel { } } roomVisible(room: PSRoom): boolean { - if (room.location === 'left' || room.location === 'right') { - return this.leftPanelWidth === 0 ? room === this.panel : room === this.leftPanel || room === this.rightPanel; + if (PS.isNormalRoom(room)) { + return !this.leftPanelWidth ? room === this.panel : room === this.leftPanel || room === this.rightPanel; } if (room.location === 'mini-window') { - return this.leftPanelWidth === 0 ? this.mainmenu === this.panel : this.mainmenu === this.leftPanel; + return !this.leftPanelWidth ? this.mainmenu === this.panel : this.mainmenu === this.leftPanel; } // some kind of popup return true; @@ -1470,6 +1487,15 @@ export const PS = new class extends PSModel { this.update(); } + isPopup(room: PSRoom | undefined | null) { + if (!room) return false; + return room.location === 'popup' || room.location === 'semimodal-popup' || room.location === 'modal-popup'; + } + isNormalRoom(room: PSRoom | undefined | null) { + if (!room) return false; + return room.location === 'left' || room.location === 'right' || + (room.location === 'mini-window' && PS.leftPanelWidth === null); + } moveRoom(room: PSRoom, location: PSRoomLocation, background?: boolean, index?: number) { if (room.location === location && index === undefined) { if (background === true) { @@ -1486,7 +1512,7 @@ export const PS = new class extends PSModel { return; } const POPUPS = ['popup', 'semimodal-popup', 'modal-popup']; - if (POPUPS.includes(room.location) && POPUPS.includes(location)) { + if (this.isPopup(room) && POPUPS.includes(location)) { room.location = location; return; } @@ -1548,6 +1574,7 @@ export const PS = new class extends PSModel { if (location === 'left') this.leftPanel = this.panel = room; if (location === 'right') this.rightPanel = this.panel = room; if (location === 'mini-window') this.leftPanel = this.panel = this.mainmenu; + if (location === 'mini-window' && PS.leftPanelWidth === null) this.leftPanel = this.panel = room; this.room = room; } } @@ -1599,6 +1626,13 @@ export const PS = new class extends PSModel { this.leave(this.popups[this.popups.length - 1]); if (!skipUpdate) this.update(); } + closeAllPopups(skipUpdate?: boolean) { + // this is more sensibly a while-loop, but I don't want accidental infinite loops + for (let i = this.popups.length - 1; i >= 0; i--) { + this.leave(this.popups[i]); + } + if (!skipUpdate) this.update(); + } join(roomid: RoomID, options?: Partial | null, noFocus?: boolean) { if (this.room.id === roomid) return; if (PS.rooms[roomid]) { diff --git a/play.pokemonshowdown.com/src/panel-battle.tsx b/play.pokemonshowdown.com/src/panel-battle.tsx index b79e8f2ba..3185c91d3 100644 --- a/play.pokemonshowdown.com/src/panel-battle.tsx +++ b/play.pokemonshowdown.com/src/panel-battle.tsx @@ -311,19 +311,35 @@ class BattlePanel extends PSRoomPanel { override componentDidMount() { const room = this.props.room; const $elem = $(this.base!); - const battle = room.battle || new Battle({ + const battle = (room.battle ||= new Battle({ + id: room.id as any, $frame: $elem.find('.battle'), $logFrame: $elem.find('.battle-log'), log: room.backlog?.map(args => '|' + args.join('|')), - }); + })); + const scene = battle.scene as BattleScene; room.backlog = null; - room.battle ||= battle; - room.log ||= battle.scene.log; - (battle.scene as BattleScene).tooltips.listen($elem.find('.battle-controls')); + room.log ||= scene.log; + scene.tooltips.listen($elem.find('.battle-controls-container')); + scene.tooltips.listen(scene.log.elem); super.componentDidMount(); battle.seekTurn(Infinity); battle.subscribe(() => this.forceUpdate()); } + battleHeight = 360; + updateLayout() { + if (!this.base) return; + const room = this.props.room; + const width = this.base.offsetWidth; + if (width && width < 640) { + const scale = (width / 640); + room.battle?.scene.$frame!.css('transform', `scale(${scale})`); + this.battleHeight = Math.round(360 * scale); + } else { + room.battle?.scene.$frame!.css('transform', 'none'); + this.battleHeight = 360; + } + } override receiveLine(args: Args) { const room = this.props.room; switch (args[0]) { @@ -371,8 +387,8 @@ class BattlePanel extends PSRoomPanel { renderControls() { const room = this.props.room; if (!room.battle) return null; - if (room.side) { - return this.renderPlayerControls(); + if (room.side && room.request) { + return this.renderPlayerControls(room.request); } const atEnd = room.battle.atQueueEnd; return
@@ -589,11 +605,9 @@ class BattlePanel extends PSRoomPanel { } return buf; } - renderPlayerControls() { + renderPlayerControls(request: BattleRequest) { const room = this.props.room; - const request = room.request; let choices = room.choices; - if (!request) return 'Error: Missing request'; if (!choices) return 'Error: Missing BattleChoiceBuilder'; if (choices.request !== request) { choices = new BattleChoiceBuilder(request); @@ -746,8 +760,25 @@ class BattlePanel extends PSRoomPanel { } override render() { const room = this.props.room; + this.updateLayout(); - return + if (room.width < 700) { + return + + + + + + +
+
; + } + + return { -