feedback7 UIBACK: a 设置 button left of 玩法说明 in the lobby's and the room's top bar (GitHub #238 by RushFTK) — ui/settings.js SettingsButton opens the same settings modal as the title screen and the match; no background-music option and no board-style option (the owner's decision of 2026-10-07: neither is official); test/ui/settings-entry.e2e.test.js; golden: unchanged (client only)
This commit is contained in:
1 parent
df540b6210
commit
911e4bff2b
6 files changed
+111
-2
No files matched your search
+1
-1
@@ -170,4 +170,4 @@
|
||||
|
||||
刷新、冻结、升级、撤退、出售和准备只在休整期有效,撤退 / 出售要先点选一名干员。方向轮盘(方向键预览、`Enter` 确认、`Esc` 取消)、「玩法说明」和「干员调配」的翻页键是这些界面自己的按键,不能修改。
|
||||
|
||||
**修改快捷键**:标题画面右上角或对局左下角的「设置」→「快捷键」。点一下要改的按键(只用键盘时按 Tab 移到它上面再按 Enter),出现「按下新按键…」后按新的键;按 `Esc` 取消,设置窗口不会关闭。可以用字母、数字、空格、标点符号键和 Delete / Insert / Home / End / PageUp / PageDown;`Esc`、`Tab`、`Enter`、方向键和 F1–F12 留给界面使用,也不能搭配 Ctrl / Alt / ⌘。新按键已被另一个快捷键使用时两者互换,列表下方会写明(例如把「撤退选中干员」改成 `X`,「出售选中干员」就换成原来的 `Q`)。「恢复默认」改回上表的按键。修改立即生效,商店栏、准备按钮和单位底部「撤退 / 出售」按钮上显示的按键也跟着变;设置和音量等一起保存在当前浏览器里(换浏览器或清除网站数据后回到默认)。按键按键帽上印的字符识别(AZERTY 等键盘按印着的字母),俄文等非拉丁布局按键位识别。手机等纯触屏设备要连接键盘才能用快捷键,设置里的快捷键列表默认折叠。
|
||||
**修改快捷键**:标题画面的「设置」按钮、大厅和房间顶栏的「设置」(玩法说明左边)或对局左下角的「设置」→「快捷键」。点一下要改的按键(只用键盘时按 Tab 移到它上面再按 Enter),出现「按下新按键…」后按新的键;按 `Esc` 取消,设置窗口不会关闭。可以用字母、数字、空格、标点符号键和 Delete / Insert / Home / End / PageUp / PageDown;`Esc`、`Tab`、`Enter`、方向键和 F1–F12 留给界面使用,也不能搭配 Ctrl / Alt / ⌘。新按键已被另一个快捷键使用时两者互换,列表下方会写明(例如把「撤退选中干员」改成 `X`,「出售选中干员」就换成原来的 `Q`)。「恢复默认」改回上表的按键。修改立即生效,商店栏、准备按钮和单位底部「撤退 / 出售」按钮上显示的按键也跟着变;设置和音量等一起保存在当前浏览器里(换浏览器或清除网站数据后回到默认)。按键按键帽上印的字符识别(AZERTY 等键盘按印着的字母),俄文等非拉丁布局按键位识别。手机等纯触屏设备要连接键盘才能用快捷键,设置里的快捷键列表默认折叠。
|
||||
@@ -501,6 +501,10 @@
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* ---- settings modal ------------------------------------------------------------------------------------------------ */
|
||||
/* the 设置 trigger of the lobby and the room (ui/settings.js SettingsButton, GitHub #238): the 玩法说明 button's twin; its gear sits
|
||||
inside the label, so the label lays glyph and text out like the button itself */
|
||||
.settings-btn .btn__label { display: inline-flex; align-items: center; gap: .1rem; }
|
||||
.settings-btn .btn__icon { color: var(--mint-500); }
|
||||
.set-list { display: flex; flex-direction: column; gap: .16rem; padding-top: .06rem; }
|
||||
.set-row { display: grid; grid-template-columns: 3.3rem 1fr auto; align-items: center; gap: .18rem; min-height: .44rem; }
|
||||
.set-row__label { display: flex; align-items: center; gap: .1rem; font-size: .18rem; color: var(--text-hi); white-space: nowrap; }
|
||||
|
||||
@@ -16,6 +16,7 @@ import { DIFFICULTIES, DIFFICULTY_NAMES, DIFFICULTY_COLORS, ROOM_CODE_LEN, MAX_S
|
||||
import { html, Button, Icon, MicroLabel, Panel, TextField, PingPill, AvatarFrame, Tooltip, Spinner, DifficultyIcon, doctorNo } from '../ui/components.js';
|
||||
import { toast, toastError } from '../ui/toasts.js';
|
||||
import { GuideButton } from '../ui/guide.js';
|
||||
import { SettingsButton } from '../ui/settings.js';
|
||||
import { LoadoutButton } from './loadout.js';
|
||||
import { net, identity } from '../net.js';
|
||||
import { store, useStore, shallowEqual, loadPref, savePref } from '../store.js';
|
||||
@@ -311,6 +312,7 @@ export function LobbyScreen() {
|
||||
<h1 class="topbar__title">${t('选择模拟协议')}</h1>
|
||||
</div>
|
||||
<div class="topbar__right">
|
||||
<${SettingsButton} class="lobby-settings" variant="secondary" label=${t('设置')} />
|
||||
<${GuideButton} class="lobby-guide" variant="secondary" label=${t('玩法说明')} />
|
||||
<${LoadoutButton} from="lobby" size="sm" class="lobby-loadout" label=${t('干员调配')} />
|
||||
<div class="me-chip">
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { toast, toastError } from '../ui/toasts.js';
|
||||
import { copyText } from '../ui/clipboard.js';
|
||||
import { GuideButton } from '../ui/guide.js';
|
||||
import { SettingsButton } from '../ui/settings.js';
|
||||
import { LoadoutButton } from './loadout.js';
|
||||
import { net } from '../net.js';
|
||||
import { store, useStore, shallowEqual, emptyMatch, isSpectating } from '../store.js';
|
||||
@@ -277,6 +278,7 @@ export function RoomScreen() {
|
||||
<${PingPill} ms=${conn.ping} online=${online} />
|
||||
<${MicroLabel}>${t('当前延迟')}<//>
|
||||
</div>
|
||||
<${SettingsButton} class="room-settings" variant="secondary" label=${t('设置')} />
|
||||
<${GuideButton} class="room-guide" variant="secondary" label=${t('玩法说明')} />
|
||||
</div>
|
||||
<div class="topbar__center">
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
// the settings modal — which also holds the language switch (ui/lang.js; kept apart in `sp.pref.lang`; under it a note
|
||||
// while the current language's pack is a machine translation, `_meta.machineTranslated`) and the 快捷键
|
||||
// section that rebinds the in-match shortcuts (the key map: ui/gameLogic/shortcuts.js; the community request
|
||||
// 「快捷键可不可以自己设置」, the owner's decision of 2026-10-07).
|
||||
// 「快捷键可不可以自己设置」, the owner's decision of 2026-10-07). The lobby and the room open it from a 设置 button next to
|
||||
// 玩法说明 (SettingsButton, GitHub #238); the title screen and the match have their own gear.
|
||||
|
||||
import { useLayoutEffect, useState } from '../../vendor/hooks.module.js';
|
||||
import { html, Modal, Button, Icon, MicroLabel } from './components.js';
|
||||
import { GIcon } from './gameComponents.js';
|
||||
import { createStore, useStore, loadPref, savePref } from '../store.js';
|
||||
import { sanitizeSettings, HOTKEY_ACTIONS, DEFAULT_HOTKEYS, hotkeyLabel, rebindHotkey, isDefaultHotkeys, captureHotkey } from './gameLogic.js';
|
||||
import { audio } from '../audio.js';
|
||||
@@ -180,3 +182,16 @@ export function SettingsModal({ open, onClose }) {
|
||||
</div>
|
||||
<//>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The 设置 button of the lobby and the room (GitHub #238 — before it the settings were reachable only from the title
|
||||
* screen and a running match): the twin of the 玩法说明 button (ui/guide.js GuideButton), with the settings modal behind it
|
||||
* (mounted only while open). The same modal as the title screen's and the match's: nothing in it is match-only.
|
||||
* @param {{ class?: string, size?: 'sm'|'md'|'lg'|'xl', variant?: string, label?: string }} props
|
||||
*/
|
||||
export function SettingsButton({ class: cls, size = 'sm', variant = 'ghost', label = t('设置') }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
return html`<${Button} variant=${variant} size=${size} class=${cx('settings-btn', cls)} onClick=${() => setOpen(true)}
|
||||
title=${t('设置')} aria-label=${t('设置')} data-testid="settings-btn"><${GIcon} name="gear" class="btn__icon" />${label}<//>
|
||||
${open ? html`<${SettingsModal} open=${true} onClose=${() => setOpen(false)} />` : null}`;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Browser check of the settings entry of the lobby and the room (GitHub #238 — 设置 was reachable only from the title
|
||||
// screen and a running match; opt-in, ~15 s):
|
||||
// SP_E2E=1 CHROME_PATH=… node --test test/ui/settings-entry.e2e.test.js
|
||||
//
|
||||
// After 开始 the lobby's top bar carries a 设置 button left of 玩法说明 (ui/settings.js SettingsButton), and so does the
|
||||
// room's after 创建同盟; each opens the very settings modal of the title screen (language, sound, 快捷键 …) and closes
|
||||
// with Esc or 完成, leaving the screen as it was. No console / page errors. The modal itself and its contents are covered
|
||||
// by the title screen's tests; this file is about the entry. Not in the modal, deliberately: background music in a
|
||||
// hidden tab and the board style (the owner's decision of 2026-10-07 on #238).
|
||||
|
||||
import { test, describe, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync } from 'node:fs';
|
||||
|
||||
const CHROME = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
||||
const ENABLED = process.env.SP_E2E === '1' && existsSync(CHROME);
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
describe('the settings entry of the lobby and the room', { skip: !ENABLED && 'set SP_E2E=1 (and have Chrome) to run' }, () => {
|
||||
let srv;
|
||||
let browser;
|
||||
let base;
|
||||
before(async () => {
|
||||
const { startServer } = await import('../../server/index.js');
|
||||
const puppeteer = (await import('puppeteer-core')).default;
|
||||
srv = await startServer({ port: 0, host: '127.0.0.1', quiet: true });
|
||||
base = `http://127.0.0.1:${srv.port}`;
|
||||
browser = await puppeteer.launch({ executablePath: CHROME, headless: true, args: ['--no-sandbox', '--force-device-scale-factor=1'] });
|
||||
});
|
||||
after(async () => {
|
||||
await browser?.close();
|
||||
await srv?.close();
|
||||
});
|
||||
|
||||
/** Is the 设置 button the sibling right before the 玩法说明 button of `screen`'s top bar? */
|
||||
const settingsBeforeGuide = (page, screen, guideClass) => page.evaluate((s, g) => {
|
||||
const btn = document.querySelector(`${s} [data-testid="settings-btn"]`);
|
||||
const guide = document.querySelector(`${s} .${g}`);
|
||||
return !!btn && !!guide && btn.nextElementSibling === guide && /设置/.test(btn.textContent) && btn.querySelector('svg.btn__icon') != null;
|
||||
}, screen, guideClass);
|
||||
const modalText = (page) => page.evaluate(() => document.querySelector('.modal')?.textContent.replace(/\s+/g, ' ').trim() ?? null);
|
||||
|
||||
test('the lobby: 设置 left of 玩法说明 opens the settings modal; Esc closes it; the room has the same entry', async () => {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1600, height: 900 });
|
||||
const problems = [];
|
||||
page.on('console', (m) => { if (m.type() === 'error') problems.push(`console: ${m.text()}`); });
|
||||
page.on('pageerror', (e) => problems.push(`pageerror: ${e.message}`));
|
||||
await page.goto(`${base}/`, { waitUntil: 'networkidle0', timeout: 60000 });
|
||||
await page.waitForSelector('input', { timeout: 15000 });
|
||||
await page.type('input', '测试博士');
|
||||
await page.evaluate(() => [...document.querySelectorAll('button')].find((b) => /开始/.test(b.textContent))?.click());
|
||||
await page.waitForSelector('.lobby-screen', { timeout: 15000 });
|
||||
await sleep(500);
|
||||
|
||||
assert.ok(await settingsBeforeGuide(page, '.lobby-screen .topbar__right', 'lobby-guide'), '设置 sits right before 玩法说明 in the lobby\'s top bar');
|
||||
assert.equal(await modalText(page), null, 'closed until asked');
|
||||
await page.click('.lobby-screen [data-testid="settings-btn"]');
|
||||
await page.waitForSelector('.modal', { timeout: 5000 });
|
||||
const text = await modalText(page);
|
||||
assert.match(text, /设置/);
|
||||
assert.match(text, /背景音乐/);
|
||||
assert.match(text, /快捷键/);
|
||||
assert.doesNotMatch(text, /棋盘/, 'no board-style option (declined)');
|
||||
assert.doesNotMatch(text, /后台/, 'no background-music option (declined)');
|
||||
await page.keyboard.press('Escape');
|
||||
await sleep(300);
|
||||
assert.equal(await modalText(page), null, 'Esc closes it');
|
||||
assert.ok(await page.$('.lobby-screen'), 'the lobby is still there');
|
||||
|
||||
// the room: 创建同盟 → the same button, closed with 完成
|
||||
await page.evaluate(() => [...document.querySelectorAll('button')].find((b) => /创建同盟/.test(b.textContent))?.click());
|
||||
await page.waitForSelector('.room-screen', { timeout: 15000 });
|
||||
await sleep(500);
|
||||
assert.ok(await settingsBeforeGuide(page, '.room-screen .topbar__left', 'room-guide'), '设置 sits right before 玩法说明 in the room\'s top bar');
|
||||
await page.click('.room-screen [data-testid="settings-btn"]');
|
||||
await page.waitForSelector('.modal', { timeout: 5000 });
|
||||
assert.match(await modalText(page), /背景音乐/);
|
||||
await page.evaluate(() => [...document.querySelectorAll('.modal button')].find((b) => /完成/.test(b.textContent))?.click());
|
||||
await sleep(300);
|
||||
assert.equal(await modalText(page), null, '完成 closes it');
|
||||
assert.ok(await page.$('.room-screen'), 'the room is still there');
|
||||
assert.deepEqual(problems, []);
|
||||
await page.close();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user