Bläddra i källkod

Merge pull request #2263 from rinnki-L/feature/web-auto-page

feat(web): 阅读界面新增自动翻页功能(滚动模式)
HapeLee 1 månad sedan
förälder
incheckning
efdca4b17e

+ 51 - 0
modules/web/src/components/ReadSettings.vue

@@ -166,6 +166,38 @@
             >开启</span
           >
         </li>
+        <li class="infinite-loading">
+          <i>自动翻页</i>
+          <span
+            class="infinite-loading-item"
+            :key="0"
+            :class="{ selected: autoPage == false }"
+            @click="setAutoPage(false)"
+            >关闭</span
+          >
+          <span
+            class="infinite-loading-item"
+            :key="1"
+            :class="{ selected: autoPage == true }"
+            @click="setAutoPage(true)"
+            >开启</span
+          >
+        </li>
+        <li class="paragraph-spacing">
+          <i>自动翻页速度(秒/页)</i>
+          <div class="resize">
+            <div class="resize">
+              <span class="less" @click="lessAutoPageSpeed">
+                <em class="iconfont">&#xe625;</em>
+              </span>
+              <b></b> <span class="lang">{{ autoPageSpeed }}</span
+              ><b></b>
+              <span class="more" @click="moreAutoPageSpeed"
+                ><em class="iconfont">&#xe626;</em></span
+              >
+            </div>
+          </div>
+        </li>
       </ul>
     </div>
   </div>
@@ -354,6 +386,25 @@ const infiniteLoading = computed(() => {
 const setInfiniteLoading = (loading: boolean) => {
   store.config.infiniteLoading = loading
 }
+
+//自动翻页
+const autoPage = computed(() => {
+  return store.config.autoPage
+})
+const setAutoPage = (on: boolean) => {
+  store.config.autoPage = on
+}
+
+//自动翻页速度(秒/页,1-120)
+const autoPageSpeed = computed(() => {
+  return store.config.autoPageSpeed
+})
+const moreAutoPageSpeed = () => {
+  if (store.config.autoPageSpeed < 120) store.config.autoPageSpeed += 1
+}
+const lessAutoPageSpeed = () => {
+  if (store.config.autoPageSpeed > 1) store.config.autoPageSpeed -= 1
+}
 </script>
 
 <style lang="scss" scoped>

+ 2 - 0
modules/web/src/store/bookStore.ts

@@ -18,6 +18,8 @@ const default_config: webReadConfig = {
   infiniteLoading: false,
   customFontName: '',
   jumpDuration: 1000,
+  autoPage: false,
+  autoPageSpeed: 10,
   spacing: {
     paragraph: 1,
     line: 0.8,

+ 97 - 0
modules/web/src/views/BookChapter.vue

@@ -186,6 +186,81 @@ const onReachBottom = (entries: IntersectionObserverEntry[]) => {
   }
 }
 
+// 自动翻页(滚动模式,行为对齐App端)
+const autoPagePaused = ref(false)
+let autoPageRaf = 0
+let autoPageLastFrame = 0
+let autoPageResumeTimer: number | undefined
+
+const startAutoPage = () => {
+  if (autoPageRaf || !store.config.autoPage) return
+  // 弹窗打开期间不启动滚动(细节5:目录/设置弹窗打开时暂停)
+  if (popCataVisible.value || readSettingsVisible.value) {
+    autoPagePaused.value = true
+    return
+  }
+  autoPageLastFrame = 0
+  const tick = (now: number) => {
+    if (!store.config.autoPage || autoPagePaused.value) {
+      autoPageRaf = 0
+      return
+    }
+    if (autoPageLastFrame === 0) autoPageLastFrame = now
+    const elapsed = now - autoPageLastFrame
+    autoPageLastFrame = now
+    // 与App端一致:每帧滚动 视口高度 / (速度秒数 × 1000ms) × 本帧毫秒数
+    const duration = store.config.autoPageSpeed * 1000
+    window.scrollBy(0, (window.innerHeight / duration) * elapsed)
+    autoPageRaf = requestAnimationFrame(tick)
+  }
+  autoPageRaf = requestAnimationFrame(tick)
+}
+const stopAutoPage = () => {
+  if (autoPageRaf) cancelAnimationFrame(autoPageRaf)
+  autoPageRaf = 0
+}
+const resumeAutoPage = () => {
+  if (autoPageResumeTimer) {
+    window.clearTimeout(autoPageResumeTimer)
+    autoPageResumeTimer = undefined
+  }
+  if (!store.config.autoPage) return
+  autoPagePaused.value = false
+  startAutoPage()
+}
+/**
+ * 暂停自动翻页
+ * @param idleResumeMs 大于0时在用户停止交互一段时间后自动恢复;0表示保持暂停直到手动恢复
+ */
+const pauseAutoPage = (idleResumeMs = 3000) => {
+  if (autoPageResumeTimer) {
+    window.clearTimeout(autoPageResumeTimer)
+    autoPageResumeTimer = undefined
+  }
+  if (!store.config.autoPage) return
+  autoPagePaused.value = true
+  stopAutoPage()
+  if (idleResumeMs > 0) {
+    autoPageResumeTimer = window.setTimeout(resumeAutoPage, idleResumeMs)
+  }
+}
+// 开关切换
+watch(
+  () => store.config.autoPage,
+  on => (on ? startAutoPage() : stopAutoPage()),
+)
+// 打开目录/设置弹窗时暂停,关闭后恢复
+watch([popCataVisible, readSettingsVisible], ([cata, settings]) => {
+  if (cata || settings) {
+    pauseAutoPage(0)
+  } else {
+    resumeAutoPage()
+  }
+})
+// 用户滚轮/触摸交互时暂停,停止操作一段时间后自动恢复
+const onUserWheel = () => pauseAutoPage()
+const onUserTouch = () => pauseAutoPage()
+
 // 字体
 const fontFamily = computed(() => {
   if (store.config.font >= 0) {
@@ -270,9 +345,11 @@ watch(
 const top = ref()
 const bottom = ref()
 const toTop = () => {
+  pauseAutoPage()
   jump(top.value)
 }
 const toBottom = () => {
+  pauseAutoPage()
   jump(bottom.value)
 }
 
@@ -382,11 +459,17 @@ const onVisibilityChange = () => {
   if (document.visibilityState == 'hidden' && _bookProgress) {
     store.saveBookProgress()
   }
+  if (document.visibilityState === 'hidden') {
+    pauseAutoPage(0)
+  } else {
+    resumeAutoPage()
+  }
 }
 // 定时同步
 
 // 章节切换
 const toNextChapter = () => {
+  pauseAutoPage()
   store.setContentLoading(true)
   const index = chapterIndex.value + 1
   if (typeof catalog.value[index] !== 'undefined') {
@@ -404,6 +487,7 @@ const toNextChapter = () => {
   }
 }
 const toPreChapter = () => {
+  pauseAutoPage()
   store.setContentLoading(true)
   const index = chapterIndex.value - 1
   if (typeof catalog.value[index] !== 'undefined') {
@@ -425,6 +509,9 @@ let canJump = true
 // 监听方向键
 const handleKeyPress = (event: KeyboardEvent) => {
   if (!canJump) return
+  if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
+    pauseAutoPage()
+  }
   switch (event.key) {
     case 'ArrowLeft':
       event.stopPropagation()
@@ -479,6 +566,9 @@ const ignoreKeyPress = (event: KeyboardEvent) => {
 
 onMounted(async () => {
   await store.loadWebConfig()
+  if (store.config.autoPage) startAutoPage()
+  window.addEventListener('wheel', onUserWheel, { passive: true })
+  window.addEventListener('touchstart', onUserTouch, { passive: true })
   //获取书籍数据
   const bookUrl = sessionStorage.getItem('bookUrl')
   const name = sessionStorage.getItem('bookName')
@@ -531,8 +621,15 @@ onUnmounted(() => {
   window.removeEventListener('keyup', handleKeyPress)
   window.removeEventListener('keydown', ignoreKeyPress)
   window.removeEventListener('resize', onResize)
+  window.removeEventListener('wheel', onUserWheel)
+  window.removeEventListener('touchstart', onUserTouch)
   // 兼容Safari < 14
   document.removeEventListener('visibilitychange', onVisibilityChange)
+  stopAutoPage()
+  if (autoPageResumeTimer) {
+    window.clearTimeout(autoPageResumeTimer)
+    autoPageResumeTimer = undefined
+  }
   readSettingsVisible.value = false
   popCataVisible.value = false
   scrollObserver?.disconnect()

+ 2 - 0
modules/web/src/web.d.ts

@@ -6,6 +6,8 @@ export type webReadConfig = {
   infiniteLoading: boolean
   customFontName: string
   jumpDuration: number
+  autoPage: boolean
+  autoPageSpeed: number
   spacing: {
     paragraph: number
     line: number