新增定时暂停播放功能

pull/930/merge
lyswhut 2022-02-14 15:38:27 +08:00
parent c891ed81e1
commit c12be6a1c8
18 changed files with 388 additions and 16 deletions

View File

@ -2,6 +2,7 @@
- 新增“双击列表里的歌曲时自动切换到当前列表播放”设置,此功能仅对歌单、排行榜有效,默认关闭
- 新增打开收藏的在线列表的对应平台详情页功能,可以在我的列表-列表右键菜单中使用
- 新增定时暂停播放功能,由于此功能大多数人可能不常用,所以将其放在设置-基本设置中
### 优化

View File

@ -2,7 +2,7 @@ const path = require('path')
const os = require('os')
const defaultSetting = {
version: '1.0.49',
version: '1.0.50',
player: {
togglePlayMethod: 'listLoop',
highQuality: false,
@ -16,6 +16,8 @@ const defaultSetting = {
isPlayLxlrc: true,
isSavePlayTime: false,
audioVisualization: false,
waitPlayEndStop: true,
waitPlayEndStopTime: 0,
},
desktopLyric: {
enable: false,

View File

@ -11,6 +11,9 @@ const names = {
clear_env_params_deeplink: 'clear_env_params_deeplink',
wait: 'wait',
wait_cancel: 'wait_cancel',
interval: 'interval',
interval_callback: 'interval_callback',
interval_cancel: 'interval_cancel',
open_dev_tools: 'open_dev_tools',
set_music_meta: 'set_music_meta',

View File

@ -143,6 +143,12 @@
"pagination__next": "Next page",
"pagination__page": "Page {num}",
"pagination__prev": "Previous page",
"play_timeout": "Timed pause",
"play_timeout_close": "Close",
"play_timeout_confirm": "Confirm",
"play_timeout_end": "Wait for the song to finish before pausing",
"play_timeout_unit": "minute",
"play_timeout_update": "Update timing",
"player__add_music_to": "Add the current song to...",
"player__buffering": "Buffering...",
"player__desktop_lyric_lock": "Right click to lock lyrics",
@ -322,6 +328,7 @@
"setting__play_quality": "Play 320K quality songs first (if supported)",
"setting__play_save_play_time": "Remember playback progress",
"setting__play_task_bar": "Show playing progress on the taskbar",
"setting__play_timeout": "Timed pause",
"setting__search": "Search",
"setting__search_focus_search_box": "Automatically focus the search box on startup",
"setting__search_history": "Search history",

View File

@ -143,6 +143,14 @@
"pagination__next": "下一页",
"pagination__page": "第 {num} 页",
"pagination__prev": "上一页",
"play_timeout": "定时暂停",
"play_timeout_close": "关闭",
"play_timeout_confirm": "确认",
"play_timeout_end": "等待歌曲播放完毕再暂停",
"play_timeout_stop": "取消定时",
"play_timeout_tip": "{time} 后暂停播放",
"play_timeout_unit": "分钟",
"play_timeout_update": "更新定时",
"player__add_music_to": "添加当前歌曲到...",
"player__buffering": "缓冲中...",
"player__desktop_lyric_lock": "右击锁定歌词",
@ -322,6 +330,7 @@
"setting__play_quality": "优先播放320K品质的歌曲如果支持",
"setting__play_save_play_time": "记住播放进度",
"setting__play_task_bar": "在任务栏上显示当前歌曲播放进度",
"setting__play_timeout": "定时暂停",
"setting__search": "搜索设置",
"setting__search_focus_search_box": "启动时自动聚焦搜索框",
"setting__search_history": "显示历史搜索记录",

View File

@ -143,6 +143,12 @@
"pagination__next": "下一頁",
"pagination__page": "第 {num} 頁",
"pagination__prev": "上一頁",
"play_timeout": "定時暫停",
"play_timeout_close": "關閉",
"play_timeout_confirm": "確認",
"play_timeout_end": "等待歌曲播放完畢再暫停",
"play_timeout_unit": "分鐘",
"play_timeout_update": "更新定時",
"player__add_music_to": "添加當前歌曲到...",
"player__album": "專輯名:",
"player__buffering": "緩衝中...",
@ -322,6 +328,7 @@
"setting__play_quality": "優先播放320K品質的歌曲如果支持",
"setting__play_save_play_time": "記住播放進度",
"setting__play_task_bar": "在任務欄上顯示當前歌曲播放進度",
"setting__play_timeout": "定時暫停",
"setting__search": "搜索設置",
"setting__search_focus_search_box": "啟動時自動聚焦搜索框",
"setting__search_history": "顯示歷史搜索記錄",

View File

@ -1,4 +1,4 @@
const { mainOn, mainHandle, NAMES: { mainWindow: ipcMainWindowNames } } = require('@common/ipc')
const { mainOn, mainHandle, mainSend, NAMES: { mainWindow: ipcMainWindowNames } } = require('@common/ipc')
const timeoutMap = new Map()
@ -23,3 +23,24 @@ mainOn(ipcMainWindowNames.wait_cancel, (event, id) => {
clearTimeout(timeout.timeout)
timeout.reject('cancelled')
})
mainOn(ipcMainWindowNames.interval, (event, { time, id }) => {
if (timeoutMap.has(id)) return
const timeout = setInterval(() => {
if (global.modules.mainWindow) mainSend(global.modules.mainWindow, ipcMainWindowNames.interval_callback, id)
}, time)
timeoutMap.set(id, {
timeout,
type: 'interval',
time,
})
})
mainOn(ipcMainWindowNames.interval_cancel, (event, id) => {
if (!timeoutMap.has(id)) return
const timeout = timeoutMap.get(id)
timeoutMap.delete(id)
if (timeout.type != 'interval') return
clearInterval(timeout.timeout)
})

View File

@ -24,7 +24,7 @@ export default {
default: false,
},
modelValue: {
type: String,
type: [String, Number],
default: '',
},
type: {

View File

@ -4,6 +4,7 @@ import {
import useMediaDevice from './useMediaDevice'
import usePlayerEvent from './usePlayerEvent'
import usePlayer from './usePlayer'
import { init as initPlayTimeoutStop } from '@renderer/utils/timeoutStop'
export default ({ setting }) => {
createAudio()
@ -11,5 +12,7 @@ export default ({ setting }) => {
usePlayerEvent()
useMediaDevice({ setting }) // 初始化音频驱动输出设置
usePlayer({ setting })
initPlayTimeoutStop()
}

View File

@ -49,6 +49,7 @@ export default ({
}
const handleLoadstart = () => {
if (global.isPlayedStop) return
startLoadingTimeout()
setAllStatus(t('player__loading'))
}
@ -77,6 +78,7 @@ export default ({
const handleError = errCode => {
if (!musicInfo.songmid) return
clearLoadingTimeout()
if (global.isPlayedStop) return
if (playMusicInfo.listId != 'download' && errCode !== 1 && retryNum < 2) { // 若音频URL无效则尝试刷新2次URL
// console.log(this.retryNum)
retryNum++
@ -96,6 +98,11 @@ export default ({
clearLoadingTimeout()
}
const handlePlayedStop = () => {
clearDelayNextTimeout()
clearLoadingTimeout()
}
window.eventHub.on(eventPlayerNames.player_loadstart, handleLoadstart)
window.eventHub.on(eventPlayerNames.player_loadeddata, handleLoadeddata)
@ -105,6 +112,7 @@ export default ({
window.eventHub.on(eventPlayerNames.player_emptied, handleEmpied)
window.eventHub.on(eventPlayerNames.error, handleError)
window.eventHub.on(eventPlayerNames.setPlayInfo, handleSetPlayInfo)
window.eventHub.on(eventPlayerNames.playedStop, handlePlayedStop)
onBeforeUnmount(() => {
window.eventHub.off(eventPlayerNames.player_loadstart, handleLoadstart)
@ -115,5 +123,6 @@ export default ({
window.eventHub.off(eventPlayerNames.player_emptied, handleEmpied)
window.eventHub.off(eventPlayerNames.error, handleError)
window.eventHub.off(eventPlayerNames.setPlayInfo, handleSetPlayInfo)
window.eventHub.off(eventPlayerNames.playedStop, handlePlayedStop)
})
}

View File

@ -103,11 +103,13 @@ export default ({ setting }) => {
setAllStatus('Try toggle source...')
},
}).then(url => {
if (global.isPlayedStop) return
if (targetSong !== musicInfoItem.value || isPlay.value || type != getPlayType(setting.value.player.highQuality, musicInfoItem.value)) return
setMusicInfo({ url })
setResource(url)
}).catch(err => {
// console.log('err', err.message)
if (global.isPlayedStop) return
if (targetSong !== musicInfoItem.value || isPlay.value) return
if (err.message == requestMsg.cancelRequest) return
if (!isRetryed) return setUrl(targetSong, isRefresh, true)
@ -198,6 +200,7 @@ export default ({ setting }) => {
const setPauseStatus = () => {
setPlay(false)
setTitle()
if (global.isPlayedStop) handlePause()
}
const setStopStatus = () => {
setPlay(false)
@ -288,11 +291,16 @@ export default ({ setting }) => {
const handleEnded = () => {
setAllStatus(t('player__end'))
if (global.isPlayedStop) return
playNext()
}
// 播放、暂停播放切换
const handleTogglePlay = async() => {
const handlePause = () => {
setPlayerPause()
}
const handlePlay = async() => {
if (playMusicInfo.musicInfo == null) return
if (isPlayerEmpty()) {
if (playMusicInfo.listId == 'download') {
@ -313,13 +321,24 @@ export default ({ setting }) => {
}
return
}
setPlayerPlay()
}
// 播放、暂停播放切换
const handleTogglePlay = () => {
if (global.isPlayedStop) global.isPlayedStop = false
if (isPlay.value) {
setPlayerPause()
handlePause()
} else {
setPlayerPlay()
handlePlay()
}
}
const handlePlayedStop = () => {
clearDelayNextTimeout()
clearLoadTimeout()
}
watch(() => setting.value.player.togglePlayMethod, newValue => {
setLoopPlay(newValue === 'singleLoop')
if (playedList.length) clearPlayedList()
@ -339,13 +358,16 @@ export default ({ setting }) => {
window.eventHub.on(eventPlayerNames.stop, setStopStatus)
window.eventHub.on(eventPlayerNames.playMusic, playMusic)
window.eventHub.on(eventPlayerNames.setPlay, handlePlay)
window.eventHub.on(eventPlayerNames.setPause, handlePause)
window.eventHub.on(eventPlayerNames.setStop, handelStop)
window.eventHub.on(eventPlayerNames.setTogglePlay, handleTogglePlay)
window.eventHub.on(eventPlayerNames.setPlayPrev, playPrev)
window.eventHub.on(eventPlayerNames.setPlayNext, playNext)
window.eventHub.on(eventPlayerNames.setPlayInfo, handleSetPlayInfo)
window.eventHub.on(eventPlayerNames.setStop, handelStop)
window.eventHub.on(eventPlayerNames.player_ended, handleEnded)
window.eventHub.on(eventPlayerNames.playedStop, handlePlayedStop)
onBeforeUnmount(() => {
@ -360,11 +382,14 @@ export default ({ setting }) => {
window.eventHub.off(eventPlayerNames.playMusic, playMusic)
window.eventHub.off(eventPlayerNames.setTogglePlay, handleTogglePlay)
window.eventHub.off(eventPlayerNames.setPlay, handlePlay)
window.eventHub.off(eventPlayerNames.setPause, handlePause)
window.eventHub.off(eventPlayerNames.setStop, handelStop)
window.eventHub.off(eventPlayerNames.setPlayPrev, playPrev)
window.eventHub.off(eventPlayerNames.setPlayNext, playNext)
window.eventHub.off(eventPlayerNames.setPlayInfo, handleSetPlayInfo)
window.eventHub.off(eventPlayerNames.setStop, handelStop)
window.eventHub.off(eventPlayerNames.player_ended, handleEnded)
window.eventHub.off(eventPlayerNames.playedStop, handlePlayedStop)
})
}

View File

@ -15,7 +15,7 @@ export default ({ playNext }) => {
const { playIndex } = updatePlayIndex()
if (playIndex < 0 && !playMusicInfo.isTempPlay) { // 歌曲被移除
if (getList(playMusicInfo.listId).length) {
if (getList(playMusicInfo.listId).length && !global.isPlayedStop) {
playNext()
} else {
window.eventHub.emit(eventPlayerNames.setStop)

View File

@ -15,6 +15,7 @@ const names = {
},
player: {
setTogglePlay: 'setTogglePlay', // 播放/暂停切换
setPlay: 'setPlay', // 播放
setPause: 'setPause', // 暂停
setStop: 'setStop', // 停止
setPlayPrev: 'setPlayPrev', // 上一曲
@ -31,6 +32,7 @@ const names = {
updateLyric: 'updateLyric',
activeTransition: 'activeTransition', // 激活进度条动画事件
playedStop: 'playedStop', // 定时停止事件
// 播放器事件
play: 'play',

View File

@ -32,6 +32,7 @@ const state = {
}
const playMusic = () => {
if (global.isPlayedStop) global.isPlayedStop = false
window.eventHub.emit(eventPlayerNames.playMusic)
}

View File

@ -0,0 +1,93 @@
import { ref, computed } from '@renderer/utils/vueTools'
import { rendererSend, rendererOn, NAMES } from '@common/ipc'
import { isPlay } from '@renderer/core/share/player'
import store from '@renderer/store'
import { player as eventPlayerNames } from '@renderer/event/names'
global.isPlayedStop = false
const time = ref(-1)
const timeoutTools = {
inited: false,
isRunning: false,
timeout: null,
time: -1,
id: 'play__stop__timeout',
exit() {
const setting = store.getters.setting
global.isPlayedStop = true
if (!setting.player.waitPlayEndStop && isPlay.value) {
window.eventHub.emit(eventPlayerNames.setPause)
}
},
clearTimeout() {
rendererSend(NAMES.mainWindow.interval_cancel, this.id)
if (!this.isRunning) return
this.time = -1
time.value = -1
this.isRunning = false
},
start(_time) {
this.clearTimeout()
this.time = _time
time.value = _time
this.isRunning = true
rendererSend(NAMES.mainWindow.interval, {
time: 1000,
id: this.id,
})
},
init() {
if (this.inited) return
this.clearTimeout()
rendererOn(NAMES.mainWindow.interval_callback, (event, id) => {
if (id !== this.id) return
if (this.time > 0) {
this.time--
time.value--
} else {
this.clearTimeout()
this.exit()
}
})
this.inited = true
},
}
export const init = () => {
timeoutTools.init()
}
export const startTimeoutStop = time => {
if (global.isPlayedStop) global.isPlayedStop = false
timeoutTools.start(time)
}
export const stopTimeoutStop = () => {
if (global.isPlayedStop) global.isPlayedStop = false
timeoutTools.clearTimeout()
}
const formatTime = time => {
// let d = parseInt(time / 86400)
// d = d ? d.toString() + ':' : ''
// time = time % 86400
let h = parseInt(time / 3600)
h = h ? h.toString() + ':' : ''
time = time % 3600
const m = parseInt(time / 60).toString().padStart(2, '0')
const s = parseInt(time % 60).toString().padStart(2, '0')
return `${h}${m}:${s}`
}
export const useTimeout = () => {
const timeLabel = computed(() => {
return time.value > 0 ? formatTime(time.value) : ''
})
return {
time,
timeLabel,
}
}

View File

@ -0,0 +1,175 @@
<template lang="pug">
material-modal(:show="modelValue" bg-close @close="handleCloseModal" @after-enter="$refs.dom_input.focus()" teleport="#view")
main(:class="$style.main")
h2 {{$t('play_timeout')}}
div(:class="$style.content")
div(:class="[$style.row, $style.inputGroup]")
base-input(:class="$style.input" ref="dom_input" v-model="time" type="number")
p(:class="$style.inputLabel") {{$t('play_timeout_unit')}}
div(:class="$style.row")
base-checkbox(id="play_timeout_end" v-model="currentStting.player.waitPlayEndStop" :label="$t('play_timeout_end')")
div(:class="[$style.row, $style.tip, { [$style.show]: !!timeLabel }]")
p {{$t('play_timeout_tip', { time: timeLabel })}}
div(:class="$style.footer")
base-btn(:class="$style.footerBtn" @click="handleCancel") {{$t(timeLabel ? 'play_timeout_stop' : 'play_timeout_close')}}
base-btn(:class="$style.footerBtn" @click="handleConfirm") {{$t(timeLabel ? 'play_timeout_update' : 'play_timeout_confirm')}}
</template>
<script>
import { currentStting } from '../setting'
import { useTimeout, startTimeoutStop, stopTimeoutStop } from '@renderer/utils/timeoutStop'
import { ref } from '@renderer/utils/vueTools'
const MAX_MIN = 1440
const rxp = /([1-9]\d*)/
export default {
props: {
modelValue: {
type: Boolean,
default: false,
},
},
setup(props, { emit }) {
const { timeLabel } = useTimeout()
const time = ref(currentStting.value.player.waitPlayEndStopTime)
const handleCloseModal = () => {
emit('update:modelValue', false)
}
const handleCancel = () => {
if (timeLabel.value) {
stopTimeoutStop()
}
handleCloseModal()
}
const verify = () => {
const orgText = time.value
let text = time.value
if (rxp.test(text)) {
text = RegExp.$1
if (parseInt(text) > MAX_MIN) {
text = text.substring(0, text.length - 1)
}
} else {
text = ''
}
time.value = text
return orgText == text ? parseInt(text) : ''
}
const handleConfirm = () => {
let time = verify()
if (time == '') return
currentStting.value.player.waitPlayEndStopTime = time
startTimeoutStop(time * 60)
handleCloseModal()
}
return {
currentStting,
timeLabel,
time,
handleCloseModal,
handleCancel,
handleConfirm,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.main {
padding: 15px;
max-width: 530px;
min-width: 280px;
display: flex;
flex-flow: column nowrap;
justify-content: center;
min-height: 0;
// max-height: 100%;
// overflow: hidden;
h2 {
font-size: 16px;
color: @color-theme_2-font;
line-height: 1.3;
text-align: center;
}
}
.content {
padding-top: 15px;
font-size: 14px;
}
.row {
padding-top: 5px;
}
.inputGroup {
display: flex;
align-items: center;
}
.input {
flex: auto;
}
.inputLabel {
flex: none;
margin-left: 10px;
}
.tip {
visibility: hidden;
&.show {
visibility: visible;
}
}
.footer {
margin-top: 20px;
display: flex;
flex-flow: row nowrap;
}
.footerBtn {
flex: auto;
height: 36px;
line-height: 36px;
padding: 0 10px !important;
width: 150px;
.mixin-ellipsis-1;
+ .footerBtn {
margin-left: 15px;
}
}
.ruleLink {
.mixin-ellipsis-1;
}
each(@themes, {
:global(#root.@{value}) {
.main {
h2 {
color: ~'@{color-@{value}-theme_2-font}';
}
}
.listItem {
&:hover {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
&.active {
background-color: ~'@{color-@{value}-theme_2-active}';
}
h3 {
color: ~'@{color-@{value}-theme_2-font}';
}
p {
color: ~'@{color-@{value}-theme_2-font-label}';
}
}
.noitem {
color: ~'@{color-@{value}-theme_2-font-label}';
}
}
})
</style>

View File

@ -9,12 +9,15 @@ dd
label {{$t('theme_' + theme.className)}}
dd
.gap-top.top
base-checkbox(id="setting_show_animate" v-model="currentStting.isShowAnimation" :label="$t('setting__basic_show_animation')")
.gap-top
base-checkbox(id="setting_animate" v-model="currentStting.randomAnimate" :label="$t('setting__basic_animation')")
.gap-top
base-checkbox(id="setting_to_tray" v-model="currentStting.tray.isShow" :label="$t('setting__basic_to_tray')")
div
.gap-top.top
base-checkbox(id="setting_show_animate" v-model="currentStting.isShowAnimation" :label="$t('setting__basic_show_animation')")
.gap-top
base-checkbox(id="setting_animate" v-model="currentStting.randomAnimate" :label="$t('setting__basic_animation')")
.gap-top
base-checkbox(id="setting_to_tray" v-model="currentStting.tray.isShow" :label="$t('setting__basic_to_tray')")
p.gap-top
base-btn.btn(min @click="isShowPlayTimeoutModal = true") {{$t('setting__play_timeout')}} {{ timeLabel ? ` (${timeLabel})` : '' }}
dd(:tips="$t('setting__basic_source_title')")
h3#basic_source {{$t('setting__basic_source')}}
@ -48,6 +51,7 @@ dd
div
base-checkbox.gap-left(v-for="item in controlBtnPositionList" :key="item.id" :id="`setting_basic_control_btn_position_${item.id}`"
name="setting_basic_control_btn_position" need v-model="currentStting.controlBtnPosition" :value="item.id" :label="item.name")
play-timeout-modal(v-model="isShowPlayTimeoutModal")
user-api-modal(v-model="isShowUserApiModal")
</template>
@ -58,12 +62,15 @@ import { langList } from '@/lang'
import { currentStting } from '../setting'
import { setWindowSize } from '@renderer/utils'
import apiSourceInfo from '@renderer/utils/music/api-source-info'
import { useTimeout } from '@renderer/utils/timeoutStop'
import PlayTimeoutModal from './PlayTimeoutModal'
import UserApiModal from './UserApiModal'
export default {
name: 'SettingBasic',
components: {
PlayTimeoutModal,
UserApiModal,
},
setup() {
@ -81,6 +88,9 @@ export default {
apiSource.value = visible
})
const isShowPlayTimeoutModal = ref(false)
const { timeLabel } = useTimeout()
const isShowUserApiModal = ref(false)
const getApiStatus = () => {
let status
@ -138,6 +148,8 @@ export default {
return {
currentStting,
themes,
isShowPlayTimeoutModal,
timeLabel,
apiSources,
isShowUserApiModal,
windowSizeList,

View File

@ -8,6 +8,8 @@ export const currentStting = ref({
volume: 1,
mediaDeviceId: 'default',
isMediaDeviceRemovedStopPlay: false,
waitPlayEndStop: true,
waitPlayEndStopTime: 0,
},
desktopLyric: {
enable: false,