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