From d157388680c09d61e85727e4a0917a288ff4291f Mon Sep 17 00:00:00 2001 From: Lunatic Date: Fri, 27 Feb 2026 14:33:19 +0800 Subject: [PATCH] Add per-chapter auto skip intro/outro for web player Per-book skip settings stored in browser localStorage. Checks each chapter's intro/outro zone during playback and automatically seeks past them, matching the behavior of the Android app implementation. --- .../components/app/MediaPlayerContainer.vue | 78 ++++++++++++++++++ .../components/modals/PlayerSettingsModal.vue | 80 ++++++++++++++++++- client/store/user.js | 3 +- 3 files changed, 159 insertions(+), 2 deletions(-) diff --git a/client/components/app/MediaPlayerContainer.vue b/client/components/app/MediaPlayerContainer.vue index 1a2b1d30a..474b5a598 100644 --- a/client/components/app/MediaPlayerContainer.vue +++ b/client/components/app/MediaPlayerContainer.vue @@ -308,6 +308,9 @@ export default { if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER && this.sleepTimerSet) { this.checkChapterEnd() } + + // 检查章节intro/outro跳过 + this.checkAndSkipIntroOutro(time) }, setDuration(duration) { this.totalDuration = duration @@ -543,6 +546,81 @@ export default { this.playerHandler.resetPlayer() // Closes player without reporting to server this.$store.commit('setMediaPlaying', null) } + }, + + // 获取当前书籍的跳过设置 + getBookSkipSettings() { + if (!this.streamLibraryItem) return null + const bookSkipSettings = this.$store.getters['user/getUserSetting']('bookSkipSettings') || {} + return bookSkipSettings[this.streamLibraryItem.id] || {} + }, + + // 检查并执行章节intro/outro跳过 + checkAndSkipIntroOutro(currentTime) { + const skipSettings = this.getBookSkipSettings() + if (!skipSettings) return + + const doSkipIntro = skipSettings.skipIntro && skipSettings.introDuration > 0 + const doSkipOutro = skipSettings.skipOutro && skipSettings.outroDuration > 0 + if (!doSkipIntro && !doSkipOutro) return + if (!this.isPlaying || !this.chapters.length) return + + // 防重入:正在跳过时等待到达目标位置后再解除 + if (this._isSkipping) { + if (this._skipTarget != null && currentTime >= this._skipTarget - 0.5) { + this._isSkipping = false + this._skipTarget = null + } + return + } + + const chapter = this.chapters.find((ch) => ch.start <= currentTime && currentTime < ch.end) + if (!chapter) return + + const introDuration = doSkipIntro ? skipSettings.introDuration : 0 + const outroDuration = doSkipOutro ? skipSettings.outroDuration : 0 + + const introEndTime = Math.min(chapter.start + introDuration, chapter.end) + const outroStartTime = Math.max(chapter.end - outroDuration, chapter.start) + + // 短章节:intro和outro区间重叠则不跳 + if (doSkipIntro && doSkipOutro && introEndTime > outroStartTime) return + + // 检查是否在intro区间 + if (doSkipIntro && currentTime < introEndTime) { + const target = introEndTime + 0.5 + this._isSkipping = true + this._skipTarget = target + this.seek(target) + return + } + + // 检查是否在outro区间 + if (doSkipOutro && currentTime >= outroStartTime) { + const chapterIndex = this.chapters.indexOf(chapter) + const nextChapter = this.chapters[chapterIndex + 1] + + if (nextChapter) { + // 有下一章:跳到下一章开头(如果同时开了skipIntro则跳过intro) + let target = nextChapter.start + if (doSkipIntro) { + const nextIntroEnd = Math.min(nextChapter.start + introDuration, nextChapter.end) + const nextOutroStart = Math.max(nextChapter.end - outroDuration, nextChapter.start) + // 确保下一章intro/outro不重叠 + if (nextIntroEnd <= nextOutroStart) { + target = nextIntroEnd + 0.5 + } + } + this._isSkipping = true + this._skipTarget = target + this.seek(target) + } else { + // 最后一章:跳到结尾,触发播放完成 + this._isSkipping = true + this._skipTarget = chapter.end + this.seek(chapter.end) + } + } } }, mounted() { diff --git a/client/components/modals/PlayerSettingsModal.vue b/client/components/modals/PlayerSettingsModal.vue index dfac28cfd..b7bbd1c6a 100644 --- a/client/components/modals/PlayerSettingsModal.vue +++ b/client/components/modals/PlayerSettingsModal.vue @@ -17,6 +17,29 @@
+ + +
+

本书跳过设置

+ +
+ +
+ 跳过开头 +
+ + +
+ +
+ +
+ 跳过结尾 +
+ + +
+
@@ -40,7 +63,14 @@ export default { jumpForwardAmount: 10, jumpBackwardAmount: 10, playbackRateIncrementDecrementValues: [0.1, 0.05], - playbackRateIncrementDecrement: 0.1 + playbackRateIncrementDecrement: 0.1, + + // 书籍跳过设置 + currentLibraryItemId: null, + skipIntro: false, + introDuration: 10, + skipOutro: false, + outroDuration: 10 } }, computed: { @@ -69,19 +99,67 @@ export default { this.playbackRateIncrementDecrement = val this.$store.dispatch('user/updateUserSettings', { playbackRateIncrementDecrement: val }) }, + + // 书籍跳过设置方法 + setSkipIntro() { + this.updateBookSkipSetting('skipIntro', this.skipIntro) + }, + setIntroDuration() { + this.introDuration = Math.max(0, Math.min(60, parseInt(this.introDuration) || 0)) + this.updateBookSkipSetting('introDuration', this.introDuration) + }, + setSkipOutro() { + this.updateBookSkipSetting('skipOutro', this.skipOutro) + }, + setOutroDuration() { + this.outroDuration = Math.max(0, Math.min(60, parseInt(this.outroDuration) || 0)) + this.updateBookSkipSetting('outroDuration', this.outroDuration) + }, + updateBookSkipSetting(key, value) { + if (!this.currentLibraryItemId) return + + const bookSkipSettings = { ...this.$store.getters['user/getUserSetting']('bookSkipSettings') || {} } + if (!bookSkipSettings[this.currentLibraryItemId]) { + bookSkipSettings[this.currentLibraryItemId] = {} + } + bookSkipSettings[this.currentLibraryItemId][key] = value + this.$store.dispatch('user/updateUserSettings', { bookSkipSettings }) + }, settingsUpdated() { this.useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') this.jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount') this.jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount') this.playbackRateIncrementDecrement = this.$store.getters['user/getUserSetting']('playbackRateIncrementDecrement') + + // 加载当前书籍的跳过设置 + this.loadBookSkipSettings() + }, + loadBookSkipSettings() { + // 获取当前播放的书籍ID + const mediaPlayerContainer = this.$root.$refs.mediaPlayerContainer || this.$parent.$refs.mediaPlayerContainer + if (mediaPlayerContainer && mediaPlayerContainer.streamLibraryItem) { + this.currentLibraryItemId = mediaPlayerContainer.streamLibraryItem.id + + const bookSkipSettings = this.$store.getters['user/getUserSetting']('bookSkipSettings') || {} + const currentBookSettings = bookSkipSettings[this.currentLibraryItemId] || {} + + this.skipIntro = currentBookSettings.skipIntro || false + this.introDuration = currentBookSettings.introDuration || 10 + this.skipOutro = currentBookSettings.skipOutro || false + this.outroDuration = currentBookSettings.outroDuration || 10 + } else { + this.currentLibraryItemId = null + } } }, mounted() { this.settingsUpdated() this.$eventBus.$on('user-settings', this.settingsUpdated) + this.$eventBus.$on('playback-session-changed', this.loadBookSkipSettings) }, beforeDestroy() { this.$eventBus.$off('user-settings', this.settingsUpdated) + this.$eventBus.$off('playback-session-changed', this.loadBookSkipSettings) } } diff --git a/client/store/user.js b/client/store/user.js index 96e79d12f..e8bfb3da3 100644 --- a/client/store/user.js +++ b/client/store/user.js @@ -18,7 +18,8 @@ export const state = () => ({ authorSortBy: 'name', authorSortDesc: false, jumpForwardAmount: 10, - jumpBackwardAmount: 10 + jumpBackwardAmount: 10, + bookSkipSettings: {} // 书籍跳过配置 { [libraryItemId]: { skipIntro, introDuration, skipOutro, outroDuration } } } })