Ver código fonte

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

* xbox for web supported

* disable debug log

* vue导入ts文件问题

* 修复 package.json 依赖问题

* 更改web.yml上游

* Bump web v3.26.022400
mobeicanyue 8 meses atrás
pai
commit
8751f62cce
25 arquivos alterados com 640 adições e 402 exclusões
  1. 1 1
      .github/workflows/web.yml
  2. 0 0
      app/src/main/assets/web/vue/assets/BookChapter-BBdriTH6.css
  3. 0 0
      app/src/main/assets/web/vue/assets/BookChapter-BKeJHrON.js
  4. 0 0
      app/src/main/assets/web/vue/assets/BookChapter-CPKoH3Kf.js
  5. 0 0
      app/src/main/assets/web/vue/assets/BookChapter-CfCqElUj.css
  6. 0 0
      app/src/main/assets/web/vue/assets/BookShelf-00b2QCsd.css
  7. 0 0
      app/src/main/assets/web/vue/assets/BookShelf-BRC_NcvN.js
  8. 0 0
      app/src/main/assets/web/vue/assets/BookShelf-Bg1OtPIl.js
  9. 0 0
      app/src/main/assets/web/vue/assets/BookShelf-C14LFudW.css
  10. 0 0
      app/src/main/assets/web/vue/assets/index-CrxHVQK7.css
  11. 0 1
      app/src/main/assets/web/vue/assets/index-D8bE419P.js
  12. 1 0
      app/src/main/assets/web/vue/assets/index-DYOzJbC3.js
  13. 0 0
      app/src/main/assets/web/vue/assets/index-srCojK1V.css
  14. 1 0
      app/src/main/assets/web/vue/assets/loading-BKNdgYqh.js
  15. 0 1
      app/src/main/assets/web/vue/assets/loading-Bj7C7XZW.js
  16. 0 4
      app/src/main/assets/web/vue/assets/vendor-C3IVQyCm.js
  17. 0 0
      app/src/main/assets/web/vue/assets/vendor-CXe1BRiH.css
  18. 4 0
      app/src/main/assets/web/vue/assets/vendor-DQqeohSF.js
  19. 0 0
      app/src/main/assets/web/vue/assets/vendor-Dt8JbPMC.css
  20. 15 16
      app/src/main/assets/web/vue/index.html
  21. 7 7
      modules/web/package.json
  22. 388 371
      modules/web/pnpm-lock.yaml
  23. 0 1
      modules/web/src/components/ToolBar.vue
  24. 221 0
      modules/web/src/utils/xboxGamepad.ts
  25. 2 0
      modules/web/src/views/BookChapter.vue

+ 1 - 1
.github/workflows/web.yml

@@ -12,7 +12,7 @@ on:
   workflow_dispatch:
 
 env:
-  UPSTREAM_REPOSITORY: gedoor/legado
+  UPSTREAM_REPOSITORY: HapeLee/legado-with-MD3
 
 jobs:
   build:

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
app/src/main/assets/web/vue/assets/BookChapter-BBdriTH6.css


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
app/src/main/assets/web/vue/assets/BookChapter-BKeJHrON.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
app/src/main/assets/web/vue/assets/BookChapter-CPKoH3Kf.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
app/src/main/assets/web/vue/assets/BookChapter-CfCqElUj.css


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
app/src/main/assets/web/vue/assets/BookShelf-00b2QCsd.css


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
app/src/main/assets/web/vue/assets/BookShelf-BRC_NcvN.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
app/src/main/assets/web/vue/assets/BookShelf-Bg1OtPIl.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
app/src/main/assets/web/vue/assets/BookShelf-C14LFudW.css


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
app/src/main/assets/web/vue/assets/index-CrxHVQK7.css


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 1
app/src/main/assets/web/vue/assets/index-D8bE419P.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
app/src/main/assets/web/vue/assets/index-DYOzJbC3.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
app/src/main/assets/web/vue/assets/index-srCojK1V.css


+ 1 - 0
app/src/main/assets/web/vue/assets/loading-BKNdgYqh.js

@@ -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};

+ 0 - 1
app/src/main/assets/web/vue/assets/loading-Bj7C7XZW.js

@@ -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};

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 4
app/src/main/assets/web/vue/assets/vendor-C3IVQyCm.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
app/src/main/assets/web/vue/assets/vendor-CXe1BRiH.css


Diferenças do arquivo suprimidas por serem muito extensas
+ 4 - 0
app/src/main/assets/web/vue/assets/vendor-DQqeohSF.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
app/src/main/assets/web/vue/assets/vendor-Dt8JbPMC.css


+ 15 - 16
app/src/main/assets/web/vue/index.html

@@ -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
modules/web/package.json

@@ -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"
   }
-}
+}

Diferenças do arquivo suprimidas por serem muito extensas
+ 388 - 371
modules/web/pnpm-lock.yaml


+ 0 - 1
modules/web/src/components/ToolBar.vue

@@ -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 - 0
modules/web/src/utils/xboxGamepad.ts

@@ -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 - 0
modules/web/src/views/BookChapter.vue

@@ -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(() => {

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff