[优化] 添加 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
+7 -7
View File
@@ -19,12 +19,12 @@
"dependencies": {
"@element-plus/icons-svg": "^2.3.1",
"@element-plus/icons-vue": "^2.3.1",
"@vueuse/core": "^13.8.0",
"@vueuse/core": "^11.1.0",
"@vueuse/shared": "^11.1.0",
"axios": "^1.7.7",
"element-plus": "^2.8.5",
"element-plus": "2.8.5",
"hotkeys-js": "^3.13.7",
"pinia": "^3.0.3",
"pinia": "^2.2.4",
"vue": "^3.5.12",
"vue-router": "^4.4.5",
"vue3-virtual-scroll-list": "^0.2.1"
@@ -32,11 +32,11 @@
"devDependencies": {
"sass-embedded": "^1.83.1",
"@tsconfig/node20": "^20.1.4",
"@types/node": "^24.3.0",
"@types/node": "^20.16.11",
"@vitejs/plugin-vue": "^5.1.4",
"@vue/eslint-config-prettier": "^10.0.0",
"@vue/eslint-config-typescript": "^14.0.1",
"@vue/tsconfig": "^0.8.1",
"@vue/tsconfig": "^0.5.1",
"eslint": "^9.12.0",
"eslint-plugin-vue": "^9.29.0",
"npm-run-all2": "^6.2.3",
@@ -46,6 +46,6 @@
"unplugin-icons": "^0.19.3",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.8",
"vue-tsc": "^3.0.6"
"vue-tsc": "^2.1.6"
}
}
}
+1319 -1265
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -69,7 +69,6 @@ import API from '@api'
import { CircleCheckFilled, Edit } from '@element-plus/icons-vue'
import hotkeys from 'hotkeys-js'
import { getSourceName, isInvaildSource, normalizeSource } from '../utils/souce'
import { getSourceName, isInvaildSource } from '../utils/souce'
const store = useSourceStore()
const pull = () => {
+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);
}
}
+2
View File
@@ -113,6 +113,7 @@ import API from '@api'
import { useLoading } from '@/hooks/loading'
import { useThrottleFn } from '@vueuse/shared'
import { isNullOrBlank } from '@/utils/utils'
import { initXboxGamepad } from '@/utils/xboxGamepad'
const content = ref()
// loading spinner
@@ -519,6 +520,7 @@ onMounted(async () => {
document.title = (name as string) + ' | ' + chapters[chapterIndex].title
}),
)
initXboxGamepad()
})
onUnmounted(() => {