[优化] 添加 web 端的 xbox 翻页支持 (#639)

* xbox for web supported

* disable debug log

* vue导入ts文件问题

* 修复 package.json 依赖问题

* 更改web.yml上游

* Bump web v3.26.022400
This commit is contained in:
mobeicanyue
2026-02-26 01:30:38 +08:00
committed by GitHub
parent ea5cc4a7b3
commit 8751f62cce
25 changed files with 1636 additions and 1349 deletions
+221
View File
@@ -0,0 +1,221 @@
// =====================================================
// 类型定义
// =====================================================
interface GamepadConfig {
DEBUG: boolean;
AXIS_THRESHOLD: number;
AXIS_COOLDOWN: number;
DPAD_INDEX: {
UP: number;
DOWN: number;
LEFT: number;
RIGHT: number;
};
}
interface DPadState {
up: boolean;
down: boolean;
left: boolean;
right: boolean;
}
interface GlobalState {
lastAxisTime: number;
lastAxisDirection: number;
dpadPressed: DPadState;
}
// =====================================================
// 配置区域
// =====================================================
const CONFIG: GamepadConfig = {
DEBUG: false, // 日志总开关
AXIS_THRESHOLD: 0.7, // 摇杆触发阈值
AXIS_COOLDOWN: 300, // 摇杆触发冷却时间 (ms)
DPAD_INDEX: { // Xbox 标准映射
UP: 12,
DOWN: 13,
LEFT: 14,
RIGHT: 15
}
};
// =====================================================
// 日志工具函数
// =====================================================
function log(...args: any[]): void {
if (!CONFIG.DEBUG) return;
console.log(...args);
}
// =====================================================
// 状态记录
// =====================================================
const state: GlobalState = {
lastAxisTime: 0,
lastAxisDirection: 0,
dpadPressed: {
up: false,
down: false,
left: false,
right: false
}
};
// =====================================================
// 通用工具函数
// =====================================================
/**
* 平滑翻页函数
* @param direction 1 = 向下, -1 = 向上
*/
function scrollPage(direction: number): void {
const offset = window.innerHeight - 110;
const distance = direction === 1 ? offset : -offset;
log(direction === 1 ? "🎮 翻页:向下" : "🎮 翻页:向上");
window.scrollBy({
top: distance,
behavior: "smooth"
});
}
/**
* 章节切换函数
* @param direction 1 = 下一章, -1 = 上一章
*/
function switchChapter(direction: number): void {
const buttons = document.querySelectorAll<HTMLElement>(".read-bar .tool-icon");
if (buttons.length < 2) return;
if (direction === 1) {
log("🎮 切换章节:下一章");
buttons[1].click();
} else {
log("🎮 切换章节:上一章");
buttons[0].click();
}
}
/**
* 边沿检测函数
*/
function isPressedOnce(current: boolean, previous: boolean): boolean {
return current && !previous;
}
// =====================================================
// 摇杆处理
// =====================================================
function handleAxis(gp: Gamepad, now: number): void {
const axisY = gp.axes[1] || 0;
if (Math.abs(axisY) <= CONFIG.AXIS_THRESHOLD) {
state.lastAxisDirection = 0;
return;
}
const direction = axisY > 0 ? 1 : -1;
const cooldownPassed = now - state.lastAxisTime > CONFIG.AXIS_COOLDOWN;
const directionChanged = direction !== state.lastAxisDirection;
if (cooldownPassed || directionChanged) {
state.lastAxisTime = now;
state.lastAxisDirection = direction;
log(`🎮 摇杆触发 | axisY=${axisY.toFixed(2)}`);
scrollPage(direction);
}
}
// =====================================================
// DPad 处理
// =====================================================
function handleDPad(gp: Gamepad): void {
const indexes = CONFIG.DPAD_INDEX;
const current: DPadState = {
up: gp.buttons[indexes.UP]?.pressed || false,
down: gp.buttons[indexes.DOWN]?.pressed || false,
left: gp.buttons[indexes.LEFT]?.pressed || false,
right: gp.buttons[indexes.RIGHT]?.pressed || false
};
if (isPressedOnce(current.up, state.dpadPressed.up)) {
log("🎮 DPad 上");
scrollPage(-1);
}
if (isPressedOnce(current.down, state.dpadPressed.down)) {
log("🎮 DPad 下");
scrollPage(1);
}
if (isPressedOnce(current.left, state.dpadPressed.left)) {
log("🎮 DPad 左");
switchChapter(-1);
}
if (isPressedOnce(current.right, state.dpadPressed.right)) {
log("🎮 DPad 右");
switchChapter(1);
}
state.dpadPressed = current;
}
// =====================================================
// 主手柄处理入口
// =====================================================
function handleGamepad(gp: Gamepad | null): void {
if (!gp) return;
const now = performance.now();
handleAxis(gp, now);
handleDPad(gp);
}
// =====================================================
// 主循环
// =====================================================
let running = false;
function gamepadLoop(): void {
const gamepads = navigator.getGamepads?.() || [];
for (const gp of gamepads) {
if (gp) handleGamepad(gp);
}
requestAnimationFrame(gamepadLoop);
}
// =====================================================
// 连接 / 断开 事件
// =====================================================
window.addEventListener("gamepadconnected", (e: GamepadEvent) => {
log("🎮 手柄已连接:", e.gamepad.id);
if (!running) {
running = true;
requestAnimationFrame(gamepadLoop);
}
});
window.addEventListener("gamepaddisconnected", (e: GamepadEvent) => {
log("🎮 手柄已断开:", e.gamepad.id);
});
export function initXboxGamepad(): void {
if (!running) {
running = true;
requestAnimationFrame(gamepadLoop);
}
}