Parcourir la source

Merge pull request #1979 from mobeicanyue/main

增强手柄功能支持,统一代码风格,简化接口定义
Kudomaga il y a 3 mois
Parent
commit
dd0fcfd423

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

@@ -11,8 +11,6 @@ on:
       - '**/modules/web/**'
       - '**/modules/web/**'
   workflow_dispatch:
   workflow_dispatch:
 
 
-env:
-  UPSTREAM_REPOSITORY: HapeLee/legado-with-MD3
 
 
 jobs:
 jobs:
   build:
   build:
@@ -57,7 +55,7 @@ jobs:
           echo "APP_VER=$version" >> $GITHUB_ENV
           echo "APP_VER=$version" >> $GITHUB_ENV
 
 
       - name: push changes
       - name: push changes
-        if: ${{ github.event_name != 'pull_request' && github.repository == env.UPSTREAM_REPOSITORY }}
+        if: ${{ github.event_name != 'pull_request' }}
         uses: stefanzweifel/git-auto-commit-action@v6.0.1
         uses: stefanzweifel/git-auto-commit-action@v6.0.1
         with:
         with:
           commit_message: Bump web ${{ env.APP_VER}}
           commit_message: Bump web ${{ env.APP_VER}}

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
app/src/main/assets/web/vue/assets/BookChapter-C7EWfyGZ.css


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
app/src/main/assets/web/vue/assets/BookChapter-CfCqElUj.css


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
app/src/main/assets/web/vue/assets/BookChapter-CvxOa6WY.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
app/src/main/assets/web/vue/assets/BookShelf-00b2QCsd.css


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
app/src/main/assets/web/vue/assets/BookShelf-Brh2uMWa.css


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
app/src/main/assets/web/vue/assets/BookShelf-CPpNHFqF.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 1
app/src/main/assets/web/vue/assets/index-CV282XDP.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
app/src/main/assets/web/vue/assets/index-CpkaNQMA.css


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
app/src/main/assets/web/vue/assets/index-CrxHVQK7.css


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

@@ -4,10 +4,10 @@
         <meta charset="UTF-8" />
         <meta charset="UTF-8" />
         <link rel="icon" href="./favicon.ico" />
         <link rel="icon" href="./favicon.ico" />
     <meta name="viewport" content="width=device-width,initial-scale=1.0" />
     <meta name="viewport" content="width=device-width,initial-scale=1.0" />
-      <script type="module" crossorigin src="./assets/index-DYOzJbC3.js"></script>
+      <script type="module" crossorigin src="./assets/index-CV282XDP.js"></script>
       <link rel="modulepreload" crossorigin href="./assets/vendor-DQqeohSF.js">
       <link rel="modulepreload" crossorigin href="./assets/vendor-DQqeohSF.js">
       <link rel="stylesheet" crossorigin href="./assets/vendor-CXe1BRiH.css">
       <link rel="stylesheet" crossorigin href="./assets/vendor-CXe1BRiH.css">
-      <link rel="stylesheet" crossorigin href="./assets/index-CrxHVQK7.css">
+      <link rel="stylesheet" crossorigin href="./assets/index-CpkaNQMA.css">
     </head>
     </head>
 
 
     <body>
     <body>

+ 246 - 104
modules/web/src/utils/xboxGamepad.ts

@@ -3,28 +3,57 @@
 // =====================================================
 // =====================================================
 
 
 interface GamepadConfig {
 interface GamepadConfig {
-  DEBUG: boolean;
-  AXIS_THRESHOLD: number;
-  AXIS_COOLDOWN: number;
+  DEBUG: boolean
+  AXIS_THRESHOLD: number
+  AXIS_COOLDOWN: number
+
   DPAD_INDEX: {
   DPAD_INDEX: {
-    UP: number;
-    DOWN: number;
-    LEFT: number;
-    RIGHT: number;
-  };
+    UP: number
+    DOWN: number
+    LEFT: number
+    RIGHT: number
+  }
+
+  BUTTON_INDEX: {
+    A: number
+    B: number
+  }
 }
 }
 
 
 interface DPadState {
 interface DPadState {
-  up: boolean;
-  down: boolean;
-  left: boolean;
-  right: boolean;
+  up: boolean
+  down: boolean
+  left: boolean
+  right: boolean
+
+  a: boolean
+  b: boolean
+}
+
+interface GamepadState {
+  lastAxisTime: number
+  lastAxisDirection: number
+  dpadPressed: DPadState
+}
+
+export interface XboxGamepadOptions {
+  /** B 键切换目录:由调用方经 Vue 状态控制,避免直接操作 DOM */
+  onToggleCatalog?: () => void
 }
 }
 
 
-interface GlobalState {
-  lastAxisTime: number;
-  lastAxisDirection: number;
-  dpadPressed: DPadState;
+function createGamepadState(): GamepadState {
+  return {
+    lastAxisTime: 0,
+    lastAxisDirection: 0,
+    dpadPressed: {
+      up: false,
+      down: false,
+      left: false,
+      right: false,
+      a: false,
+      b: false,
+    },
+  }
 }
 }
 
 
 // =====================================================
 // =====================================================
@@ -32,190 +61,303 @@ interface GlobalState {
 // =====================================================
 // =====================================================
 
 
 const CONFIG: GamepadConfig = {
 const CONFIG: GamepadConfig = {
-  DEBUG: false,              // 日志总开关
-  AXIS_THRESHOLD: 0.7,      // 摇杆触发阈值
-  AXIS_COOLDOWN: 300,       // 摇杆触发冷却时间 (ms)
-  DPAD_INDEX: {             // Xbox 标准映射
+  DEBUG: false, // 日志总开关
+  AXIS_THRESHOLD: 0.7, // 摇杆触发阈值
+  AXIS_COOLDOWN: 300, // 摇杆触发冷却时间 (ms)
+
+  DPAD_INDEX: {
+    // Xbox 标准映射
     UP: 12,
     UP: 12,
     DOWN: 13,
     DOWN: 13,
     LEFT: 14,
     LEFT: 14,
-    RIGHT: 15
-  }
-};
+    RIGHT: 15,
+  },
+
+  // Xbox 按键
+  BUTTON_INDEX: {
+    A: 0,
+
+    B: 1,
+  },
+}
 
 
 // =====================================================
 // =====================================================
 // 日志工具函数
 // 日志工具函数
 // =====================================================
 // =====================================================
 
 
 function log(...args: any[]): void {
 function log(...args: any[]): void {
-  if (!CONFIG.DEBUG) return;
-  console.log(...args);
+  if (!CONFIG.DEBUG) return
+
+  console.log(...args)
 }
 }
 
 
 // =====================================================
 // =====================================================
 // 状态记录
 // 状态记录
 // =====================================================
 // =====================================================
 
 
-const state: GlobalState = {
-  lastAxisTime: 0,
-  lastAxisDirection: 0,
-  dpadPressed: {
-    up: false,
-    down: false,
-    left: false,
-    right: false
-  }
-};
+let running = false
+
+let onToggleCatalog: (() => void) | null = null
+
+// 每个手柄独立一份按键/摇杆边沿状态,避免多手柄互相串触发
+const gamepadStates = new Map<number, GamepadState>()
 
 
 // =====================================================
 // =====================================================
 // 通用工具函数
 // 通用工具函数
 // =====================================================
 // =====================================================
 
 
 /**
 /**
- * 平滑翻页函数
- * @param direction 1 = 向下, -1 = 向上
+ * 平滑翻页
  */
  */
 function scrollPage(direction: number): void {
 function scrollPage(direction: number): void {
-  const offset = window.innerHeight - 110;
-  const distance = direction === 1 ? offset : -offset;
+  const offset = window.innerHeight - 110
 
 
-  log(direction === 1 ? "🎮 翻页:向下" : "🎮 翻页:向上");
+  const distance = direction === 1 ? offset : -offset
 
 
   window.scrollBy({
   window.scrollBy({
     top: distance,
     top: distance,
-    behavior: "smooth"
-  });
+
+    behavior: 'smooth',
+  })
+}
+
+/**
+ * 顶部
+ */
+function goTop(): void {
+  log('🎮 前往顶部')
+
+  window.scrollTo({
+    top: 0,
+
+    behavior: 'smooth',
+  })
+}
+
+/**
+ * 底部
+ */
+function goBottom(): void {
+  log('🎮 前往底部')
+
+  window.scrollTo({
+    top: document.body.scrollHeight,
+
+    behavior: 'smooth',
+  })
 }
 }
 
 
 /**
 /**
- * 章节切换函数
- * @param direction 1 = 下一章, -1 = 上一章
+ * 章节切换
  */
  */
 function switchChapter(direction: number): void {
 function switchChapter(direction: number): void {
-  const buttons = document.querySelectorAll<HTMLElement>(".read-bar .tool-icon");
+  const buttons = document.querySelectorAll<HTMLElement>('.read-bar .tool-icon')
 
 
-  if (buttons.length < 2) return;
+  if (buttons.length < 2) return
 
 
   if (direction === 1) {
   if (direction === 1) {
-    log("🎮 切换章节:下一章");
-    buttons[1].click();
+    log('🎮 下一章')
+
+    buttons[1].click()
   } else {
   } else {
-    log("🎮 切换章节:上一章");
-    buttons[0].click();
+    log('🎮 上一章')
+
+    buttons[0].click()
+  }
+}
+
+/**
+ * 全屏切换
+ */
+function toggleFullscreen(): void {
+  if (!document.fullscreenElement) {
+    const promise = document.documentElement.requestFullscreen?.()
+
+    // 手柄轮询触发通常没有用户激活,可能被浏览器拒绝,需要捕获
+    promise?.catch(() => log('🎮 进入浏览器全屏被拒绝'))
+
+    log('🎮 进入浏览器全屏')
+  } else {
+    document.exitFullscreen?.()
+
+    log('🎮 退出浏览器全屏')
+  }
+}
+
+/**
+ * 目录显示隐藏
+ */
+function toggleCatalog(): void {
+  if (onToggleCatalog) {
+    onToggleCatalog()
+
+    log('🎮 切换目录')
+
+    return
   }
   }
+
+  log('❌ 未注册目录切换回调')
 }
 }
 
 
 /**
 /**
- * 边沿检测函数
+ * 边沿检测
  */
  */
 function isPressedOnce(current: boolean, previous: boolean): boolean {
 function isPressedOnce(current: boolean, previous: boolean): boolean {
-  return current && !previous;
+  return current && !previous
 }
 }
 
 
 // =====================================================
 // =====================================================
 // 摇杆处理
 // 摇杆处理
 // =====================================================
 // =====================================================
 
 
-function handleAxis(gp: Gamepad, now: number): void {
-  const axisY = gp.axes[1] || 0;
+function handleAxis(gp: Gamepad, now: number, gs: GamepadState): void {
+  const axisY = gp.axes[1] || 0
 
 
   if (Math.abs(axisY) <= CONFIG.AXIS_THRESHOLD) {
   if (Math.abs(axisY) <= CONFIG.AXIS_THRESHOLD) {
-    state.lastAxisDirection = 0;
-    return;
+    gs.lastAxisDirection = 0
+
+    return
   }
   }
 
 
-  const direction = axisY > 0 ? 1 : -1;
-  const cooldownPassed = now - state.lastAxisTime > CONFIG.AXIS_COOLDOWN;
-  const directionChanged = direction !== state.lastAxisDirection;
+  const direction = axisY > 0 ? 1 : -1
+
+  const cooldownPassed = now - gs.lastAxisTime > CONFIG.AXIS_COOLDOWN
+
+  const directionChanged = direction !== gs.lastAxisDirection
 
 
   if (cooldownPassed || directionChanged) {
   if (cooldownPassed || directionChanged) {
-    state.lastAxisTime = now;
-    state.lastAxisDirection = direction;
-    log(`🎮 摇杆触发 | axisY=${axisY.toFixed(2)}`);
-    scrollPage(direction);
+    gs.lastAxisTime = now
+
+    gs.lastAxisDirection = direction
+
+    scrollPage(direction)
   }
   }
 }
 }
 
 
 // =====================================================
 // =====================================================
-// DPad 处理
+// DPad + 按键处理
 // =====================================================
 // =====================================================
 
 
-function handleDPad(gp: Gamepad): void {
-  const indexes = CONFIG.DPAD_INDEX;
+function handleDPad(gp: Gamepad, gs: GamepadState): void {
+  const indexes = CONFIG.DPAD_INDEX
+
+  const buttons = CONFIG.BUTTON_INDEX
 
 
   const current: DPadState = {
   const current: DPadState = {
     up: gp.buttons[indexes.UP]?.pressed || false,
     up: gp.buttons[indexes.UP]?.pressed || false,
+
     down: gp.buttons[indexes.DOWN]?.pressed || false,
     down: gp.buttons[indexes.DOWN]?.pressed || false,
+
     left: gp.buttons[indexes.LEFT]?.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);
+    right: gp.buttons[indexes.RIGHT]?.pressed || false,
+
+    a: gp.buttons[buttons.A]?.pressed || false,
+
+    b: gp.buttons[buttons.B]?.pressed || false,
+  }
+
+  // 十字 ↑ 顶部
+
+  if (isPressedOnce(current.up, gs.dpadPressed.up)) {
+    goTop()
+  }
+
+  // 十字 ↓ 底部
+
+  if (isPressedOnce(current.down, gs.dpadPressed.down)) {
+    goBottom()
+  }
+
+  // 左右章节
+
+  if (isPressedOnce(current.left, gs.dpadPressed.left)) {
+    switchChapter(-1)
   }
   }
-  if (isPressedOnce(current.down, state.dpadPressed.down)) {
-    log("🎮 DPad 下");
-    scrollPage(1);
+
+  if (isPressedOnce(current.right, gs.dpadPressed.right)) {
+    switchChapter(1)
   }
   }
-  if (isPressedOnce(current.left, state.dpadPressed.left)) {
-    log("🎮 DPad 左");
-    switchChapter(-1);
+
+  // A 全屏
+
+  if (isPressedOnce(current.a, gs.dpadPressed.a)) {
+    toggleFullscreen()
   }
   }
-  if (isPressedOnce(current.right, state.dpadPressed.right)) {
-    log("🎮 DPad 右");
-    switchChapter(1);
+
+  // B 目录
+
+  if (isPressedOnce(current.b, gs.dpadPressed.b)) {
+    toggleCatalog()
   }
   }
 
 
-  state.dpadPressed = current;
+  gs.dpadPressed = current
 }
 }
 
 
 // =====================================================
 // =====================================================
 // 主手柄处理入口
 // 主手柄处理入口
 // =====================================================
 // =====================================================
 
 
-function handleGamepad(gp: Gamepad | null): void {
-  if (!gp) return;
-  const now = performance.now();
-  handleAxis(gp, now);
-  handleDPad(gp);
+function handleGamepad(gp: Gamepad): void {
+  let gs = gamepadStates.get(gp.index)
+
+  if (!gs) {
+    gs = createGamepadState()
+
+    gamepadStates.set(gp.index, gs)
+  }
+
+  const now = performance.now()
+
+  handleAxis(gp, now, gs)
+
+  handleDPad(gp, gs)
 }
 }
 
 
 // =====================================================
 // =====================================================
 // 主循环
 // 主循环
 // =====================================================
 // =====================================================
 
 
-let running = false;
-
 function gamepadLoop(): void {
 function gamepadLoop(): void {
-  const gamepads = navigator.getGamepads?.() || [];
+  const gamepads = navigator.getGamepads?.() || []
 
 
   for (const gp of gamepads) {
   for (const gp of gamepads) {
-    if (gp) handleGamepad(gp);
+    if (gp) handleGamepad(gp)
   }
   }
 
 
-  requestAnimationFrame(gamepadLoop);
+  requestAnimationFrame(gamepadLoop)
 }
 }
 
 
 // =====================================================
 // =====================================================
-// 连接 / 断开 事件
+// 连接 / 断开事件
 // =====================================================
 // =====================================================
 
 
-window.addEventListener("gamepadconnected", (e: GamepadEvent) => {
-  log("🎮 手柄已连接:", e.gamepad.id);
+window.addEventListener('gamepadconnected', (e: GamepadEvent) => {
+  log('🎮 手柄已连接:', e.gamepad.id)
+
   if (!running) {
   if (!running) {
-    running = true;
-    requestAnimationFrame(gamepadLoop);
+    running = true
+
+    requestAnimationFrame(gamepadLoop)
   }
   }
-});
+})
+
+window.addEventListener('gamepaddisconnected', (e: GamepadEvent) => {
+  log('🎮 手柄断开:', e.gamepad.id)
 
 
-window.addEventListener("gamepaddisconnected", (e: GamepadEvent) => {
-  log("🎮 手柄已断开:", e.gamepad.id);
-});
+  gamepadStates.delete(e.gamepad.index)
+})
+
+// =====================================================
+// 外部调用
+// =====================================================
+
+export function initXboxGamepad(options?: XboxGamepadOptions): void {
+  onToggleCatalog = options?.onToggleCatalog ?? null
 
 
-export function initXboxGamepad(): void {
   if (!running) {
   if (!running) {
-    running = true;
-    requestAnimationFrame(gamepadLoop);
+    running = true
+
+    requestAnimationFrame(gamepadLoop)
   }
   }
-}
+}

+ 5 - 1
modules/web/src/views/BookChapter.vue

@@ -520,7 +520,11 @@ onMounted(async () => {
       document.title = (name as string) + ' | ' + chapters[chapterIndex].title
       document.title = (name as string) + ' | ' + chapters[chapterIndex].title
     }),
     }),
   )
   )
-  initXboxGamepad()
+  initXboxGamepad({
+    onToggleCatalog: () => {
+      popCataVisible.value = !popCataVisible.value
+    },
+  })
 })
 })
 
 
 onUnmounted(() => {
 onUnmounted(() => {

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff