[优化] 添加 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
+1 -1
View File
@@ -12,7 +12,7 @@ on:
workflow_dispatch:
env:
UPSTREAM_REPOSITORY: gedoor/legado
UPSTREAM_REPOSITORY: HapeLee/legado-with-MD3
jobs:
build:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.el-link[data-v-085627fb]{padding:4px}.el-text[data-v-085627fb]{padding-top:20px}[data-v-d8dae8d3] .el-checkbox__label{flex:1;display:flex;justify-content:space-between;align-items:center}.error[data-v-d8dae8d3]{border-color:var(--el-color-error)!important;color:var(--el-color-error)!important;--el-checkbox-checked-text-color: var(--el-color-error);--el-checkbox-checked-bg-color: var(--el-color-error);--el-checkbox-checked-input-border-color: var(--el-color-error)}.edit[data-v-d8dae8d3]{border-color:var(--el-color-dark)!important}.tool[data-v-258cd99b]{display:flex;margin:4px 0;justify-content:center}#source-list[data-v-258cd99b]{margin-top:6px;height:calc(100vh - 119px)}#source-list[data-v-258cd99b] .el-checkbox{margin-bottom:4px;width:100%}[data-v-3ac68c8a] #debug-text{height:calc(100vh - 86px)}[data-v-f62d9369] .el-input{width:100%}[data-v-f62d9369] #source-json{height:calc(100vh - 50px)}[data-v-fd81540f] .el-tabs__header{margin-bottom:5px}.flex-space-between[data-v-096adf6d]{display:flex;justify-content:space-between;align-items:baseline}.flex-column-center[data-v-096adf6d]{display:flex;flex-direction:column;justify-content:center}.menu>.el-button[data-v-096adf6d]{margin:4px;padding:1em;width:6em}.hotkeys-item .title[data-v-096adf6d]{width:5em;display:flex;justify-content:flex-end;margin-right:1em}.hotkeys-item .hotkeys-item__content[data-v-096adf6d]{display:flex;flex-wrap:wrap;flex:1}.hotkeys-item .hotkeys-item__content div[data-v-096adf6d]{margin-bottom:1em}.hotkeys-item .hotkeys-item__content span[data-v-096adf6d]{margin:.5em}[data-v-c07c5146] .el-tab-pane{height:calc(100vh - 55px);padding-top:15px;padding-right:5px;overflow-y:auto}[data-v-c07c5146] .el-tabs__header{margin:0}kbd{align-items:center;background:#7d7d7d1a;border-radius:3px;border:0;padding:4px 5px;font-weight:700;box-shadow:inset 0 -2px #cdcde6,inset 0 0 1px 1px #fff,0 1px 2px 1px #1e235a66}code{border-radius:4px;padding:.15rem .5rem;background-color:var(--el-fill-color-light);transition:color .25s,background-color .5s;font-size:14px}body{padding:0;margin:0}.el-tabs__header{position:sticky;top:0;z-index:2}.editor[data-v-f2c47af3]{display:flex;height:100vh;overflow:hidden}.editor .left[data-v-f2c47af3]{flex:1;margin-left:20px}.editor .right[data-v-f2c47af3]{flex:1;width:360px;margin-right:20px}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
.el-link[data-v-f5093f50]{padding:4px}.el-text[data-v-f5093f50]{padding-top:20px}[data-v-008e87f8] .el-checkbox__label{flex:1;display:flex;justify-content:space-between;align-items:center}.error[data-v-008e87f8]{border-color:var(--el-color-error)!important;color:var(--el-color-error)!important;--el-checkbox-checked-text-color: var(--el-color-error);--el-checkbox-checked-bg-color: var(--el-color-error);--el-checkbox-checked-input-border-color: var(--el-color-error)}.edit[data-v-008e87f8]{border-color:var(--el-color-dark)!important}.tool[data-v-f42bab7c]{display:flex;margin:4px 0;justify-content:center}#source-list[data-v-f42bab7c]{margin-top:6px;height:calc(100vh - 119px)}#source-list[data-v-f42bab7c] .el-checkbox{margin-bottom:4px;width:100%}[data-v-29708ead] #debug-text{height:calc(100vh - 86px)}[data-v-1d793504] .el-input{width:100%}[data-v-1d793504] #source-json{height:calc(100vh - 50px)}[data-v-7b77cf24] .el-tabs__header{margin-bottom:5px}.flex-space-between[data-v-5658ded6]{display:flex;justify-content:space-between;align-items:baseline}.flex-column-center[data-v-5658ded6]{display:flex;flex-direction:column;justify-content:center}.menu>.el-button[data-v-5658ded6]{margin:4px;padding:1em;width:6em}.hotkeys-item .title[data-v-5658ded6]{width:5em;display:flex;justify-content:flex-end;margin-right:1em}.hotkeys-item .hotkeys-item__content[data-v-5658ded6]{display:flex;flex-wrap:wrap;flex:1}.hotkeys-item .hotkeys-item__content div[data-v-5658ded6]{margin-bottom:1em}.hotkeys-item .hotkeys-item__content span[data-v-5658ded6]{margin:.5em}[data-v-ad9f3194] .el-tab-pane{height:calc(100vh - 55px);padding-top:15px;padding-right:5px;overflow-y:auto}[data-v-ad9f3194] .el-tabs__header{margin:0}kbd{align-items:center;background:#7d7d7d1a;border-radius:3px;border:0;padding:4px 5px;font-weight:700;box-shadow:inset 0 -2px #cdcde6,inset 0 0 1px 1px #fff,0 1px 2px 1px #1e235a66}code{border-radius:4px;padding:.15rem .5rem;background-color:var(--el-fill-color-light);transition:color .25s,background-color .5s;font-size:14px}body{padding:0;margin:0}.el-tabs__header{position:sticky;top:0;z-index:2}.editor[data-v-d184d43a]{display:flex;height:100vh;overflow:hidden}.editor .left[data-v-d184d43a]{flex:1;margin-left:20px}.editor .right[data-v-d184d43a]{flex:1;width:360px;margin-right:20px}
@@ -0,0 +1 @@
import{ak as i,N as g,a9 as c,I as d,al as f,u as m}from"./vendor-DQqeohSF.js";const L=(s,t,l=i)=>{const a=d(!1);let r=null;const o=()=>a.value=!1,n=()=>a.value=!0;g(a,e=>{if(!e)return r==null?void 0:r.close();r=f.service({target:m(s),spinner:l,text:t,lock:!0,background:"rgba(0, 0, 0, 0)"})});const u=e=>{if(!(e instanceof Promise))throw TypeError("loadingWrapper argument must be Promise");return n(),e.finally(o)};return c(()=>{o()}),{isLoading:a,showLoading:n,closeLoading:o,loadingWrapper:u}};export{L as u};
@@ -1 +0,0 @@
import{aj as l,B as g,L as c,a8 as d,ak as f,u as m}from"./vendor-C3IVQyCm.js";const L=(s,t,u=l)=>{const a=g(!1);let r=null;const o=()=>a.value=!1,n=()=>a.value=!0;c(a,e=>{if(!e)return r==null?void 0:r.close();r=f.service({target:m(s),spinner:u,text:t,lock:!0,background:"rgba(0, 0, 0, 0)"})});const i=e=>{if(!(e instanceof Promise))throw TypeError("loadingWrapper argument must be Promise");return n(),e.finally(o)};return d(()=>{o()}),{isLoading:a,showLoading:n,closeLoading:o,loadingWrapper:i}};export{L as u};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+15 -16
View File
@@ -1,17 +1,16 @@
<!DOCTYPE html>
<html lang="zh" class="">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="./favicon.ico" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<script crossorigin src="./assets/index-D8bE419P.js" type="module"></script>
<link crossorigin href="./assets/vendor-C3IVQyCm.js" rel="modulepreload">
<link crossorigin href="./assets/vendor-Dt8JbPMC.css" rel="stylesheet">
<link crossorigin href="./assets/index-srCojK1V.css" rel="stylesheet">
</head>
<body>
<div id="app"></div>
</body>
<!DOCTYPE html>
<html lang="zh" class="">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="./favicon.ico" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<script type="module" crossorigin src="./assets/index-DYOzJbC3.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-DQqeohSF.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-CXe1BRiH.css">
<link rel="stylesheet" crossorigin href="./assets/index-CrxHVQK7.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
+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(() => {