From 6e7ba0e51c05d82263830937e6eb0b57f22fea72 Mon Sep 17 00:00:00 2001 From: wz219 Date: Thu, 9 Oct 2025 19:42:14 +1300 Subject: [PATCH] modified --- client/components/player/PlayerUi.vue | 236 +++++++++++++++++--------- package-lock.json | 131 +++++++++++++- package.json | 3 +- 3 files changed, 287 insertions(+), 83 deletions(-) diff --git a/client/components/player/PlayerUi.vue b/client/components/player/PlayerUi.vue index f929943c4..346518155 100644 --- a/client/components/player/PlayerUi.vue +++ b/client/components/player/PlayerUi.vue @@ -4,40 +4,60 @@
- + - - + + + + + - - - - - - - - @@ -55,7 +75,8 @@

- {{ currentChapterName }}  ({{ $getString('LabelPlayerChapterNumberMarker', [currentChapterIndex + 1, chapters.length]) }}) + {{ currentChapterName }} +  ({{ $getString('LabelPlayerChapterNumberMarker', [currentChapterIndex + 1, chapters.length]) }})

@@ -74,15 +95,9 @@ export default { props: { loading: Boolean, paused: Boolean, - chapters: { - type: Array, - default: () => [] - }, + chapters: { type: Array, default: () => [] }, currentChapter: Object, - bookmarks: { - type: Array, - default: () => [] - }, + bookmarks: { type: Array, default: () => [] }, sleepTimerSet: Boolean, sleepTimerRemaining: Number, sleepTimerType: String, @@ -100,7 +115,14 @@ export default { showChaptersModal: false, showPlayerSettingsModal: false, currentTime: 0, - duration: 0 + duration: 0, + + // 渐降音量状态 + fadeActive: false, + fadeMsTotal: 0, + fadeMsLeft: 0, + fadeTimer: null, + fadeStartVolume: 1 } }, watch: { @@ -114,38 +136,40 @@ export default { }, computed: { sleepTimerRemainingString() { - if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER) { - return 'EoC' - } else { - var rounded = Math.round(this.sleepTimerRemaining) - if (rounded < 90) { - return `${rounded}s` - } - var minutesRounded = Math.round(rounded / 60) - if (minutesRounded <= 90) { - return `${minutesRounded}m` - } - var hoursRounded = Math.round(minutesRounded / 60) - return `${hoursRounded}h` - } + if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER) return 'EoC' + const rounded = Math.round(this.sleepTimerRemaining || 0) + if (rounded < 90) return `${rounded}s` + const m = Math.round(rounded / 60) + if (m <= 90) return `${m}m` + const h = Math.round(m / 60) + return `${h}h` }, + + fadeTooltipCancel() { + return (this.$strings && this.$strings.LabelCancelFadeVolume) || 'Cancel volume fade' + }, + fadeRemainingPretty() { + const sec = Math.max(0, Math.ceil(this.fadeMsLeft / 1000)) + if (sec < 90) return `${sec}s` + const m = Math.round(sec / 60) + if (m <= 90) return `${m}m` + const h = Math.round(m / 60) + return `${h}h` + }, + timeRemaining() { if (this.useChapterTrack && this.currentChapter) { - var currChapTime = this.currentTime - this.currentChapter.start - return (this.currentChapterDuration - currChapTime) / this.playbackRate + const curr = this.currentTime - this.currentChapter.start + return (this.currentChapterDuration - curr) / this.playbackRate } return (this.duration - this.currentTime) / this.playbackRate }, timeRemainingPretty() { - if (this.timeRemaining < 0) { - return this.$secondsToTimestamp(this.timeRemaining * -1) - } - return '-' + this.$secondsToTimestamp(this.timeRemaining) + return this.timeRemaining < 0 ? this.$secondsToTimestamp(-this.timeRemaining) : '-' + this.$secondsToTimestamp(this.timeRemaining) }, progressPercent() { const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration - const time = this.useChapterTrack ? Math.max(this.currentTime - this.currentChapterStart) : this.currentTime - + const time = this.useChapterTrack ? Math.max(0, this.currentTime - this.currentChapterStart) : this.currentTime if (!duration) return 0 return Math.round((100 * time) / duration) }, @@ -153,12 +177,10 @@ export default { return this.currentChapter?.title || '' }, currentChapterDuration() { - if (!this.currentChapter) return 0 - return this.currentChapter.end - this.currentChapter.start + return this.currentChapter ? this.currentChapter.end - this.currentChapter.start : 0 }, currentChapterStart() { - if (!this.currentChapter) return 0 - return this.currentChapter.start + return this.currentChapter ? this.currentChapter.start : 0 }, isFullscreen() { return this.$store.state.playerIsFullscreen @@ -175,14 +197,80 @@ export default { return this.$store.state.playerQueueItems || [] }, useChapterTrack() { - const _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false - return this.chapters.length ? _useChapterTrack : false + const v = this.$store.getters['user/getUserSetting']('useChapterTrack') || false + return this.chapters.length ? v : false }, playbackRateIncrementDecrement() { return this.$store.getters['user/getUserSetting']('playbackRateIncrementDecrement') } }, methods: { + /* ===== 渐降音量:UI入口 ===== */ + toggleFadeDialog() { + if (this.fadeActive) { + this.cancelFade() + return + } + const defVal = '15' + const msg = (this.$strings && this.$strings.PromptFadeMinutes) || 'Fade to silence in how many minutes?' + const v = window.prompt(msg, defVal) + if (v == null) return + const minutes = Number(v) + if (!isFinite(minutes) || minutes <= 0) { + this.$toast && this.$toast.error('Invalid minutes') + return + } + this.startFade(minutes * 60 * 1000) + }, + + /* ===== 渐降音量:核心逻辑 ===== */ + startFade(msTotal) { + // 清理旧的 + this.cancelFade(false) + + this.fadeMsTotal = Math.max(1000, msTotal) + this.fadeMsLeft = this.fadeMsTotal + this.fadeStartVolume = Math.max(0, Math.min(1, this.volume)) + this.fadeActive = true + + // 先同步一次 + this.applyFade(0) + + const tickMs = 500 + this.fadeTimer = setInterval(() => { + this.fadeMsLeft = Math.max(0, this.fadeMsLeft - tickMs) + const elapsed = this.fadeMsTotal - this.fadeMsLeft + this.applyFade(elapsed) + + if (this.fadeMsLeft <= 0) { + this.setVolume(0) + this.cancelFade(false) + } + }, tickMs) + }, + + applyFade(elapsedMs) { + const t = Math.min(1, Math.max(0, elapsedMs / this.fadeMsTotal)) + const vol = +(this.fadeStartVolume * (1 - t)).toFixed(4) + this.volume = vol + this.setVolume(vol) + }, + + cancelFade(toast = true) { + if (this.fadeTimer) { + clearInterval(this.fadeTimer) + this.fadeTimer = null + } + if (this.fadeActive && toast && this.$toast) { + const msg = (this.$strings && this.$strings.ToastFadeCancelled) || 'Volume fade cancelled' + this.$toast.info(msg) + } + this.fadeActive = false + this.fadeMsTotal = 0 + this.fadeMsLeft = 0 + }, + + // ===== 现有播放器逻辑 ===== toggleFullscreen(isFullscreen) { this.$store.commit('setPlayerIsFullscreen', isFullscreen) }, @@ -203,6 +291,7 @@ export default { jumpForward() { this.$emit('jumpForward') }, + increaseVolume() { if (this.volume >= 1) return this.volume = Math.min(1, this.volume + 0.1) @@ -221,6 +310,7 @@ export default { this.$refs.volumeControl.toggleMute() } }, + increasePlaybackRate() { if (this.playbackRate >= 10) return this.playbackRate = Number((this.playbackRate + this.playbackRateIncrementDecrement || 0.1).toFixed(2)) @@ -247,12 +337,10 @@ export default { setUseChapterTrack() { this.useChapterTrack = !this.useChapterTrack if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack) - this.$store.dispatch('user/updateUserSettings', { useChapterTrack: this.useChapterTrack }) this.updateTimestamp() }, checkUpdateChapterTrack() { - // Changing media in player may not have chapters if (!this.chapters.length && this.useChapterTrack) { this.useChapterTrack = false if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack) @@ -265,21 +353,19 @@ export default { this.seek(0) }, prevChapter() { - if (!this.currentChapter || this.currentChapterIndex === 0) { - return this.restart() - } - var timeInCurrentChapter = this.currentTime - this.currentChapter.start - if (timeInCurrentChapter <= 3 && this.chapters[this.currentChapterIndex - 1]) { - var prevChapter = this.chapters[this.currentChapterIndex - 1] - this.seek(prevChapter.start) + if (!this.currentChapter || this.currentChapterIndex === 0) return this.restart() + const t = this.currentTime - this.currentChapter.start + if (t <= 3 && this.chapters[this.currentChapterIndex - 1]) { + const prev = this.chapters[this.currentChapterIndex - 1] + this.seek(prev.start) } else { this.seek(this.currentChapter.start) } }, goToNext() { if (this.hasNextChapter) { - const nextChapter = this.chapters[this.currentChapterIndex + 1] - this.seek(nextChapter.start) + const next = this.chapters[this.currentChapterIndex + 1] + this.seek(next.start) } else if (this.hasNextItemInQueue) { this.$emit('nextItemInQueue') } @@ -288,26 +374,22 @@ export default { if (this.$refs.trackbar) this.$refs.trackbar.setPercentageReady(1) }, setChunksReady(chunks, numSegments) { - var largestSeg = 0 + let largestSeg = 0 for (let i = 0; i < chunks.length; i++) { - var chunk = chunks[i] - if (typeof chunk === 'string') { - var chunkRange = chunk.split('-').map((c) => Number(c)) - if (chunkRange.length < 2) continue - if (chunkRange[1] > largestSeg) largestSeg = chunkRange[1] - } else if (chunk > largestSeg) { - largestSeg = chunk + const c = chunks[i] + if (typeof c === 'string') { + const r = c.split('-').map((x) => Number(x)) + if (r.length >= 2 && r[1] > largestSeg) largestSeg = r[1] + } else if (c > largestSeg) { + largestSeg = c } } - var percentageReady = largestSeg / numSegments - if (this.$refs.trackbar) this.$refs.trackbar.setPercentageReady(percentageReady) + const p = largestSeg / numSegments + if (this.$refs.trackbar) this.$refs.trackbar.setPercentageReady(p) }, updateTimestamp() { const ts = this.$refs.currentTimestamp - if (!ts) { - console.error('No timestamp el') - return - } + if (!ts) return console.error('No timestamp el') const time = this.useChapterTrack ? Math.max(0, this.currentTime - this.currentChapterStart) : this.currentTime ts.innerText = this.$secondsToTimestamp(time / this.playbackRate) }, @@ -323,7 +405,6 @@ export default { }, init() { this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1 - if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack) this.setPlaybackRate(this.playbackRate) }, @@ -337,7 +418,6 @@ export default { this.toggleFullscreen(false) return } - if (this.loading) return this.$emit('close') }, @@ -357,12 +437,12 @@ export default { mounted() { this.$eventBus.$on('player-hotkey', this.hotkey) this.$eventBus.$on('user-settings', this.settingsUpdated) - this.init() }, beforeDestroy() { this.$eventBus.$off('player-hotkey', this.hotkey) this.$eventBus.$off('user-settings', this.settingsUpdated) + this.cancelFade(false) // 清理定时器 } } diff --git a/package-lock.json b/package-lock.json index ac3b1f6a7..0e0ac4cc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", "lru-cache": "^10.0.3", + "mysql2": "^3.14.2", "node-unrar-js": "^2.0.2", "nodemailer": "^6.9.13", "openid-client": "^5.6.1", @@ -24,7 +25,7 @@ "passport": "^0.6.0", "passport-jwt": "^4.0.1", "semver": "^7.6.3", - "sequelize": "^6.35.2", + "sequelize": "^6.37.7", "socket.io": "^4.5.4", "sqlite3": "^5.1.7", "ssrf-req-filter": "^1.1.0", @@ -907,6 +908,15 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/axios": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", @@ -1557,6 +1567,15 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "optional": true }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2121,6 +2140,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2581,6 +2609,12 @@ "node": ">=8" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -2988,6 +3022,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", @@ -3005,6 +3045,21 @@ "node": "14 || >=16.14" } }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -3541,6 +3596,59 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/mysql2": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.2.tgz", + "integrity": "sha512-YD6mZMeoypmheHT6b2BrVmQFvouEpRICuvPIREulx2OvP1xAxxeqkMQqZSTBefv0PiOBKGYFa2zQtY+gf/4eQw==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/nanoid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", @@ -4493,16 +4601,22 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/sequelize": { - "version": "6.35.2", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.35.2.tgz", - "integrity": "sha512-EdzLaw2kK4/aOnWQ7ed/qh3B6/g+1DvmeXr66RwbcqSm/+QRS9X0LDI5INBibsy4eNJHWIRPo3+QK0zL+IPBHg==", + "version": "6.37.7", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz", + "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==", "funding": [ { "type": "opencollective", "url": "https://opencollective.com/sequelize" } ], + "license": "MIT", "dependencies": { "@types/debug": "^4.1.8", "@types/validator": "^13.7.17", @@ -4976,6 +5090,15 @@ } } }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ssrf-req-filter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ssrf-req-filter/-/ssrf-req-filter-1.1.0.tgz", diff --git a/package.json b/package.json index 87c052c28..16958b803 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", "lru-cache": "^10.0.3", + "mysql2": "^3.14.2", "node-unrar-js": "^2.0.2", "nodemailer": "^6.9.13", "openid-client": "^5.6.1", @@ -52,7 +53,7 @@ "passport": "^0.6.0", "passport-jwt": "^4.0.1", "semver": "^7.6.3", - "sequelize": "^6.35.2", + "sequelize": "^6.37.7", "socket.io": "^4.5.4", "sqlite3": "^5.1.7", "ssrf-req-filter": "^1.1.0",