v2.0.0
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import Sortable, { AutoScroll } from 'sortablejs/modular/sortable.core.esm'
|
||||
import { onMounted } from '@renderer/utils/vueTools'
|
||||
import { base as eventBaseName } from '@renderer/event/names'
|
||||
import { onMounted } from '@common/utils/vueTools'
|
||||
import { clearDownKeys } from '@renderer/event'
|
||||
|
||||
Sortable.mount(new AutoScroll())
|
||||
|
||||
@@ -28,13 +28,14 @@ export default ({ dom_list, dragingItemClassName, filter, onUpdate, onStart = no
|
||||
onUnchoose() {
|
||||
onEnd()
|
||||
// 处于拖动状态期间,键盘事件无法监听,拖动结束手动清理按下的键
|
||||
window.eventHub.emit(eventBaseName.setClearDownKeys)
|
||||
// window.app_event.emit(eventBaseName.setClearDownKeys)
|
||||
clearDownKeys()
|
||||
},
|
||||
onStart(event) {
|
||||
window.eventHub.emit(eventBaseName.dragStart)
|
||||
window.app_event.dragStart()
|
||||
},
|
||||
onEnd(event) {
|
||||
window.eventHub.emit(eventBaseName.dragEnd)
|
||||
window.app_event.dragEnd()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useI18n } from '@renderer/utils/vueTools'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
import { dialog } from '@renderer/plugins/Dialog'
|
||||
|
||||
export default () => {
|
||||
const { t } = useI18n()
|
||||
const t = useI18n()
|
||||
|
||||
|
||||
return (type) => {
|
||||
@@ -19,6 +19,7 @@ export default () => {
|
||||
message = t('list_import_tip__alldata')
|
||||
break
|
||||
case 'playListPart':
|
||||
case 'playListPart_v2':
|
||||
message = t('list_import_tip__playlist_part')
|
||||
break
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { onMounted, onBeforeUnmount, ref } from '@renderer/utils/vueTools'
|
||||
|
||||
export default name => {
|
||||
const keyDown = ref(false)
|
||||
const down = `key_${name}_down`
|
||||
const up = `key_${name}_up`
|
||||
|
||||
const handle_key_down = event => {
|
||||
if (!keyDown.value) {
|
||||
// console.log(event)
|
||||
switch (event.event.target.tagName) {
|
||||
case 'INPUT':
|
||||
case 'SELECT':
|
||||
case 'TEXTAREA':
|
||||
return
|
||||
default: if (event.event.target.isContentEditable) return
|
||||
}
|
||||
|
||||
keyDown.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const handle_key_up = () => {
|
||||
if (keyDown.value) keyDown.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.eventHub.on(down, handle_key_down)
|
||||
window.eventHub.on(up, handle_key_up)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.eventHub.off(down, handle_key_down)
|
||||
window.eventHub.off(up, handle_key_up)
|
||||
})
|
||||
|
||||
return keyDown
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { onMounted, onBeforeUnmount, ref } from '@common/utils/vueTools'
|
||||
|
||||
export default (name: string) => {
|
||||
const keyDown = ref(false)
|
||||
const down = `key_${name}_down`
|
||||
const up = `key_${name}_up`
|
||||
|
||||
const handle_key_down = (event: LX.KeyDownEevent) => {
|
||||
if (!keyDown.value) {
|
||||
// console.log(event)
|
||||
switch ((event.event?.target as HTMLElement).tagName) {
|
||||
case 'INPUT':
|
||||
case 'SELECT':
|
||||
case 'TEXTAREA':
|
||||
return
|
||||
default: if ((event.event?.target as HTMLElement).isContentEditable) return
|
||||
}
|
||||
|
||||
keyDown.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const handle_key_up = () => {
|
||||
if (keyDown.value) keyDown.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.key_event.on(down, handle_key_down)
|
||||
window.key_event.on(up, handle_key_up)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.key_event.off(down, handle_key_down)
|
||||
window.key_event.off(up, handle_key_up)
|
||||
})
|
||||
|
||||
return keyDown
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from '@renderer/utils/vueTools'
|
||||
import { scrollTo, throttle, formatPlayTime2 } from '@renderer/utils'
|
||||
import { player as eventPlayerNames } from '@renderer/event/names'
|
||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from '@common/utils/vueTools'
|
||||
import { throttle, formatPlayTime2 } from '@common/utils/common'
|
||||
import { scrollTo } from '@common/utils/renderer'
|
||||
import { play } from '@renderer/core/player/action'
|
||||
// import { player as eventPlayerNames } from '@renderer/event/names'
|
||||
|
||||
export default ({ isPlay, lyric, isShowLyricProgressSetting }) => {
|
||||
export default ({ isPlay, lyric, playProgress, isShowLyricProgressSetting, offset }) => {
|
||||
const dom_lyric = ref(null)
|
||||
const dom_lyric_text = ref(null)
|
||||
const dom_skip_line = ref(null)
|
||||
@@ -28,10 +30,8 @@ export default ({ isPlay, lyric, isShowLyricProgressSetting }) => {
|
||||
if (time == -1) return
|
||||
handleSkipMouseLeave()
|
||||
isStopScroll.value = false
|
||||
let offset = lyric.offset + lyric.tempOffset
|
||||
if (offset) offset = offset / 1000
|
||||
window.eventHub.emit(eventPlayerNames.setProgress, Math.max(time + offset, 0))
|
||||
if (!isPlay.value) window.eventHub.emit(eventPlayerNames.setPlay)
|
||||
window.app_event.setProgress(time)
|
||||
if (!isPlay.value) play()
|
||||
}
|
||||
const handleSkipMouseEnter = () => {
|
||||
isSkipMouseEnter = true
|
||||
@@ -51,13 +51,15 @@ export default ({ isPlay, lyric, isShowLyricProgressSetting }) => {
|
||||
if (dom_pre_line === dom) return
|
||||
if (dom.tagName == 'SPAN') {
|
||||
dom = dom.parentNode.parentNode
|
||||
} else if (dom.classList.contains('font')) {
|
||||
} else if (dom.classList.contains('line')) {
|
||||
dom = dom.parentNode
|
||||
}
|
||||
if (dom.time == null) {
|
||||
if (lyric.lines.length) {
|
||||
time = dom.classList.contains('pre') ? 0 : lyric.lines[lyric.lines.length - 1].time ?? 0
|
||||
if (time) time = time / 1000
|
||||
time = Math.max(time - lyric.offset - lyric.tempOffset, 0)
|
||||
time /= 1000
|
||||
if (time > playProgress.maxPlayTime) time = playProgress.maxPlayTime
|
||||
timeStr.value = formatPlayTime2(time)
|
||||
} else {
|
||||
time = -1
|
||||
@@ -65,7 +67,9 @@ export default ({ isPlay, lyric, isShowLyricProgressSetting }) => {
|
||||
}
|
||||
} else {
|
||||
time = dom.time
|
||||
if (time) time = time / 1000
|
||||
time = Math.max(time - lyric.offset - lyric.tempOffset, 0)
|
||||
time /= 1000
|
||||
if (time > playProgress.maxPlayTime) time = playProgress.maxPlayTime
|
||||
timeStr.value = formatPlayTime2(time)
|
||||
}
|
||||
dom_pre_line = dom
|
||||
@@ -100,31 +104,49 @@ export default ({ isPlay, lyric, isShowLyricProgressSetting }) => {
|
||||
handleScrollLrc()
|
||||
}, 3000)
|
||||
}
|
||||
const handleLyricMouseDown = event => {
|
||||
const handleLyricDown = (y) => {
|
||||
// console.log(event)
|
||||
if (delayScrollTimeout) {
|
||||
clearTimeout(delayScrollTimeout)
|
||||
delayScrollTimeout = null
|
||||
}
|
||||
isMsDown.value = true
|
||||
msDownY = event.clientY
|
||||
msDownY = y
|
||||
msDownScrollY = dom_lyric.value.scrollTop
|
||||
}
|
||||
const handleLyricMouseDown = event => {
|
||||
handleLyricDown(event.clientY)
|
||||
}
|
||||
const handleLyricTouchStart = event => {
|
||||
if (event.changedTouches.length) {
|
||||
const touch = event.changedTouches[0]
|
||||
handleLyricDown(touch.clientY)
|
||||
}
|
||||
}
|
||||
const handleMouseMsUp = event => {
|
||||
isMsDown.value = false
|
||||
}
|
||||
const handleMouseMsMove = event => {
|
||||
const handleMove = (y) => {
|
||||
if (isMsDown.value) {
|
||||
if (!isStopScroll.value) isStopScroll.value = true
|
||||
if (cancelScrollFn) {
|
||||
cancelScrollFn()
|
||||
cancelScrollFn = null
|
||||
}
|
||||
dom_lyric.value.scrollTop = msDownScrollY + msDownY - event.clientY
|
||||
dom_lyric.value.scrollTop = msDownScrollY + msDownY - y
|
||||
startLyricScrollTimeout()
|
||||
setTime()
|
||||
}
|
||||
}
|
||||
const handleMouseMsMove = event => {
|
||||
handleMove(event.clientY)
|
||||
}
|
||||
const handleTouchMove = (e) => {
|
||||
if (e.changedTouches.length) {
|
||||
const touch = e.changedTouches[0]
|
||||
handleMove(touch.clientY)
|
||||
}
|
||||
}
|
||||
|
||||
const handleWheel = (event) => {
|
||||
console.log(event.deltaY)
|
||||
@@ -146,7 +168,7 @@ export default ({ isPlay, lyric, isShowLyricProgressSetting }) => {
|
||||
dom_lyric_text.value.textContent = ''
|
||||
dom_lyric_text.value.appendChild(dom_line_content)
|
||||
nextTick(() => {
|
||||
dom_lines = dom_lyric.value.querySelectorAll('.lrc-content')
|
||||
dom_lines = dom_lyric.value.querySelectorAll('.line-content')
|
||||
handleScrollLrc()
|
||||
})
|
||||
}
|
||||
@@ -189,6 +211,8 @@ export default ({ isPlay, lyric, isShowLyricProgressSetting }) => {
|
||||
onMounted(() => {
|
||||
document.addEventListener('mousemove', handleMouseMsMove)
|
||||
document.addEventListener('mouseup', handleMouseMsUp)
|
||||
document.addEventListener('touchmove', handleTouchMove)
|
||||
document.addEventListener('touchend', handleMouseMsUp)
|
||||
|
||||
initLrc(lyric.lines, null)
|
||||
nextTick(() => {
|
||||
@@ -199,6 +223,8 @@ export default ({ isPlay, lyric, isShowLyricProgressSetting }) => {
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('mousemove', handleMouseMsMove)
|
||||
document.removeEventListener('mouseup', handleMouseMsUp)
|
||||
document.removeEventListener('touchmove', handleTouchMove)
|
||||
document.removeEventListener('touchend', handleMouseMsUp)
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -209,9 +235,11 @@ export default ({ isPlay, lyric, isShowLyricProgressSetting }) => {
|
||||
isMsDown,
|
||||
timeStr,
|
||||
handleLyricMouseDown,
|
||||
handleLyricTouchStart,
|
||||
handleWheel,
|
||||
handleSkipPlay,
|
||||
handleSkipMouseEnter,
|
||||
handleSkipMouseLeave,
|
||||
handleScrollLrc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { onMounted, onBeforeUnmount, watch, reactive, ref, nextTick } from '@renderer/utils/vueTools'
|
||||
import { onMounted, onBeforeUnmount, watch, reactive, ref } from '@common/utils/vueTools'
|
||||
import { isFullscreen } from '@renderer/store'
|
||||
|
||||
let rootOffset = window.dt ? 0 : 8
|
||||
|
||||
export default ({ visible, location, onHide }) => {
|
||||
const transition1 = 'transform, opacity'
|
||||
@@ -10,17 +13,20 @@ export default ({ visible, location, onHide }) => {
|
||||
top: 0,
|
||||
opacity: 0,
|
||||
transitionProperty: 'transform, opacity',
|
||||
transform: 'scale(0) translate(0,0)',
|
||||
transform: 'scale(.8, .7) translate(0,0)',
|
||||
pointerEvents: 'none',
|
||||
})
|
||||
|
||||
const handleShow = () => {
|
||||
show = true
|
||||
menuStyles.opacity = 1
|
||||
menuStyles.transform = `scaleY(1) translate(${handleGetOffsetXY(location.value.x, location.value.y)})`
|
||||
menuStyles.transform = `scale(1) translate(${handleGetOffsetXY(location.value.x, location.value.y)})`
|
||||
menuStyles.pointerEvents = 'auto'
|
||||
}
|
||||
const handleHide = () => {
|
||||
menuStyles.opacity = 0
|
||||
menuStyles.transform = 'scale(0) translate(0, 0)'
|
||||
menuStyles.transform = 'scale(.8, .7) translate(0, 0)'
|
||||
menuStyles.pointerEvents = 'none'
|
||||
show = false
|
||||
}
|
||||
const handleGetOffsetXY = (left, top) => {
|
||||
@@ -33,8 +39,8 @@ export default ({ visible, location, onHide }) => {
|
||||
const offsetHeight = containerHeight - top - listHeight
|
||||
let x = 0
|
||||
let y = 0
|
||||
if (containerWidth > listWidth && offsetWidth < 17) {
|
||||
x = offsetWidth - 17
|
||||
if (containerWidth > listWidth && offsetWidth < 12) {
|
||||
x = offsetWidth - 12
|
||||
}
|
||||
if (containerHeight > listHeight && offsetHeight < 5) {
|
||||
y = offsetHeight - 5
|
||||
@@ -55,15 +61,19 @@ export default ({ visible, location, onHide }) => {
|
||||
visible ? handleShow() : handleHide()
|
||||
}, { immediate: true })
|
||||
|
||||
watch(isFullscreen, isFullscreen => {
|
||||
rootOffset = window.dt || isFullscreen ? 0 : 8
|
||||
}, { immediate: true })
|
||||
|
||||
watch(location, location => {
|
||||
menuStyles.left = location.x + 2 + 'px'
|
||||
menuStyles.top = location.y + 'px'
|
||||
nextTick(() => {
|
||||
if (show) {
|
||||
if (menuStyles.transitionProperty != transition2) menuStyles.transitionProperty = transition2
|
||||
menuStyles.transform = `scaleY(1) translate(${handleGetOffsetXY(location.x, location.y)})`
|
||||
}
|
||||
})
|
||||
menuStyles.left = location.x - rootOffset + 2 + 'px'
|
||||
menuStyles.top = location.y - rootOffset + 'px'
|
||||
// nextTick(() => {
|
||||
if (show) {
|
||||
if (menuStyles.transitionProperty != transition2) menuStyles.transitionProperty = transition2
|
||||
menuStyles.transform = `scale(1) translate(${handleGetOffsetXY(location.x, location.y)})`
|
||||
}
|
||||
// })
|
||||
}, { deep: true })
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
+11
-10
@@ -1,20 +1,21 @@
|
||||
import { appSetting, setTogglePlayMode } from '@renderer/store/setting'
|
||||
import {
|
||||
computed,
|
||||
useCommit,
|
||||
} from '@renderer/utils/vueTools'
|
||||
} from '@common/utils/vueTools'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
|
||||
const playNextModes = [
|
||||
'listLoop',
|
||||
'random',
|
||||
'list',
|
||||
'singleLoop',
|
||||
]
|
||||
|
||||
export default ({ setting, t }) => {
|
||||
const setPlayNextMode = useCommit('setPlayNextMode')
|
||||
'none',
|
||||
] as const
|
||||
|
||||
export default () => {
|
||||
const t = useI18n()
|
||||
const nextTogglePlayName = computed(() => {
|
||||
switch (setting.value.player.togglePlayMethod) {
|
||||
switch (appSetting['player.togglePlayMethod']) {
|
||||
case 'listLoop': return t('player__play_toggle_mode_list_loop')
|
||||
case 'random': return t('player__play_toggle_mode_random')
|
||||
case 'singleLoop': return t('player__play_toggle_mode_single_loop')
|
||||
@@ -24,9 +25,9 @@ export default ({ setting, t }) => {
|
||||
})
|
||||
|
||||
const toggleNextPlayMode = () => {
|
||||
let index = playNextModes.indexOf(setting.value.player.togglePlayMethod)
|
||||
if (++index >= playNextModes.length) index = -1
|
||||
setPlayNextMode(playNextModes[index] || '')
|
||||
let index = playNextModes.indexOf(appSetting['player.togglePlayMethod'])
|
||||
if (++index >= playNextModes.length) index = 0
|
||||
setTogglePlayMode(playNextModes[index] ?? playNextModes[0])
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ref, onBeforeUnmount, toRef } from '@renderer/utils/vueTools'
|
||||
import { player as eventPlayerNames } from '@renderer/event/names'
|
||||
import { playProgress } from '@renderer/core/share/playProgress'
|
||||
import { ref, onBeforeUnmount, toRef } from '@common/utils/vueTools'
|
||||
import { playProgress } from '@renderer/store/player/playProgress'
|
||||
|
||||
export default () => {
|
||||
const isActiveTransition = ref(false)
|
||||
@@ -15,10 +14,10 @@ export default () => {
|
||||
isActiveTransition.value = true
|
||||
}
|
||||
|
||||
window.eventHub.on(eventPlayerNames.activeTransition, handleActiveTransition)
|
||||
window.app_event.on('activePlayProgressTransition', handleActiveTransition)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.eventHub.off(eventPlayerNames.activeTransition, handleActiveTransition)
|
||||
window.app_event.off('activePlayProgressTransition', handleActiveTransition)
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { setListUpdateTime } from '@renderer/utils/data'
|
||||
import { setFetchingListStatus } from '@renderer/core/share/list'
|
||||
import { useAction, useCommit } from '@renderer/utils/vueTools'
|
||||
|
||||
export default () => {
|
||||
const getBoardListAll = useAction('leaderboard', 'getListAll')
|
||||
const getListDetailAll = useAction('songList', 'getListDetailAll')
|
||||
const setList = useCommit('list', 'setList')
|
||||
|
||||
const fetchList = (id, source, sourceListId) => {
|
||||
setFetchingListStatus(id, true)
|
||||
|
||||
let promise
|
||||
if (/board__/.test(sourceListId)) {
|
||||
const id = sourceListId.replace(/board__/, '')
|
||||
promise = getBoardListAll({ id, isRefresh: true })
|
||||
} else {
|
||||
promise = getListDetailAll({ source, id: sourceListId, isRefresh: true })
|
||||
}
|
||||
return promise.finally(() => {
|
||||
setFetchingListStatus(id, false)
|
||||
})
|
||||
}
|
||||
|
||||
return async targetListInfo => {
|
||||
const list = await fetchList(targetListInfo.id, targetListInfo.source, targetListInfo.sourceListId)
|
||||
// console.log(targetListInfo.list.length, list.length)
|
||||
setList({
|
||||
...targetListInfo,
|
||||
list,
|
||||
})
|
||||
setListUpdateTime(targetListInfo.id, Date.now())
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,29 @@
|
||||
import {
|
||||
computed,
|
||||
useCommit,
|
||||
} from '@renderer/utils/vueTools'
|
||||
} from '@common/utils/vueTools'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
import { appSetting, setLockDesktopLyric, setVisibleDesktopLyric } from '@renderer/store/setting'
|
||||
|
||||
export default ({ setting, t }) => {
|
||||
const setVisibleDesktopLyric = useCommit('setVisibleDesktopLyric')
|
||||
const setLockDesktopLyric = useCommit('setLockDesktopLyric')
|
||||
export default () => {
|
||||
const t = useI18n()
|
||||
|
||||
const toggleDesktopLyricBtnTitle = computed(() => {
|
||||
return `${
|
||||
setting.value.desktopLyric.enable
|
||||
appSetting['desktopLyric.enable']
|
||||
? t('player__desktop_lyric_off')
|
||||
: t('player__desktop_lyric_on')
|
||||
}(${
|
||||
setting.value.desktopLyric.isLock
|
||||
appSetting['desktopLyric.isLock']
|
||||
? t('player__desktop_lyric_unlock')
|
||||
: t('player__desktop_lyric_lock')
|
||||
})`
|
||||
})
|
||||
|
||||
const toggleDesktopLyric = () => {
|
||||
setVisibleDesktopLyric(!setting.value.desktopLyric.enable)
|
||||
setVisibleDesktopLyric(!appSetting['desktopLyric.enable'])
|
||||
}
|
||||
const toggleLockDesktopLyric = () => {
|
||||
setLockDesktopLyric(!setting.value.desktopLyric.isLock)
|
||||
setLockDesktopLyric(!appSetting['desktopLyric.isLock'])
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { rendererSend, rendererInvoke, NAMES } from '../../common/ipc'
|
||||
import { throttle } from './index'
|
||||
|
||||
let listPosition = {}
|
||||
let listPrevSelectId
|
||||
let listUpdateInfo = {}
|
||||
|
||||
const saveListPosition = throttle(() => {
|
||||
rendererSend(NAMES.mainWindow.save_data, {
|
||||
path: 'listPosition',
|
||||
data: listPosition,
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
export const initListPosition = () => {
|
||||
return rendererInvoke(NAMES.mainWindow.get_data, 'listPosition').then(data => {
|
||||
if (!data) data = {}
|
||||
listPosition = data
|
||||
})
|
||||
}
|
||||
export const getListPosition = id => listPosition[id] || 0
|
||||
export const getListPositionAll = () => listPosition
|
||||
export const setListPositionAll = positions => {
|
||||
listPosition = positions
|
||||
saveListPosition()
|
||||
}
|
||||
export const setListPosition = (id, position) => {
|
||||
listPosition[id] = position || 0
|
||||
saveListPosition()
|
||||
}
|
||||
export const removeListPosition = id => {
|
||||
delete listPosition[id]
|
||||
saveListPosition()
|
||||
}
|
||||
|
||||
const saveListPrevSelectId = throttle(() => {
|
||||
rendererSend(NAMES.mainWindow.save_data, {
|
||||
path: 'listPrevSelectId',
|
||||
data: listPrevSelectId,
|
||||
})
|
||||
}, 200)
|
||||
export const initListPrevSelectId = () => {
|
||||
return rendererInvoke(NAMES.mainWindow.get_data, 'listPrevSelectId').then(id => {
|
||||
listPrevSelectId = id
|
||||
})
|
||||
}
|
||||
export const getListPrevSelectId = () => listPrevSelectId
|
||||
export const setListPrevSelectId = id => {
|
||||
listPrevSelectId = id
|
||||
saveListPrevSelectId()
|
||||
}
|
||||
|
||||
export const initListUpdateInfo = () => {
|
||||
return rendererInvoke(NAMES.mainWindow.get_data, 'listUpdateInfo').then(data => {
|
||||
if (!data) data = {}
|
||||
// console.log(data)
|
||||
listUpdateInfo = data
|
||||
})
|
||||
}
|
||||
const saveListUpdateInfo = throttle(() => {
|
||||
rendererSend(NAMES.mainWindow.save_data, {
|
||||
path: 'listUpdateInfo',
|
||||
data: listUpdateInfo,
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
export const getListUpdateInfo = () => listUpdateInfo
|
||||
export const setListUpdateInfo = info => {
|
||||
listUpdateInfo = info
|
||||
saveListUpdateInfo()
|
||||
}
|
||||
export const setListAutoUpdate = (id, enable) => {
|
||||
const targetInfo = listUpdateInfo[id] ?? { updateTime: '', isAutoUpdate: false }
|
||||
targetInfo.isAutoUpdate = enable
|
||||
listUpdateInfo[id] = targetInfo
|
||||
saveListUpdateInfo()
|
||||
}
|
||||
export const setListUpdateTime = (id, time) => {
|
||||
const targetInfo = listUpdateInfo[id] ?? { updateTime: '', isAutoUpdate: false }
|
||||
targetInfo.updateTime = time
|
||||
listUpdateInfo[id] = targetInfo
|
||||
saveListUpdateInfo()
|
||||
}
|
||||
// export const setListUpdateInfo = (id, { updateTime, isAutoUpdate }) => {
|
||||
// listUpdateInfo[id] = { updateTime, isAutoUpdate }
|
||||
// saveListUpdateInfo()
|
||||
// }
|
||||
export const removeListUpdateInfo = id => {
|
||||
delete listUpdateInfo[id]
|
||||
saveListUpdateInfo()
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/* eslint-disable @typescript-eslint/no-dynamic-delete */
|
||||
import {
|
||||
saveListPositionInfo as saveListPositionInfoFromData,
|
||||
getListPositionInfo as getListPositionInfoFromData,
|
||||
saveListPrevSelectId as saveListPrevSelectIdFromData,
|
||||
getListPrevSelectId as getListPrevSelectIdFromData,
|
||||
saveListUpdateInfo as saveListUpdateInfoFromData,
|
||||
getListUpdateInfo as getListUpdateInfoFromData,
|
||||
saveSearchSetting as saveSearchSettingFromData,
|
||||
getSearchSetting as getSearchSettingFromData,
|
||||
saveSongListSetting as saveSongListSettingFromData,
|
||||
getSongListSetting as getSongListSettingFromData,
|
||||
saveLeaderboardSetting as saveLeaderboardSettingFromData,
|
||||
getLeaderboardSetting as getLeaderboardSettingFromData,
|
||||
saveViewPrevState as saveViewPrevStateFromData,
|
||||
} from '@renderer/utils/ipc'
|
||||
import { throttle } from '@common/utils'
|
||||
import { DEFAULT_SETTING, LIST_IDS } from '@common/constants'
|
||||
import { dateFormat } from './index'
|
||||
import { setUpdateTime } from '@renderer/store/list/action'
|
||||
|
||||
let listPosition: LX.List.ListPositionInfo
|
||||
let listPrevSelectId: string
|
||||
let listUpdateInfo: LX.List.ListUpdateInfo
|
||||
|
||||
let searchSetting: typeof DEFAULT_SETTING['search']
|
||||
let songListSetting: typeof DEFAULT_SETTING['songList']
|
||||
let leaderboardSetting: typeof DEFAULT_SETTING['leaderboard']
|
||||
|
||||
const saveListPositionThrottle = throttle(() => {
|
||||
saveListPositionInfoFromData(listPosition)
|
||||
}, 1000)
|
||||
const saveSearchSettingThrottle = throttle(() => {
|
||||
saveSearchSettingFromData(searchSetting)
|
||||
}, 1000)
|
||||
const saveSongListSettingThrottle = throttle(() => {
|
||||
saveSongListSettingFromData(songListSetting)
|
||||
}, 1000)
|
||||
const saveLeaderboardSettingThrottle = throttle(() => {
|
||||
saveLeaderboardSettingFromData(leaderboardSetting)
|
||||
}, 1000)
|
||||
const saveViewPrevStateThrottle = throttle((state) => {
|
||||
saveViewPrevStateFromData(state)
|
||||
}, 1000)
|
||||
|
||||
|
||||
export const getListPosition = async(id: string): Promise<number> => {
|
||||
if (listPosition == null) listPosition = await getListPositionInfoFromData() ?? {}
|
||||
return listPosition[id] ?? 0
|
||||
}
|
||||
export const setListPosition = async(id: string, position?: number) => {
|
||||
if (listPosition == null) await getListPosition(id)
|
||||
listPosition[id] = position ?? 0
|
||||
saveListPositionThrottle()
|
||||
}
|
||||
export const removeListPosition = (id: string) => {
|
||||
delete listPosition[id]
|
||||
saveListPositionThrottle()
|
||||
}
|
||||
export const overwriteListPosition = (ids: string[]) => {
|
||||
const removedIds = []
|
||||
for (const id of Object.keys(listPosition)) {
|
||||
if (ids.includes(id)) continue
|
||||
removedIds.push(id)
|
||||
}
|
||||
for (const id of removedIds) delete listPosition[id]
|
||||
saveListPositionThrottle()
|
||||
}
|
||||
|
||||
const saveListPrevSelectIdThrottle = throttle(() => {
|
||||
saveListPrevSelectIdFromData(listPrevSelectId)
|
||||
}, 200)
|
||||
export const getListPrevSelectId = async() => {
|
||||
if (listPrevSelectId == null) listPrevSelectId = await getListPrevSelectIdFromData() ?? LIST_IDS.DEFAULT
|
||||
return listPrevSelectId ?? LIST_IDS.DEFAULT
|
||||
}
|
||||
export const saveListPrevSelectId = (id: string) => {
|
||||
listPrevSelectId = id
|
||||
saveListPrevSelectIdThrottle()
|
||||
}
|
||||
|
||||
const saveListUpdateInfo = throttle(() => {
|
||||
saveListUpdateInfoFromData(listUpdateInfo)
|
||||
}, 1000)
|
||||
|
||||
export const getListUpdateInfo = async() => {
|
||||
if (listUpdateInfo == null) {
|
||||
listUpdateInfo = await getListUpdateInfoFromData() ?? {}
|
||||
for (const [id, info] of Object.entries(listUpdateInfo)) {
|
||||
setUpdateTime(id, info.updateTime ? dateFormat(info.updateTime) : '')
|
||||
}
|
||||
}
|
||||
return listUpdateInfo
|
||||
}
|
||||
export const setListUpdateInfo = async(info: LX.List.ListUpdateInfo) => {
|
||||
if (listUpdateInfo == null) await getListUpdateInfo()
|
||||
listUpdateInfo = info
|
||||
saveListUpdateInfo()
|
||||
}
|
||||
export const setListAutoUpdate = async(id: string, enable: boolean) => {
|
||||
if (listUpdateInfo == null) await getListUpdateInfo()
|
||||
const targetInfo = listUpdateInfo[id] ?? { updateTime: 0, isAutoUpdate: false }
|
||||
targetInfo.isAutoUpdate = enable
|
||||
listUpdateInfo[id] = targetInfo
|
||||
saveListUpdateInfo()
|
||||
}
|
||||
export const setListUpdateTime = async(id: string, time: number) => {
|
||||
if (listUpdateInfo == null) await getListUpdateInfo()
|
||||
const targetInfo = listUpdateInfo[id] ?? { updateTime: 0, isAutoUpdate: false }
|
||||
targetInfo.updateTime = time
|
||||
listUpdateInfo[id] = targetInfo
|
||||
saveListUpdateInfo()
|
||||
}
|
||||
// export const setListUpdateInfo = (id, { updateTime, isAutoUpdate }) => {
|
||||
// listUpdateInfo[id] = { updateTime, isAutoUpdate }
|
||||
// saveListUpdateInfo()
|
||||
// }
|
||||
export const removeListUpdateInfo = (id: string) => {
|
||||
delete listUpdateInfo[id]
|
||||
saveListUpdateInfo()
|
||||
}
|
||||
export const overwriteListUpdateInfo = (ids: string[]) => {
|
||||
const removedIds = []
|
||||
for (const id of Object.keys(listPosition)) {
|
||||
if (ids.includes(id)) continue
|
||||
removedIds.push(id)
|
||||
}
|
||||
for (const id of removedIds) delete listUpdateInfo[id]
|
||||
saveListUpdateInfo()
|
||||
}
|
||||
|
||||
|
||||
export const getSearchSetting = async() => {
|
||||
if (!searchSetting) searchSetting = await getSearchSettingFromData()
|
||||
return { ...searchSetting }
|
||||
}
|
||||
export const setSearchSetting = async(setting: Partial<typeof DEFAULT_SETTING['search']>) => {
|
||||
if (!searchSetting) await getSearchSetting()
|
||||
let requiredSave = false
|
||||
if (setting.source && searchSetting.source != setting.source) requiredSave = true
|
||||
if (setting.type && searchSetting.type != setting.type) requiredSave = true
|
||||
if (setting.temp_source && searchSetting.temp_source != setting.temp_source) requiredSave = true
|
||||
|
||||
if (!requiredSave) return
|
||||
searchSetting = Object.assign(searchSetting, setting)
|
||||
saveSearchSettingThrottle()
|
||||
}
|
||||
|
||||
export const getSongListSetting = async() => {
|
||||
if (!songListSetting) songListSetting = await getSongListSettingFromData()
|
||||
return { ...songListSetting }
|
||||
}
|
||||
export const setSongListSetting = async(setting: Partial<typeof DEFAULT_SETTING['songList']>) => {
|
||||
if (!songListSetting) await getSongListSetting()
|
||||
songListSetting = Object.assign(songListSetting, setting)
|
||||
saveSongListSettingThrottle()
|
||||
}
|
||||
|
||||
export const getLeaderboardSetting = async() => {
|
||||
if (!leaderboardSetting) leaderboardSetting = await getLeaderboardSettingFromData()
|
||||
return { ...leaderboardSetting }
|
||||
}
|
||||
export const setLeaderboardSetting = async(setting: Partial<typeof DEFAULT_SETTING['leaderboard']>) => {
|
||||
if (!leaderboardSetting) await getLeaderboardSetting()
|
||||
leaderboardSetting = Object.assign(leaderboardSetting, setting)
|
||||
saveLeaderboardSettingThrottle()
|
||||
}
|
||||
|
||||
export const saveViewPrevState = (state: typeof DEFAULT_SETTING['viewPrevState']) => {
|
||||
saveViewPrevStateThrottle(state)
|
||||
}
|
||||
@@ -1,334 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import request from 'request'
|
||||
import { EventEmitter } from 'events'
|
||||
import { performance } from 'perf_hooks'
|
||||
import { STATUS } from './util'
|
||||
|
||||
|
||||
const defaultChunkInfo = {
|
||||
path: null,
|
||||
startByte: 0,
|
||||
endByte: '',
|
||||
}
|
||||
|
||||
const defaultRequestOptions = {
|
||||
method: 'GET',
|
||||
headers: {},
|
||||
}
|
||||
const defaultOptions = {
|
||||
|
||||
}
|
||||
|
||||
class Task extends EventEmitter {
|
||||
/**
|
||||
*
|
||||
* @param {String} url download url
|
||||
* @param {Object} chunkInfo
|
||||
* @param {Object} options
|
||||
*/
|
||||
constructor(url, savePath, filename, options = {}) {
|
||||
super()
|
||||
|
||||
this.resumeLastChunk = null
|
||||
this.downloadUrl = url
|
||||
this.chunkInfo = Object.assign({}, defaultChunkInfo, {
|
||||
path: path.join(savePath, filename),
|
||||
startByte: 0,
|
||||
})
|
||||
if (!this.chunkInfo.endByte) this.chunkInfo.endByte = ''
|
||||
|
||||
this.options = Object.assign({}, defaultOptions, options)
|
||||
|
||||
this.requestOptions = Object.assign({}, defaultRequestOptions, this.options.requestOptions || {})
|
||||
if (!this.requestOptions.headers) this.requestOptions.headers = {}
|
||||
|
||||
this.progress = {
|
||||
total: 0,
|
||||
downloaded: 0,
|
||||
speed: 0,
|
||||
}
|
||||
this.statsEstimate = {
|
||||
time: 0,
|
||||
bytes: 0,
|
||||
prevBytes: 0,
|
||||
}
|
||||
this.status = STATUS.idle
|
||||
}
|
||||
|
||||
__init() {
|
||||
this.status = STATUS.init
|
||||
const { path, startByte, endByte } = this.chunkInfo
|
||||
this.progress.downloaded = 0
|
||||
if (startByte != null) this.requestOptions.headers.range = `bytes=${startByte}-${endByte}`
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!path) return resolve()
|
||||
fs.stat(path, (errStat, stats) => {
|
||||
if (errStat) {
|
||||
if (errStat.code !== 'ENOENT') {
|
||||
this.__handleError(errStat)
|
||||
reject(errStat)
|
||||
return
|
||||
}
|
||||
} else if (stats.size >= 10) {
|
||||
fs.open(path, 'r', (errOpen, fd) => {
|
||||
if (errOpen) {
|
||||
this.__handleError(errOpen)
|
||||
reject(errOpen)
|
||||
return
|
||||
}
|
||||
fs.read(fd, Buffer.alloc(10), 0, 10, stats.size - 10, (errRead, bytesRead, buffer) => {
|
||||
if (errRead) {
|
||||
this.__handleError(errRead)
|
||||
reject(errRead)
|
||||
return
|
||||
}
|
||||
fs.close(fd, errClose => {
|
||||
if (errClose) {
|
||||
this.__handleError(errClose)
|
||||
reject(errClose)
|
||||
return
|
||||
}
|
||||
|
||||
// resume download
|
||||
// console.log(buffer)
|
||||
this.resumeLastChunk = buffer
|
||||
this.progress.downloaded = stats.size
|
||||
this.requestOptions.headers.range = `bytes=${stats.size - 10}-${endByte || ''}`
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
__httpFetch(url, options) {
|
||||
// console.log(options)
|
||||
this.request = request(url, options)
|
||||
.on('response', response => {
|
||||
if (response.statusCode !== 200 && response.statusCode !== 206) {
|
||||
if (response.statusCode == 416) {
|
||||
fs.unlink(this.chunkInfo.path, async err => {
|
||||
await this.__handleError(new Error(response.statusMessage))
|
||||
this.chunkInfo.startByte = 0
|
||||
this.resumeLastChunk = null
|
||||
this.progress.downloaded = 0
|
||||
if (err) this.__handleError(err)
|
||||
})
|
||||
return
|
||||
}
|
||||
this.status = STATUS.failed
|
||||
this.emit('fail', response)
|
||||
this.__closeRequest()
|
||||
this.__closeWriteStream()
|
||||
return
|
||||
}
|
||||
this.emit('response', response)
|
||||
try {
|
||||
this.__initDownload(response)
|
||||
} catch (error) {
|
||||
return this.__handleError(error)
|
||||
}
|
||||
this.status = STATUS.running
|
||||
response
|
||||
.on('data', this.__handleWriteData.bind(this))
|
||||
.on('error', err => this.__handleError(err))
|
||||
.on('end', () => {
|
||||
if (response.complete) {
|
||||
this.__handleComplete()
|
||||
} else {
|
||||
// this.__handleError(new Error('The connection was terminated while the message was still being sent'))
|
||||
this.stop()
|
||||
}
|
||||
})
|
||||
})
|
||||
.on('error', err => this.__handleError(err))
|
||||
.on('close', () => this.__closeWriteStream())
|
||||
}
|
||||
|
||||
__initDownload(response) {
|
||||
this.progress.total = parseInt(response.headers['content-length'] || 0)
|
||||
let options = {}
|
||||
let isResumable = this.options.forceResume || response.headers['accept-ranges'] !== 'none' || (typeof response.headers['accept-ranges'] == 'string' && parseInt(response.headers['accept-ranges'].replace(/^bytes=(\d+)/, '$1')) > 0)
|
||||
if (isResumable) {
|
||||
options.flags = 'a'
|
||||
if (this.progress.downloaded) this.progress.total -= 10
|
||||
} else {
|
||||
if (this.chunkInfo.startByte > 0) return this.__handleError(new Error('The resource cannot be resumed download.'))
|
||||
}
|
||||
this.progress.total += this.progress.downloaded
|
||||
this.statsEstimate.prevBytes = this.progress.downloaded
|
||||
if (!this.chunkInfo.path) return this.__handleError(new Error('Chunk save Path is not set.'))
|
||||
this.ws = fs.createWriteStream(this.chunkInfo.path, options)
|
||||
|
||||
this.ws.on('finish', () => this.__closeWriteStream())
|
||||
this.ws.on('error', err => {
|
||||
fs.unlink(this.chunkInfo.path, async unlinkErr => {
|
||||
await this.__handleError(err)
|
||||
this.chunkInfo.startByte = 0
|
||||
this.resumeLastChunk = null
|
||||
this.progress.downloaded = 0
|
||||
if (unlinkErr && unlinkErr.code !== 'ENOENT') this.__handleError(unlinkErr)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
__handleComplete() {
|
||||
if (this.status == STATUS.error) return
|
||||
this.__closeWriteStream().then(() => {
|
||||
if (this.progress.downloaded == this.progress.total) {
|
||||
this.status = STATUS.completed
|
||||
this.emit('completed')
|
||||
} else {
|
||||
this.status = STATUS.stopped
|
||||
this.emit('stop')
|
||||
}
|
||||
})
|
||||
console.log('end')
|
||||
}
|
||||
|
||||
__handleError(error) {
|
||||
if (this.status == STATUS.error) return
|
||||
this.status = STATUS.error
|
||||
this.__closeRequest()
|
||||
this.__closeWriteStream()
|
||||
this.emit('error', error)
|
||||
}
|
||||
|
||||
__closeWriteStream() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.ws) return resolve()
|
||||
console.log('close write stream')
|
||||
this.ws.close(err => {
|
||||
if (err) {
|
||||
this.status = STATUS.error
|
||||
this.emit('error', err)
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
this.ws = null
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
__closeRequest() {
|
||||
if (!this.request) return
|
||||
console.log('close request')
|
||||
this.request.abort()
|
||||
this.request = null
|
||||
}
|
||||
|
||||
__handleWriteData(chunk) {
|
||||
if (this.resumeLastChunk) {
|
||||
chunk = this.__handleDiffChunk(chunk)
|
||||
if (!chunk) {
|
||||
this.__handleStop().finally(() => {
|
||||
this.__handleError(new Error('Resume failed, response chunk does not match.'))
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
// console.log('data', chunk)
|
||||
if (this.status == STATUS.stopped || this.ws == null) return console.log('cancel write')
|
||||
this.__calculateProgress(chunk.length)
|
||||
this.ws.write(chunk, err => {
|
||||
if (!err) return
|
||||
console.log(err)
|
||||
this.__handleError(err)
|
||||
this.stop()
|
||||
})
|
||||
}
|
||||
|
||||
__handleDiffChunk(chunk) {
|
||||
// console.log('diff', chunk)
|
||||
let resumeLastChunkLen = this.resumeLastChunk.length
|
||||
let chunkLen = chunk.length
|
||||
let isOk
|
||||
if (chunkLen >= resumeLastChunkLen) {
|
||||
isOk = chunk.slice(0, resumeLastChunkLen).toString('hex') === this.resumeLastChunk.toString('hex')
|
||||
if (!isOk) return null
|
||||
|
||||
this.resumeLastChunk = null
|
||||
return chunk.slice(resumeLastChunkLen)
|
||||
} else {
|
||||
isOk = chunk.slice(0, chunkLen).toString('hex') === this.resumeLastChunk.slice(0, chunkLen).toString('hex')
|
||||
if (!isOk) return null
|
||||
this.resumeLastChunk = this.resumeLastChunk.slice(chunkLen)
|
||||
return chunk.slice(chunkLen)
|
||||
}
|
||||
}
|
||||
|
||||
__handleStop() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.request) {
|
||||
this.request.abort()
|
||||
this.request = null
|
||||
}
|
||||
if (this.ws) {
|
||||
this.ws.close(err => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
this.emit('error', err)
|
||||
return
|
||||
}
|
||||
this.ws = null
|
||||
resolve()
|
||||
})
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
__calculateProgress(receivedBytes) {
|
||||
const currentTime = performance.now()
|
||||
const elaspsedTime = currentTime - this.statsEstimate.time
|
||||
|
||||
const progress = this.progress
|
||||
progress.downloaded += receivedBytes
|
||||
progress.progress = progress.total ? (progress.downloaded / progress.total) * 100 : -1
|
||||
|
||||
|
||||
// emit the progress every second or if finished
|
||||
if (progress.downloaded === progress.total || elaspsedTime > 1000) {
|
||||
this.statsEstimate.time = currentTime
|
||||
this.statsEstimate.bytes = progress.downloaded - this.statsEstimate.prevBytes
|
||||
this.statsEstimate.prevBytes = progress.downloaded
|
||||
this.emit('progress', {
|
||||
total: progress.total,
|
||||
downloaded: progress.downloaded,
|
||||
progress: progress.progress,
|
||||
speed: this.statsEstimate.bytes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.status = STATUS.running
|
||||
await this.__init()
|
||||
this.__httpFetch(this.downloadUrl, this.requestOptions)
|
||||
this.emit('start')
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (this.status === STATUS.stopped) return
|
||||
this.status = STATUS.stopped
|
||||
await this.__handleStop()
|
||||
this.emit('stop')
|
||||
}
|
||||
|
||||
refreshUrl(url) {
|
||||
this.downloadUrl = url
|
||||
}
|
||||
|
||||
updateSaveInfo(filePath, fileName) {
|
||||
this.chunkInfo.path = path.join(filePath, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
export default Task
|
||||
@@ -1,83 +0,0 @@
|
||||
import Downloader from './Downloader'
|
||||
// import { pauseResumeTimer } from './util'
|
||||
import { sizeFormate, getProxyInfo } from '../index'
|
||||
import { debugDownload } from '../env'
|
||||
|
||||
// these are the default options
|
||||
// const options = {
|
||||
// method: 'GET', // Request Method Verb
|
||||
// // Custom HTTP Header ex: Authorization, User-Agent
|
||||
// headers: {},
|
||||
// fileName: '', // Custom filename when saved
|
||||
// override: false, // if true it will override the file, otherwise will append '(number)' to the end of file
|
||||
// forceResume: false, // If the server does not return the "accept-ranges" header, can be force if it does support it
|
||||
// // httpRequestOptions: {}, // Override the http request options
|
||||
// // httpsRequestOptions: {}, // Override the https request options, ex: to add SSL Certs
|
||||
// }
|
||||
|
||||
export default ({
|
||||
url,
|
||||
path,
|
||||
fileName,
|
||||
method = 'get',
|
||||
headers,
|
||||
forceResume,
|
||||
// resumeTime = 5000,
|
||||
onCompleted = () => {},
|
||||
onError = () => {},
|
||||
onFail = () => {},
|
||||
onStart = () => {},
|
||||
onStop = () => {},
|
||||
onProgress = () => {},
|
||||
} = {}) => {
|
||||
const dl = new Downloader(url, path, fileName, {
|
||||
requestOptions: {
|
||||
method,
|
||||
headers,
|
||||
proxy: getProxyInfo(),
|
||||
},
|
||||
|
||||
forceResume,
|
||||
})
|
||||
|
||||
dl.on('completed', () => {
|
||||
onCompleted()
|
||||
debugDownload && console.log('Download Completed')
|
||||
}).on('error', err => {
|
||||
if (err.message === 'socket hang up') return
|
||||
onError(err)
|
||||
debugDownload && console.error('Something happend', err)
|
||||
}).on('start', () => {
|
||||
onStart()
|
||||
// pauseResumeTimer(dl, resumeTime)
|
||||
}).on('progress', stats => {
|
||||
const progress = stats.progress.toFixed(2)
|
||||
const speed = sizeFormate(stats.speed)
|
||||
onProgress({
|
||||
progress,
|
||||
speed,
|
||||
downloaded: stats.downloaded,
|
||||
total: stats.total,
|
||||
})
|
||||
if (debugDownload) {
|
||||
const downloaded = sizeFormate(stats.downloaded)
|
||||
const total = sizeFormate(stats.total)
|
||||
console.log(`${speed}/s - ${progress}% [${downloaded}/${total}]`)
|
||||
}
|
||||
}).on('stop', () => {
|
||||
onStop()
|
||||
debugDownload && console.log('paused')
|
||||
}).on('fail', resp => {
|
||||
onFail(resp)
|
||||
debugDownload && console.log('fail')
|
||||
})
|
||||
|
||||
debugDownload && console.log('Downloading: ', url)
|
||||
|
||||
dl.start().catch(err => {
|
||||
onError(err)
|
||||
})
|
||||
|
||||
return dl
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
exports.STATUS = {
|
||||
idle: 'IDLE',
|
||||
init: 'INIT',
|
||||
running: 'RUNNING',
|
||||
paused: 'PAUSED',
|
||||
stopped: 'STOPPED',
|
||||
completed: 'COMPLETED',
|
||||
error: 'ERROR',
|
||||
failed: 'FAILED',
|
||||
}
|
||||
@@ -1,613 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { shell, clipboard } from 'electron'
|
||||
import { httpOverHttp, httpsOverHttp } from 'tunnel'
|
||||
import crypto from 'crypto'
|
||||
import { rendererSend, rendererInvoke, NAMES } from '@common/ipc'
|
||||
import { log } from '@common/utils'
|
||||
import iconv from 'iconv-lite'
|
||||
import { gzip, gunzip } from 'zlib'
|
||||
import { proxy, qualityList } from '@renderer/core/share'
|
||||
|
||||
|
||||
/**
|
||||
* 获取两个数之间的随机整数,大于等于min,小于max
|
||||
* @param {*} min
|
||||
* @param {*} max
|
||||
*/
|
||||
export const getRandom = (min, max) => Math.floor(Math.random() * (max - min)) + min
|
||||
|
||||
|
||||
export const sizeFormate = size => {
|
||||
// https://gist.github.com/thomseddon/3511330
|
||||
if (!size) return '0 B'
|
||||
let units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let number = Math.floor(Math.log(size) / Math.log(1024))
|
||||
return `${(size / Math.pow(1024, Math.floor(number))).toFixed(2)} ${units[number]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期格式化
|
||||
* @param {*} date 时间
|
||||
* @param {String} format 时间格式,默认YYYY-MM-DD hh:mm:ss
|
||||
*/
|
||||
export const dateFormat = (date = new Date(), format = 'YYYY-MM-DD hh:mm:ss') => {
|
||||
if (typeof date != 'object') date = new Date(date)
|
||||
const munFix = (n) => n < 10 ? ('0' + n) : n
|
||||
return format
|
||||
.replace('YYYY', date.getFullYear())
|
||||
.replace('MM', munFix(date.getMonth() + 1))
|
||||
.replace('DD', munFix(date.getDate()))
|
||||
.replace('hh', munFix(date.getHours()))
|
||||
.replace('mm', munFix(date.getMinutes()))
|
||||
.replace('ss', munFix(date.getSeconds()))
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间格式化
|
||||
*/
|
||||
export const dateFormat2 = time => {
|
||||
let differ = parseInt((Date.now() - time) / 1000)
|
||||
if (differ < 60) {
|
||||
return window.i18n.t('date_format_second', { num: differ })
|
||||
} else if (differ < 3600) {
|
||||
return window.i18n.t('date_format_minute', { num: parseInt(differ / 60) })
|
||||
} else if (differ < 86400) {
|
||||
return window.i18n.t('date_format_hour', { num: parseInt(differ / 3600) })
|
||||
} else {
|
||||
return dateFormat(time)
|
||||
}
|
||||
}
|
||||
|
||||
export const formatPlayTime = time => {
|
||||
let m = parseInt(time / 60)
|
||||
let s = parseInt(time % 60)
|
||||
return m === 0 && s === 0 ? '--/--' : (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s)
|
||||
}
|
||||
|
||||
export const formatPlayTime2 = time => {
|
||||
let m = parseInt(time / 60)
|
||||
let s = parseInt(time % 60)
|
||||
return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s)
|
||||
}
|
||||
|
||||
export const b64DecodeUnicode = str => {
|
||||
// Going backwards: from bytestream, to percent-encoding, to original string.
|
||||
return decodeURIComponent(window.atob(str).split('').map(function(c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
|
||||
}).join(''))
|
||||
}
|
||||
|
||||
const encodeNames = {
|
||||
' ': ' ',
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
''': "'",
|
||||
''': "'",
|
||||
}
|
||||
export const decodeName = (str = '') => str?.replace(/(?:&|<|>|"|'|'| )/gm, s => encodeNames[s]) || ''
|
||||
|
||||
const easeInOutQuad = (t, b, c, d) => {
|
||||
t /= d / 2
|
||||
if (t < 1) return (c / 2) * t * t + b
|
||||
t--
|
||||
return (-c / 2) * (t * (t - 2) - 1) + b
|
||||
}
|
||||
const handleScrollY = (element, to, duration = 300, fn = () => {}) => {
|
||||
if (!element) return fn()
|
||||
const start = element.scrollTop || element.scrollY || 0
|
||||
let cancel = false
|
||||
if (to > start) {
|
||||
let maxScrollTop = element.scrollHeight - element.clientHeight
|
||||
if (to > maxScrollTop) to = maxScrollTop
|
||||
} else if (to < start) {
|
||||
if (to < 0) to = 0
|
||||
} else return fn()
|
||||
const change = to - start
|
||||
const increment = 10
|
||||
if (!change) return fn()
|
||||
|
||||
let currentTime = 0
|
||||
let val
|
||||
|
||||
const animateScroll = () => {
|
||||
currentTime += increment
|
||||
val = parseInt(easeInOutQuad(currentTime, start, change, duration))
|
||||
if (element.scrollTo) {
|
||||
element.scrollTo(0, val)
|
||||
} else {
|
||||
element.scrollTop = val
|
||||
}
|
||||
if (currentTime < duration) {
|
||||
if (cancel) return fn()
|
||||
setTimeout(animateScroll, increment)
|
||||
} else {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
animateScroll()
|
||||
return () => {
|
||||
cancel = true
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 设置滚动条位置
|
||||
* @param {*} element 要设置滚动的容器 dom
|
||||
* @param {*} to 滚动的目标位置
|
||||
* @param {*} duration 滚动完成时间 ms
|
||||
* @param {*} fn 滚动完成后的回调
|
||||
* @param {*} delay 延迟执行时间
|
||||
*/
|
||||
export const scrollTo = (element, to, duration = 300, fn = () => {}, delay = 0) => {
|
||||
let cancelFn
|
||||
let timeout
|
||||
if (delay) {
|
||||
let scrollCancelFn
|
||||
cancelFn = () => {
|
||||
timeout == null ? scrollCancelFn && scrollCancelFn() : clearTimeout(timeout)
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null
|
||||
scrollCancelFn = handleScrollY(element, to, duration, fn, delay)
|
||||
}, delay)
|
||||
} else {
|
||||
cancelFn = handleScrollY(element, to, duration, fn, delay)
|
||||
}
|
||||
return cancelFn
|
||||
}
|
||||
const handleScrollX = (element, to, duration = 300, fn = () => {}) => {
|
||||
if (!element) return fn()
|
||||
const start = element.scrollLeft || element.scrollX || 0
|
||||
let cancel = false
|
||||
if (to > start) {
|
||||
let maxScrollLeft = element.scrollWidth - element.clientWidth
|
||||
if (to > maxScrollLeft) to = maxScrollLeft
|
||||
} else if (to < start) {
|
||||
if (to < 0) to = 0
|
||||
} else return fn()
|
||||
const change = to - start
|
||||
const increment = 10
|
||||
if (!change) return fn()
|
||||
|
||||
let currentTime = 0
|
||||
let val
|
||||
|
||||
const animateScroll = () => {
|
||||
currentTime += increment
|
||||
val = parseInt(easeInOutQuad(currentTime, start, change, duration))
|
||||
if (element.scrollTo) {
|
||||
element.scrollTo(val, 0)
|
||||
} else {
|
||||
element.scrollLeft = val
|
||||
}
|
||||
if (currentTime < duration) {
|
||||
if (cancel) return fn()
|
||||
setTimeout(animateScroll, increment)
|
||||
} else {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
animateScroll()
|
||||
return () => {
|
||||
cancel = true
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 设置滚动条位置
|
||||
* @param {*} element 要设置滚动的容器 dom
|
||||
* @param {*} to 滚动的目标位置
|
||||
* @param {*} duration 滚动完成时间 ms
|
||||
* @param {*} fn 滚动完成后的回调
|
||||
* @param {*} delay 延迟执行时间
|
||||
*/
|
||||
export const scrollXTo = (element, to, duration = 300, fn = () => {}, delay = 0) => {
|
||||
let cancelFn
|
||||
let timeout
|
||||
if (delay) {
|
||||
let scrollCancelFn
|
||||
cancelFn = () => {
|
||||
timeout == null ? scrollCancelFn && scrollCancelFn() : clearTimeout(timeout)
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null
|
||||
scrollCancelFn = handleScrollX(element, to, duration, fn, delay)
|
||||
}, delay)
|
||||
} else {
|
||||
cancelFn = handleScrollX(element, to, duration, fn, delay)
|
||||
}
|
||||
return cancelFn
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路径是否存在
|
||||
* @param {*} path 路径
|
||||
*/
|
||||
export const checkPath = (path) => new Promise(resolve => {
|
||||
fs.access(path, fs.constants.F_OK, err => {
|
||||
if (err) return resolve(false)
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 选择路径
|
||||
* @param {*} 选项
|
||||
*/
|
||||
export const selectDir = options => rendererInvoke(NAMES.mainWindow.select_dir, options)
|
||||
|
||||
/**
|
||||
* 打开保存对话框
|
||||
* @param {*} 选项
|
||||
*/
|
||||
export const openSaveDir = options => rendererInvoke(NAMES.mainWindow.show_save_dialog, options)
|
||||
|
||||
/**
|
||||
* 在资源管理器中打开目录
|
||||
* @param {*} dir
|
||||
*/
|
||||
export const openDirInExplorer = dir => {
|
||||
shell.showItemInFolder(dir)
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/53387532
|
||||
export const compareVer = (currentVer, targetVer) => {
|
||||
// treat non-numerical characters as lower version
|
||||
// replacing them with a negative number based on charcode of each character
|
||||
const fix = s => `.${s.toLowerCase().charCodeAt(0) - 2147483647}.`
|
||||
|
||||
currentVer = ('' + currentVer).replace(/[^0-9.]/g, fix).split('.')
|
||||
targetVer = ('' + targetVer).replace(/[^0-9.]/g, fix).split('.')
|
||||
let c = Math.max(currentVer.length, targetVer.length)
|
||||
for (let i = 0; i < c; i++) {
|
||||
// convert to integer the most efficient way
|
||||
currentVer[i] = ~~currentVer[i]
|
||||
targetVer[i] = ~~targetVer[i]
|
||||
if (currentVer[i] > targetVer[i]) return 1
|
||||
else if (currentVer[i] < targetVer[i]) return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
export const isObject = item => item && typeof item === 'object' && !Array.isArray(item)
|
||||
|
||||
/**
|
||||
* 对象深度合并
|
||||
* @param {} target 要合并源对象
|
||||
* @param {} source 要合并目标对象
|
||||
*/
|
||||
export const objectDeepMerge = (target, source, mergedObj) => {
|
||||
if (!mergedObj) {
|
||||
mergedObj = new Set()
|
||||
mergedObj.add(target)
|
||||
}
|
||||
let base = {}
|
||||
Object.keys(source).forEach(item => {
|
||||
if (isObject(source[item])) {
|
||||
if (mergedObj.has(source[item])) return
|
||||
if (!isObject(target[item])) target[item] = {}
|
||||
mergedObj.add(source[item])
|
||||
objectDeepMerge(target[item], source[item], mergedObj)
|
||||
return
|
||||
}
|
||||
base[item] = source[item]
|
||||
})
|
||||
Object.assign(target, base)
|
||||
}
|
||||
|
||||
/**
|
||||
* 在浏览器打开URL
|
||||
* @param {*} url
|
||||
*/
|
||||
export const openUrl = url => {
|
||||
if (!/^https?:\/\//.test(url)) return
|
||||
shell.openExternal(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置标题
|
||||
*/
|
||||
let dom_title = document.getElementsByTagName('title')[0]
|
||||
export const setTitle = (title = '洛雪音乐助手') => {
|
||||
dom_title.innerText = title
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建 MD5 hash
|
||||
* @param {*} str
|
||||
*/
|
||||
export const toMD5 = str => crypto.createHash('md5').update(str).digest('hex')
|
||||
|
||||
/**
|
||||
* 复制文本到剪贴板
|
||||
* @param {*} str
|
||||
*/
|
||||
export const clipboardWriteText = str => clipboard.writeText(str)
|
||||
|
||||
/**
|
||||
* 从剪贴板读取文本
|
||||
* @param {*} str
|
||||
*/
|
||||
export const clipboardReadText = str => clipboard.readText()
|
||||
|
||||
/**
|
||||
* 设置音频 meta 信息
|
||||
* @param {*} filePath
|
||||
* @param {*} meta
|
||||
*/
|
||||
export const setMeta = (filePath, meta) => {
|
||||
rendererSend(NAMES.mainWindow.set_music_meta, { filePath, meta })
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存歌词文件
|
||||
* @param {*} filePath
|
||||
* @param {*} lrc
|
||||
* @param {*} format
|
||||
*/
|
||||
export const saveLrc = (filePath, lrc, format) => {
|
||||
switch (format) {
|
||||
case 'gbk':
|
||||
fs.writeFile(filePath, iconv.encode(lrc, 'gbk', { addBOM: true }), err => {
|
||||
if (err) console.log(err)
|
||||
})
|
||||
break
|
||||
case 'utf8':
|
||||
default:
|
||||
fs.writeFile(filePath, lrc, 'utf8', err => {
|
||||
if (err) console.log(err)
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成节流函数
|
||||
* @param {*} fn
|
||||
* @param {*} delay
|
||||
*/
|
||||
export const throttle = (fn, delay = 100) => {
|
||||
let timer = null
|
||||
let _args = null
|
||||
return function(...args) {
|
||||
_args = args
|
||||
if (timer) return
|
||||
timer = setTimeout(() => {
|
||||
timer = null
|
||||
fn.apply(this, _args)
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成防抖函数
|
||||
* @param {*} fn
|
||||
* @param {*} delay
|
||||
*/
|
||||
export const debounce = (fn, delay = 100) => {
|
||||
let timer = null
|
||||
let _args = null
|
||||
return function(...args) {
|
||||
_args = args
|
||||
if (timer) clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
timer = null
|
||||
fn.apply(this, _args)
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
|
||||
const async_removeItem = (arr, num, callback) => window.requestAnimationFrame(() => {
|
||||
let len = arr.length
|
||||
if (len > num) {
|
||||
arr.splice(0, num)
|
||||
return async_removeItem(arr, num, callback)
|
||||
} else {
|
||||
arr.splice(0, len)
|
||||
return callback()
|
||||
}
|
||||
})
|
||||
const async_addItem = (arr, newArr, num, callback) => window.requestAnimationFrame(() => {
|
||||
let len = newArr.length
|
||||
if (len > num) {
|
||||
arr.push(...newArr.splice(0, num))
|
||||
return async_addItem(arr, newArr, num, callback)
|
||||
} else {
|
||||
arr.push(...newArr.splice(0, len))
|
||||
return callback()
|
||||
}
|
||||
})
|
||||
/**
|
||||
* 异步设置数组
|
||||
* @param {*} from 原数组
|
||||
* @param {*} to 设置后的数组内容
|
||||
* @param {*} num 每次设置的个数
|
||||
*/
|
||||
export const asyncSetArray = (from, to, num = 100) => new Promise(resolve => {
|
||||
async_removeItem(from, num, () => {
|
||||
async_addItem(from, Array.from(to), num, () => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取缓存大小
|
||||
*/
|
||||
export const getCacheSize = () => rendererInvoke(NAMES.mainWindow.get_cache_size)
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
export const clearCache = () => rendererInvoke(NAMES.mainWindow.clear_cache)
|
||||
|
||||
/**
|
||||
* 设置窗口大小
|
||||
* @param {*} width
|
||||
* @param {*} height
|
||||
*/
|
||||
export const setWindowSize = (width, height) => rendererSend(NAMES.mainWindow.set_window_size, { width, height })
|
||||
|
||||
|
||||
export const getProxyInfo = () => {
|
||||
return proxy.enable && proxy.host
|
||||
? `http://${proxy.username}:${proxy.password}@${proxy.host}:${proxy.port}`
|
||||
: proxy.envProxy
|
||||
? `http://${proxy.envProxy.host}:${proxy.envProxy.port}`
|
||||
: undefined
|
||||
}
|
||||
|
||||
const httpsRxp = /^https:/
|
||||
export const getRequestAgent = url => {
|
||||
let options
|
||||
if (proxy.enable && proxy.host) {
|
||||
options = {
|
||||
proxy: {
|
||||
host: proxy.host,
|
||||
port: proxy.port,
|
||||
},
|
||||
}
|
||||
} else if (proxy.envProxy) {
|
||||
options = {
|
||||
proxy: {
|
||||
host: proxy.envProxy.host,
|
||||
port: proxy.envProxy.port,
|
||||
},
|
||||
}
|
||||
}
|
||||
return options ? (httpsRxp.test(url) ? httpsOverHttp : httpOverHttp)(options) : undefined
|
||||
}
|
||||
|
||||
|
||||
export const assertApiSupport = source => qualityList.value[source] != undefined
|
||||
|
||||
export const getSetting = () => rendererInvoke(NAMES.mainWindow.get_setting)
|
||||
export const saveSetting = setting => rendererInvoke(NAMES.mainWindow.set_app_setting, setting)
|
||||
|
||||
export const getPlayList = () => rendererInvoke(NAMES.mainWindow.get_playlist).catch(error => {
|
||||
rendererInvoke(NAMES.mainWindow.get_data_path).then(dataPath => {
|
||||
let filePath = path.join(dataPath, 'playList.json.bak')
|
||||
rendererInvoke(NAMES.mainWindow.show_dialog, {
|
||||
type: 'error',
|
||||
message: window.i18n.t('load_list_file_error_title'),
|
||||
detail: window.i18n.t('load_list_file_error_detail', {
|
||||
path: filePath,
|
||||
detail: error.message,
|
||||
}),
|
||||
}).then(() => openDirInExplorer(filePath))
|
||||
})
|
||||
return rendererInvoke(NAMES.mainWindow.get_playlist, true)
|
||||
})
|
||||
|
||||
// 解析URL参数为对象
|
||||
export const parseUrlParams = str => {
|
||||
const params = {}
|
||||
if (typeof str !== 'string') return params
|
||||
const paramsArr = str.split('&')
|
||||
for (const param of paramsArr) {
|
||||
let [key, value] = param.split('=')
|
||||
params[key] = value
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
export const getLyric = musicInfo => rendererInvoke(NAMES.mainWindow.get_lyric, `${musicInfo.source}_${musicInfo.songmid}`)
|
||||
export const getLyricRaw = musicInfo => rendererInvoke(NAMES.mainWindow.get_lyric_raw, `${musicInfo.source}_${musicInfo.songmid}`)
|
||||
export const setLyric = (musicInfo, { lyric, tlyric, rlyric, lxlyric }) => rendererSend(NAMES.mainWindow.save_lyric_raw, {
|
||||
id: `${musicInfo.source}_${musicInfo.songmid}`,
|
||||
lyrics: { lyric, tlyric, rlyric, lxlyric },
|
||||
})
|
||||
export const setLyricEdited = (musicInfo, { lyric, tlyric, rlyric, lxlyric }) => rendererSend(NAMES.mainWindow.save_lyric_edited, {
|
||||
id: `${musicInfo.source}_${musicInfo.songmid}`,
|
||||
lyrics: { lyric, tlyric, rlyric, lxlyric },
|
||||
})
|
||||
export const removeLyricEdited = musicInfo => rendererSend(NAMES.mainWindow.remove_lyric_edited, `${musicInfo.source}_${musicInfo.songmid}`)
|
||||
|
||||
export const clearLyric = () => rendererSend(NAMES.mainWindow.clear_lyric_raw)
|
||||
|
||||
export const getMusicUrl = (musicInfo, type) => rendererInvoke(NAMES.mainWindow.get_music_url, `${musicInfo.source}_${musicInfo.songmid}_${type}`)
|
||||
export const setMusicUrl = (musicInfo, type, url) => rendererSend(NAMES.mainWindow.save_music_url, {
|
||||
id: `${musicInfo.source}_${musicInfo.songmid}_${type}`,
|
||||
url,
|
||||
})
|
||||
export const clearMusicUrl = () => rendererSend(NAMES.mainWindow.clear_music_url)
|
||||
|
||||
export const gzipData = str => {
|
||||
return new Promise((resolve, reject) => {
|
||||
gzip(str, (err, result) => {
|
||||
if (err) return reject(err)
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
export const gunzipData = buf => {
|
||||
return new Promise((resolve, reject) => {
|
||||
gunzip(buf, (err, result) => {
|
||||
if (err) return reject(err)
|
||||
resolve(result.toString())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const saveLxConfigFile = async(path, data) => {
|
||||
if (!path.endsWith('.lxmc')) path += '.lxmc'
|
||||
fs.writeFile(path, await gzipData(JSON.stringify(data)), 'binary', err => {
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
export const readLxConfigFile = async path => {
|
||||
let isJSON = path.endsWith('.json')
|
||||
let data = await fs.promises.readFile(path, isJSON ? 'utf8' : 'binary')
|
||||
if (!data || isJSON) return data
|
||||
data = await gunzipData(Buffer.from(data, 'binary'))
|
||||
data = JSON.parse(data.toString('utf8'))
|
||||
|
||||
// 修复v1.14.0出现的导出数据被序列化两次的问题
|
||||
if (typeof data != 'object') {
|
||||
try {
|
||||
data = JSON.parse(data)
|
||||
} catch (err) {
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export const saveStrToFile = (path, str) => new Promise((resolve, reject) => {
|
||||
fs.writeFile(path, str, err => {
|
||||
if (err) {
|
||||
log.error(err)
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
const fileNameRxp = /[\\/:*?#"<>|]/g
|
||||
export const filterFileName = name => name.replace(fileNameRxp, '')
|
||||
|
||||
|
||||
export const getFontSizeWithScreen = (screenWidth = window.innerWidth) => {
|
||||
return screenWidth <= 1440
|
||||
? 16
|
||||
: screenWidth <= 1920
|
||||
? 18
|
||||
: screenWidth <= 2560
|
||||
? 20
|
||||
: screenWidth <= 2560 ? 20 : 22
|
||||
}
|
||||
|
||||
|
||||
export const deduplicationList = list => {
|
||||
const ids = new Set()
|
||||
return list.filter(s => {
|
||||
if (ids.has(s.songmid)) return false
|
||||
ids.add(s.songmid)
|
||||
return true
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { dateFormat } from '@common/utils/common'
|
||||
|
||||
export * from '@common/utils/renderer'
|
||||
export * from '@common/utils/nodejs'
|
||||
export * from '@common/utils/common'
|
||||
export * from '@common/utils/tools'
|
||||
|
||||
/**
|
||||
* 时间格式化
|
||||
*/
|
||||
export const dateFormat2 = (time: number): string => {
|
||||
let differ = Math.trunc((Date.now() - time) / 1000)
|
||||
if (differ < 60) {
|
||||
return window.i18n.t('date_format_second', { num: differ })
|
||||
} else if (differ < 3600) {
|
||||
return window.i18n.t('date_format_minute', { num: Math.trunc(differ / 60) })
|
||||
} else if (differ < 86400) {
|
||||
return window.i18n.t('date_format_hour', { num: Math.trunc(differ / 3600) })
|
||||
} else {
|
||||
return dateFormat(time)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置标题
|
||||
*/
|
||||
let dom_title = document.getElementsByTagName('title')[0]
|
||||
export const setTitle = (title: string | null) => {
|
||||
if (!title) title = '洛雪音乐助手'
|
||||
dom_title.innerText = title
|
||||
}
|
||||
|
||||
|
||||
// export const getProxyInfo = () => {
|
||||
// return proxy.enable && proxy.host
|
||||
// ? `http://${proxy.username}:${proxy.password}@${proxy.host}:${proxy.port}`
|
||||
// : proxy.envProxy
|
||||
// ? `http://${proxy.envProxy.host}:${proxy.envProxy.port}`
|
||||
// : undefined
|
||||
// }
|
||||
|
||||
|
||||
export const getFontSizeWithScreen = (screenWidth: number = window.innerWidth): number => {
|
||||
return screenWidth <= 1440
|
||||
? 16
|
||||
: screenWidth <= 1920
|
||||
? 18
|
||||
: screenWidth <= 2560
|
||||
? 20
|
||||
: screenWidth <= 2560 ? 20 : 22
|
||||
}
|
||||
|
||||
|
||||
export const deduplicationList = <T extends LX.Music.MusicInfo>(list: T[]): T[] => {
|
||||
const ids: Set<string | number> = new Set()
|
||||
return list.filter(s => {
|
||||
if (ids.has(s.id)) return false
|
||||
ids.add(s.id)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export const langS2T = async(str: string) => {
|
||||
return await window.lx.worker.main.langS2t(Buffer.from(str).toString('base64')).then(b64 => Buffer.from(b64, 'base64').toString())
|
||||
}
|
||||
@@ -0,0 +1,734 @@
|
||||
import { rendererSend, rendererInvoke, rendererOn, rendererOff } from '@common/rendererIpc'
|
||||
import { HOTKEY_RENDERER_EVENT_NAME, WIN_MAIN_RENDERER_EVENT_NAME, CMMON_EVENT_NAME } from '@common/ipcNames'
|
||||
import { ProgressInfo, UpdateDownloadedEvent, UpdateInfo } from 'electron-updater'
|
||||
import { markRaw } from '@common/utils/vueTools'
|
||||
import * as hotKeys from '@common/hotKey'
|
||||
import { APP_EVENT_NAMES, DATA_KEYS, DEFAULT_SETTING } from '@common/constants'
|
||||
|
||||
type RemoveListener = () => void
|
||||
|
||||
export const getSetting = async() => {
|
||||
return rendererInvoke<LX.AppSetting>(CMMON_EVENT_NAME.get_app_setting)
|
||||
}
|
||||
export const updateSetting = async(setting: Partial<LX.AppSetting>) => {
|
||||
await rendererInvoke(CMMON_EVENT_NAME.set_app_setting, setting)
|
||||
}
|
||||
export const onSettingChanged = (listener: LX.IpcRendererEventListenerParams<Partial<LX.AppSetting>>): RemoveListener => {
|
||||
rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.on_config_change, listener)
|
||||
return () => {
|
||||
rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.on_config_change, listener)
|
||||
}
|
||||
}
|
||||
|
||||
export const sendInited = () => {
|
||||
return rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.inited)
|
||||
}
|
||||
|
||||
export const getOtherSource = async(id: string): Promise<LX.Music.MusicInfoOnline[]> => {
|
||||
return rendererInvoke<string, LX.Music.MusicInfoOnline[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_other_source, id)
|
||||
}
|
||||
export const saveOtherSource = async(id: string, sourceInfo: LX.Music.MusicInfoOnline[]) => {
|
||||
await rendererInvoke<LX.Music.MusicInfoOtherSourceSave>(WIN_MAIN_RENDERER_EVENT_NAME.save_other_source, {
|
||||
id,
|
||||
list: sourceInfo,
|
||||
})
|
||||
}
|
||||
export const clearOtherSource = async() => {
|
||||
await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_other_source)
|
||||
}
|
||||
export const getOtherSourceCount = async() => {
|
||||
return await rendererInvoke<number>(WIN_MAIN_RENDERER_EVENT_NAME.get_other_source_count)
|
||||
}
|
||||
|
||||
|
||||
export const getHotKeyConfig = async() => {
|
||||
return rendererInvoke<LX.HotKeyConfigAll>(WIN_MAIN_RENDERER_EVENT_NAME.get_hot_key)
|
||||
}
|
||||
|
||||
export const setIgnoreMouseEvents = (ignore: boolean) => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.set_ignore_mouse_events, ignore)
|
||||
}
|
||||
|
||||
export const getEnvParams = async() => {
|
||||
return rendererInvoke<LX.EnvParams>(CMMON_EVENT_NAME.get_env_params)
|
||||
}
|
||||
|
||||
export const clearEnvParamsDeeplink = () => {
|
||||
rendererSend(CMMON_EVENT_NAME.clear_env_params_deeplink)
|
||||
}
|
||||
|
||||
export const onDeeplink = (listener: LX.IpcRendererEventListenerParams<string>): RemoveListener => {
|
||||
rendererOn(CMMON_EVENT_NAME.deeplink, listener)
|
||||
return () => {
|
||||
rendererOff(CMMON_EVENT_NAME.deeplink, listener)
|
||||
}
|
||||
}
|
||||
|
||||
export const checkUpdate = () => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.update_check)
|
||||
}
|
||||
|
||||
export const quitUpdate = () => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.quit_update)
|
||||
}
|
||||
|
||||
export const onUpdateAvailable = (listener: LX.IpcRendererEventListenerParams<UpdateInfo>): RemoveListener => {
|
||||
rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.update_available, listener)
|
||||
return () => {
|
||||
rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.update_available, listener)
|
||||
}
|
||||
}
|
||||
|
||||
export const onUpdateError = (listener: LX.IpcRendererEventListenerParams<string>): RemoveListener => {
|
||||
rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.update_error, listener)
|
||||
return () => {
|
||||
rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.update_error, listener)
|
||||
}
|
||||
}
|
||||
|
||||
export const onUpdateProgress = (listener: LX.IpcRendererEventListenerParams<ProgressInfo>): RemoveListener => {
|
||||
rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.update_progress, listener)
|
||||
return () => {
|
||||
rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.update_progress, listener)
|
||||
}
|
||||
}
|
||||
|
||||
export const onUpdateDownloaded = (listener: LX.IpcRendererEventListenerParams<UpdateDownloadedEvent>): RemoveListener => {
|
||||
rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.update_downloaded, listener)
|
||||
return () => {
|
||||
rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.update_downloaded, listener)
|
||||
}
|
||||
}
|
||||
|
||||
export const onUpdateNotAvailable = (listener: LX.IpcRendererEventListenerParams<UpdateInfo>): RemoveListener => {
|
||||
rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.update_not_available, listener)
|
||||
return () => {
|
||||
rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.update_not_available, listener)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const importUserApi = async(fileText: string) => {
|
||||
return rendererInvoke<string, LX.UserApi.ImportUserApi>(WIN_MAIN_RENDERER_EVENT_NAME.import_user_api, fileText)
|
||||
}
|
||||
export const setUserApi = async(source: LX.UserApi.UserApiSetApiParams): Promise<void> => {
|
||||
return rendererInvoke<LX.UserApi.UserApiSetApiParams>(WIN_MAIN_RENDERER_EVENT_NAME.set_user_api, source)
|
||||
}
|
||||
export const removeUserApi = async(ids: string[]) => {
|
||||
return rendererInvoke<string[], LX.UserApi.UserApiInfo[]>(WIN_MAIN_RENDERER_EVENT_NAME.remove_user_api, ids)
|
||||
}
|
||||
export const onShowUserApiUpdateAlert = (listener: LX.IpcRendererEventListenerParams<LX.UserApi.UserApiUpdateInfo>): RemoveListener => {
|
||||
rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.user_api_show_update_alert, listener)
|
||||
return () => {
|
||||
rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.user_api_show_update_alert, listener)
|
||||
}
|
||||
}
|
||||
export const setAllowShowUserApiUpdateAlert = async(id: string, enable: boolean): Promise<void> => {
|
||||
return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.user_api_set_allow_update_alert, { id, enable })
|
||||
}
|
||||
export const onUserApiStatus = (listener: LX.IpcRendererEventListenerParams<LX.UserApi.UserApiStatus>): RemoveListener => {
|
||||
rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.user_api_status, listener)
|
||||
return () => {
|
||||
rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.user_api_status, listener)
|
||||
}
|
||||
}
|
||||
export const getUserApiList = async() => {
|
||||
return rendererInvoke<LX.UserApi.UserApiInfo[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_user_api_list)
|
||||
}
|
||||
export const sendUserApiRequest = async({ requestKey, data }: LX.UserApi.UserApiRequestParams): Promise<any> => {
|
||||
return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.request_user_api, {
|
||||
requestKey,
|
||||
data,
|
||||
})
|
||||
}
|
||||
export const userApiRequestCancel = (requestKey: LX.UserApi.UserApiRequestCancelParams) => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.request_user_api_cancel, requestKey)
|
||||
}
|
||||
|
||||
// export const setDesktopLyricInfo = (type, data, info) => {
|
||||
// rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.set_lyric_info, {
|
||||
// type,
|
||||
// data,
|
||||
// info,
|
||||
// })
|
||||
// }
|
||||
// export const onGetDesktopLyricInfo = callback => {
|
||||
// rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_info, callback)
|
||||
// return () => {
|
||||
// rendererOff(callback)
|
||||
// }
|
||||
// }
|
||||
|
||||
export const setTaskBarProgress = (progress: number, mode?: Electron.ProgressBarOptions['mode']) => {
|
||||
rendererSend<LX.Player.ProgressBarOptions>(WIN_MAIN_RENDERER_EVENT_NAME.progress, {
|
||||
progress: progress < 0 ? progress : Math.max(0.01, progress),
|
||||
mode: mode ?? 'normal',
|
||||
})
|
||||
}
|
||||
|
||||
export const savePlayInfo = (playInfo: LX.Player.SavedPlayInfo) => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {
|
||||
path: DATA_KEYS.playInfo,
|
||||
data: playInfo,
|
||||
})
|
||||
}
|
||||
// 获取上次关闭时的当前歌曲播放信息
|
||||
export const getPlayInfo = async() => {
|
||||
return rendererInvoke<string, LX.Player.SavedPlayInfo | null>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.playInfo)
|
||||
}
|
||||
|
||||
export const saveSearchHistoryList = (list: LX.List.SearchHistoryList) => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {
|
||||
path: DATA_KEYS.searchHistoryList,
|
||||
data: list,
|
||||
})
|
||||
}
|
||||
// 获取搜索历史列表
|
||||
export const getSearchHistoryList = async() => {
|
||||
return rendererInvoke<string, string[] | null>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.searchHistoryList)
|
||||
}
|
||||
|
||||
export const saveListPositionInfo = (listPosition: LX.List.ListPositionInfo) => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {
|
||||
path: DATA_KEYS.listScrollPosition,
|
||||
data: listPosition,
|
||||
})
|
||||
}
|
||||
// 获取搜索历史列表
|
||||
export const getListPositionInfo = async() => {
|
||||
return rendererInvoke<string, LX.List.ListPositionInfo | null>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.listScrollPosition)
|
||||
}
|
||||
|
||||
export const saveListPrevSelectId = (listPosition: string | null) => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {
|
||||
path: DATA_KEYS.listPrevSelectId,
|
||||
data: listPosition,
|
||||
})
|
||||
}
|
||||
// 获取上一次选中的列表id
|
||||
export const getListPrevSelectId = async() => {
|
||||
return rendererInvoke<string, string | null>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.listPrevSelectId)
|
||||
}
|
||||
|
||||
export const saveListUpdateInfo = (listPosition: LX.List.ListUpdateInfo) => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {
|
||||
path: DATA_KEYS.listUpdateInfo,
|
||||
data: listPosition,
|
||||
})
|
||||
}
|
||||
// 获取列表更新记录
|
||||
export const getListUpdateInfo = async() => {
|
||||
return rendererInvoke<string, LX.List.ListUpdateInfo | null>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.listUpdateInfo)
|
||||
}
|
||||
|
||||
export const saveIgnoreVersion = (version: string) => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {
|
||||
path: DATA_KEYS.ignoreVersion,
|
||||
data: version,
|
||||
})
|
||||
}
|
||||
// 获取忽略更新的版本号
|
||||
export const getIgnoreVersion = async() => {
|
||||
return rendererInvoke<string, string | null>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.ignoreVersion)
|
||||
}
|
||||
|
||||
export const saveLeaderboardSetting = (source: typeof DEFAULT_SETTING['leaderboard']) => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {
|
||||
path: DATA_KEYS.leaderboardSetting,
|
||||
data: source,
|
||||
})
|
||||
}
|
||||
export const getLeaderboardSetting = async() => {
|
||||
return await rendererInvoke<string, typeof DEFAULT_SETTING['leaderboard']>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.leaderboardSetting) ?? { ...DEFAULT_SETTING.leaderboard }
|
||||
}
|
||||
export const saveSongListSetting = (setting: typeof DEFAULT_SETTING['songList']) => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {
|
||||
path: DATA_KEYS.songListSetting,
|
||||
data: setting,
|
||||
})
|
||||
}
|
||||
export const getSongListSetting = async() => {
|
||||
return await rendererInvoke<string, typeof DEFAULT_SETTING['songList']>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.songListSetting) ?? { ...DEFAULT_SETTING.songList }
|
||||
}
|
||||
export const saveSearchSetting = (setting: typeof DEFAULT_SETTING['search']) => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {
|
||||
path: DATA_KEYS.searchSetting,
|
||||
data: setting,
|
||||
})
|
||||
}
|
||||
export const getSearchSetting = async() => {
|
||||
return await rendererInvoke<string, typeof DEFAULT_SETTING['search']>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.searchSetting) ?? { ...DEFAULT_SETTING.search }
|
||||
}
|
||||
export const saveViewPrevState = (state: typeof DEFAULT_SETTING['viewPrevState']) => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {
|
||||
path: DATA_KEYS.viewPrevState,
|
||||
data: state,
|
||||
})
|
||||
}
|
||||
export const getViewPrevState = async() => {
|
||||
return await rendererInvoke<string, typeof DEFAULT_SETTING['viewPrevState']>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.viewPrevState) ?? { ...DEFAULT_SETTING.viewPrevState }
|
||||
}
|
||||
|
||||
|
||||
export const getSystemFonts = async() => {
|
||||
return rendererInvoke<string[]>(CMMON_EVENT_NAME.get_system_fonts).catch(() => {
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export const allHotKeys = markRaw({
|
||||
local: [
|
||||
{
|
||||
name: hotKeys.HOTKEY_PLAYER.toggle_play.name,
|
||||
action: hotKeys.HOTKEY_PLAYER.toggle_play.action,
|
||||
type: APP_EVENT_NAMES.winMainName,
|
||||
},
|
||||
{
|
||||
name: hotKeys.HOTKEY_PLAYER.prev.name,
|
||||
action: hotKeys.HOTKEY_PLAYER.prev.action,
|
||||
type: APP_EVENT_NAMES.winMainName,
|
||||
},
|
||||
{
|
||||
name: hotKeys.HOTKEY_PLAYER.next.name,
|
||||
action: hotKeys.HOTKEY_PLAYER.next.action,
|
||||
type: APP_EVENT_NAMES.winMainName,
|
||||
},
|
||||
{
|
||||
name: hotKeys.HOTKEY_COMMON.focusSearchInput.name,
|
||||
action: hotKeys.HOTKEY_COMMON.focusSearchInput.action,
|
||||
type: APP_EVENT_NAMES.winMainName,
|
||||
},
|
||||
{
|
||||
name: hotKeys.HOTKEY_COMMON.min.name,
|
||||
action: hotKeys.HOTKEY_COMMON.min.action,
|
||||
type: APP_EVENT_NAMES.winMainName,
|
||||
},
|
||||
{
|
||||
name: hotKeys.HOTKEY_COMMON.close.name,
|
||||
action: hotKeys.HOTKEY_COMMON.close.action,
|
||||
type: APP_EVENT_NAMES.winMainName,
|
||||
},
|
||||
],
|
||||
global: [
|
||||
{
|
||||
name: hotKeys.HOTKEY_COMMON.min_toggle.name,
|
||||
action: hotKeys.HOTKEY_COMMON.min_toggle.action,
|
||||
type: APP_EVENT_NAMES.winMainName,
|
||||
},
|
||||
{
|
||||
name: hotKeys.HOTKEY_COMMON.hide_toggle.name,
|
||||
action: hotKeys.HOTKEY_COMMON.hide_toggle.action,
|
||||
type: APP_EVENT_NAMES.winMainName,
|
||||
},
|
||||
{
|
||||
name: hotKeys.HOTKEY_COMMON.close.name,
|
||||
action: hotKeys.HOTKEY_COMMON.close.action,
|
||||
type: APP_EVENT_NAMES.winMainName,
|
||||
},
|
||||
{
|
||||
name: hotKeys.HOTKEY_PLAYER.toggle_play.name,
|
||||
action: hotKeys.HOTKEY_PLAYER.toggle_play.action,
|
||||
type: APP_EVENT_NAMES.winMainName,
|
||||
},
|
||||
{
|
||||
name: hotKeys.HOTKEY_PLAYER.prev.name,
|
||||
action: hotKeys.HOTKEY_PLAYER.prev.action,
|
||||
type: APP_EVENT_NAMES.winMainName,
|
||||
},
|
||||
{
|
||||
name: hotKeys.HOTKEY_PLAYER.next.name,
|
||||
action: hotKeys.HOTKEY_PLAYER.next.action,
|
||||
type: APP_EVENT_NAMES.winMainName,
|
||||
},
|
||||
{
|
||||
name: hotKeys.HOTKEY_PLAYER.volume_up.name,
|
||||
action: hotKeys.HOTKEY_PLAYER.volume_up.action,
|
||||
type: APP_EVENT_NAMES.winMainName,
|
||||
},
|
||||
{
|
||||
name: hotKeys.HOTKEY_PLAYER.volume_down.name,
|
||||
action: hotKeys.HOTKEY_PLAYER.volume_down.action,
|
||||
type: APP_EVENT_NAMES.winMainName,
|
||||
},
|
||||
{
|
||||
name: hotKeys.HOTKEY_PLAYER.volume_mute.name,
|
||||
action: hotKeys.HOTKEY_PLAYER.volume_mute.action,
|
||||
type: APP_EVENT_NAMES.winMainName,
|
||||
},
|
||||
{
|
||||
name: hotKeys.HOTKEY_DESKTOP_LYRIC.toggle_visible.name,
|
||||
action: hotKeys.HOTKEY_DESKTOP_LYRIC.toggle_visible.action,
|
||||
type: APP_EVENT_NAMES.winLyricName,
|
||||
},
|
||||
{
|
||||
name: hotKeys.HOTKEY_DESKTOP_LYRIC.toggle_lock.name,
|
||||
action: hotKeys.HOTKEY_DESKTOP_LYRIC.toggle_lock.action,
|
||||
type: APP_EVENT_NAMES.winLyricName,
|
||||
},
|
||||
{
|
||||
name: hotKeys.HOTKEY_DESKTOP_LYRIC.toggle_always_top.name,
|
||||
action: hotKeys.HOTKEY_DESKTOP_LYRIC.toggle_always_top.action,
|
||||
type: APP_EVENT_NAMES.winLyricName,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export const hotKeySetEnable = async(enable: boolean) => {
|
||||
return rendererInvoke(HOTKEY_RENDERER_EVENT_NAME.enable, enable)
|
||||
}
|
||||
|
||||
export const hotKeySetConfig = async(config: LX.HotKeyActions) => {
|
||||
return rendererInvoke(HOTKEY_RENDERER_EVENT_NAME.set_config, config)
|
||||
}
|
||||
|
||||
export const hotKeyGetStatus = async() => {
|
||||
return rendererInvoke<LX.HotKeyState>(HOTKEY_RENDERER_EVENT_NAME.status)
|
||||
}
|
||||
|
||||
// 主进程操作播放器状态
|
||||
export const onPlayerAction = (listener: LX.IpcRendererEventListenerParams<LX.Player.StatusButtonActions>): RemoveListener => {
|
||||
rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.player_action_on_button_click, listener)
|
||||
return () => {
|
||||
rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.player_action_on_button_click, listener)
|
||||
}
|
||||
}
|
||||
// export const setTaskbarThumbnailClip = async(clip: Electron.Rectangle) => {
|
||||
// await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.taskbar_set_thumbnail_clip, clip)
|
||||
// }
|
||||
// 播放器状态更新 通知主进程
|
||||
export const setPlayerAction = (buttons: LX.TaskBarButtonFlags) => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.player_action_set_buttons, buttons)
|
||||
}
|
||||
|
||||
/**
|
||||
* On Theme Change
|
||||
* @param listener LX.IpcRendererEventListenerParams<shouldUseDarkColors: boolean>
|
||||
* @returns RemoveListener Fn
|
||||
*/
|
||||
export const onThemeChange = (listener: LX.IpcRendererEventListenerParams<LX.ThemeSetting>): RemoveListener => {
|
||||
rendererOn(CMMON_EVENT_NAME.theme_change, listener)
|
||||
return () => {
|
||||
rendererOff(CMMON_EVENT_NAME.theme_change, listener)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择路径
|
||||
*/
|
||||
export const showSelectDialog = async(options: Electron.OpenDialogOptions) => {
|
||||
return rendererInvoke<Electron.OpenDialogOptions, Electron.OpenDialogReturnValue>(WIN_MAIN_RENDERER_EVENT_NAME.show_select_dialog, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开保存对话框
|
||||
*/
|
||||
export const openSaveDir = async(options: Electron.SaveDialogOptions) => {
|
||||
return rendererInvoke<Electron.SaveDialogOptions, Electron.SaveDialogReturnValue>(WIN_MAIN_RENDERER_EVENT_NAME.show_save_dialog, options)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取缓存大小
|
||||
*/
|
||||
export const getCacheSize = async() => {
|
||||
return rendererInvoke<number>(WIN_MAIN_RENDERER_EVENT_NAME.get_cache_size)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
export const clearCache = async() => {
|
||||
await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_cache)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置窗口大小
|
||||
* @param {*} width
|
||||
* @param {*} height
|
||||
*/
|
||||
export const setWindowSize = (width: number, height: number) => {
|
||||
const params: Partial<Electron.Rectangle> = {
|
||||
width,
|
||||
height,
|
||||
}
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.set_window_size, params)
|
||||
}
|
||||
|
||||
|
||||
export const getPlayerLyric = async(musicInfo: LX.Music.MusicInfo) => {
|
||||
return rendererInvoke<string, LX.Player.LyricInfo>(WIN_MAIN_RENDERER_EVENT_NAME.get_palyer_lyric, musicInfo.id)
|
||||
}
|
||||
|
||||
export const getLyricRaw = async(musicInfo: LX.Music.MusicInfo): Promise<LX.Music.LyricInfo> => {
|
||||
return rendererInvoke<string, LX.Music.LyricInfo>(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_raw, musicInfo.id)
|
||||
}
|
||||
|
||||
export const clearLyricRaw = async() => {
|
||||
await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_lyric_raw)
|
||||
}
|
||||
|
||||
export const getLyricRawCount = async() => {
|
||||
return await rendererInvoke<number>(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_raw_count)
|
||||
}
|
||||
|
||||
|
||||
export const getLyricEdited = async(musicInfo: LX.Music.MusicInfo): Promise<LX.Music.LyricInfo> => {
|
||||
return rendererInvoke<string, LX.Music.LyricInfo>(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_edited, musicInfo.id)
|
||||
}
|
||||
|
||||
export const saveLyric = async(musicInfo: LX.Music.MusicInfo, lyricInfo: LX.Music.LyricInfo | LX.Player.LyricInfo) => {
|
||||
// console.log(musicInfo)
|
||||
if ('rawlrcInfo' in lyricInfo) {
|
||||
const { rawlrcInfo, ...info } = lyricInfo
|
||||
const tasks = [
|
||||
rendererInvoke<LX.Music.LyricInfoSave>(WIN_MAIN_RENDERER_EVENT_NAME.save_lyric_raw, {
|
||||
id: musicInfo.id,
|
||||
lyrics: rawlrcInfo,
|
||||
}),
|
||||
]
|
||||
if (info.lyric != rawlrcInfo.lyric) {
|
||||
tasks.push(rendererInvoke<LX.Music.LyricInfoSave>(WIN_MAIN_RENDERER_EVENT_NAME.save_lyric_edited, {
|
||||
id: musicInfo.id,
|
||||
lyrics: info,
|
||||
}))
|
||||
}
|
||||
console.log(tasks)
|
||||
await Promise.all(tasks)
|
||||
} else {
|
||||
await rendererInvoke<LX.Music.LyricInfoSave>(WIN_MAIN_RENDERER_EVENT_NAME.save_lyric_raw, {
|
||||
id: musicInfo.id,
|
||||
lyrics: lyricInfo,
|
||||
})
|
||||
}
|
||||
}
|
||||
export const saveLyricEdited = async(musicInfo: LX.Music.MusicInfo, lyricInfo: LX.Music.LyricInfo) => {
|
||||
await rendererInvoke<LX.Music.LyricInfoSave>(WIN_MAIN_RENDERER_EVENT_NAME.save_lyric_edited, {
|
||||
id: musicInfo.id,
|
||||
lyrics: lyricInfo,
|
||||
})
|
||||
}
|
||||
export const removeLyricEdited = async(musicInfo: LX.Music.MusicInfo) => {
|
||||
await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.remove_lyric_edited, musicInfo.id)
|
||||
}
|
||||
|
||||
export const clearLyric = async() => {
|
||||
await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_lyric_raw)
|
||||
}
|
||||
|
||||
export const clearLyricEdited = async() => {
|
||||
await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_lyric_edited)
|
||||
}
|
||||
|
||||
export const getLyricEditedCount = async() => {
|
||||
return await rendererInvoke<number>(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_edited_count)
|
||||
}
|
||||
|
||||
|
||||
export const saveTheme = async(theme: LX.Theme) => {
|
||||
return rendererInvoke<LX.Theme>(WIN_MAIN_RENDERER_EVENT_NAME.save_theme, theme)
|
||||
}
|
||||
export const removeTheme = async(id: string) => {
|
||||
return rendererInvoke<string>(WIN_MAIN_RENDERER_EVENT_NAME.remove_theme, id)
|
||||
}
|
||||
export const getThemes = async() => {
|
||||
return rendererInvoke<{ themes: LX.Theme[], userThemes: LX.Theme[], dataPath: string }>(WIN_MAIN_RENDERER_EVENT_NAME.get_themes)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存获取歌曲URL
|
||||
* @param musicInfo 歌曲信息
|
||||
* @param type URL音质
|
||||
* @returns
|
||||
*/
|
||||
export const getMusicUrl = async(musicInfo: LX.Music.MusicInfo, type: LX.Quality): Promise<string> => {
|
||||
return rendererInvoke<string, string>(WIN_MAIN_RENDERER_EVENT_NAME.get_music_url, `${musicInfo.id}_${type}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存歌曲URL
|
||||
* @param musicInfo 歌曲信息
|
||||
* @param type URL音质
|
||||
* @param url 歌曲URL
|
||||
*/
|
||||
export const saveMusicUrl = async(musicInfo: LX.Music.MusicInfo, type: LX.Quality, url: string) => {
|
||||
await rendererInvoke<LX.Music.MusicUrlInfo>(WIN_MAIN_RENDERER_EVENT_NAME.save_music_url, {
|
||||
id: `${musicInfo.id}_${type}`,
|
||||
url,
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 清理所有缓存的歌曲URL
|
||||
*/
|
||||
export const clearMusicUrl = async() => {
|
||||
await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_music_url)
|
||||
}
|
||||
|
||||
export const getMusicUrlCount = async() => {
|
||||
return await rendererInvoke<number>(WIN_MAIN_RENDERER_EVENT_NAME.get_music_url_count)
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出应用
|
||||
*/
|
||||
export const quitApp = () => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.quit)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭窗口
|
||||
*/
|
||||
export const closeWindow = () => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.close)
|
||||
}
|
||||
|
||||
/**
|
||||
* 最小化窗口
|
||||
*/
|
||||
export const minWindow = () => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.min)
|
||||
}
|
||||
|
||||
/**
|
||||
* 最大化窗口
|
||||
*/
|
||||
export const maxWindow = () => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.max)
|
||||
}
|
||||
|
||||
/**
|
||||
* 最小化、最大化窗口切换
|
||||
*/
|
||||
export const minMaxWindowToggle = () => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.min_toggle)
|
||||
}
|
||||
/**
|
||||
* 显示、隐藏窗口切换
|
||||
*/
|
||||
export const showHideWindowToggle = () => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.hide_toggle)
|
||||
}
|
||||
/**
|
||||
* 聚焦窗口
|
||||
*/
|
||||
export const focusWindow = () => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.focus)
|
||||
}
|
||||
|
||||
/**
|
||||
* 窗口获取焦点事件
|
||||
* @param listener
|
||||
* @returns
|
||||
*/
|
||||
export const onFocus = (listener: LX.IpcRendererEventListener): RemoveListener => {
|
||||
rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.focus, listener)
|
||||
return () => {
|
||||
rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.focus, listener)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷键触发事件
|
||||
* @param listener
|
||||
* @returns
|
||||
*/
|
||||
export const onKeyDown = (listener: LX.IpcRendererEventListenerParams<LX.HotKeyEvent>): RemoveListener => {
|
||||
rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.key_down, listener)
|
||||
return () => {
|
||||
rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.key_down, listener)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷键设置更新事件
|
||||
* @param listener
|
||||
* @returns
|
||||
*/
|
||||
export const onUpdateHotkey = (listener: LX.IpcRendererEventListenerParams<LX.HotKeyConfigAll>): RemoveListener => {
|
||||
rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.set_hot_key_config, listener)
|
||||
return () => {
|
||||
rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.set_hot_key_config, listener)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置全屏
|
||||
* @param isFullscreen 是否全屏
|
||||
* @returns
|
||||
*/
|
||||
export const setFullScreen = async(isFullscreen: boolean): Promise<boolean> => {
|
||||
return rendererInvoke<boolean, boolean>(WIN_MAIN_RENDERER_EVENT_NAME.fullscreen, isFullscreen)
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开开发者工具
|
||||
* @returns
|
||||
*/
|
||||
export const openDevTools = () => {
|
||||
rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.open_dev_tools)
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收同步事件
|
||||
* @param listener
|
||||
* @returns
|
||||
*/
|
||||
export const onSyncAction = (listener: LX.IpcRendererEventListenerParams<LX.Sync.SyncMainWindowActions>): RemoveListener => {
|
||||
rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, listener)
|
||||
return () => {
|
||||
rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, listener)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送同步事件
|
||||
* @param action
|
||||
* @returns
|
||||
*/
|
||||
export const sendSyncAction = async(action: LX.Sync.SyncServiceActions) => {
|
||||
return rendererInvoke<LX.Sync.SyncServiceActions>(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, action)
|
||||
}
|
||||
|
||||
// export const refreshSyncCode = async(): Promise<string> => {
|
||||
// return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.sync_generate_code)
|
||||
// }
|
||||
|
||||
// export const onSyncStatus = (listener: LX.IpcRendererEventListenerParams<LX.Sync.Status>): RemoveListener => {
|
||||
// rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.sync_status, listener)
|
||||
|
||||
// return () => {
|
||||
// rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.sync_status, listener)
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* 桌面歌词进程创建事件
|
||||
* @param listener
|
||||
* @returns
|
||||
*/
|
||||
export const onNewDesktopLyricProcess = (listener: LX.IpcRendererEventListener): RemoveListener => {
|
||||
rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.process_new_desktop_lyric_client, listener)
|
||||
return () => {
|
||||
rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.process_new_desktop_lyric_client, listener)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const downloadTasksGet = async() => {
|
||||
return rendererInvoke<LX.Download.ListItem[]>(WIN_MAIN_RENDERER_EVENT_NAME.download_list_get)
|
||||
}
|
||||
export const downloadTasksCreate = async(list: LX.Download.ListItem[], addMusicLocationType: LX.AddMusicLocationType) => {
|
||||
return rendererInvoke<LX.Download.saveDownloadMusicInfo>(WIN_MAIN_RENDERER_EVENT_NAME.download_list_add, {
|
||||
list,
|
||||
addMusicLocationType,
|
||||
})
|
||||
}
|
||||
export const downloadTasksUpdate = async(list: LX.Download.ListItem[]) => {
|
||||
return rendererInvoke<LX.Download.ListItem[]>(WIN_MAIN_RENDERER_EVENT_NAME.download_list_update, list)
|
||||
}
|
||||
export const downloadTasksRemove = async(ids: string[]) => {
|
||||
return rendererInvoke<string[]>(WIN_MAIN_RENDERER_EVENT_NAME.download_list_remove, ids)
|
||||
}
|
||||
export const downloadListClear = async() => {
|
||||
return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.download_list_clear)
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
import { isMac } from '../../common/utils'
|
||||
import { isMac } from '@common/utils'
|
||||
|
||||
const downKeys = new Set()
|
||||
const downKeys: Set<string> = new Set()
|
||||
|
||||
const handleEvent = (type, event, keys, isEditing) => {
|
||||
export declare type KeyActionType = LX.KeyDownEevent['type']
|
||||
export declare type Keys = LX.KeyDownEevent['keys']
|
||||
export declare type Key = LX.KeyDownEevent['key']
|
||||
export declare type EventKey = LX.KeyDownEevent['eventKey']
|
||||
export declare type Event = LX.KeyDownEevent['event']
|
||||
|
||||
const handleEvent = (type: KeyActionType, event: LX.KeyEvent, keys: Keys, isEditing: boolean) => {
|
||||
let eventKey = event.key
|
||||
if (isMac) {
|
||||
let index = keys.indexOf('meta')
|
||||
@@ -27,8 +33,8 @@ const handleEvent = (type, event, keys, isEditing) => {
|
||||
}
|
||||
|
||||
// 修饰键处理
|
||||
const eventModifiers = event => {
|
||||
let modifiers = []
|
||||
const eventModifiers = (event: LX.KeyEvent): string[] => {
|
||||
let modifiers: string[] = []
|
||||
if (event.ctrlKey) modifiers.push('ctrl')
|
||||
if (event.shiftKey) modifiers.push('shift')
|
||||
if (event.altKey) modifiers.push('alt')
|
||||
@@ -38,7 +44,7 @@ const eventModifiers = event => {
|
||||
}
|
||||
|
||||
// 是否忽略事件(表单元素等默认忽略)
|
||||
const assertStopCallback = element => {
|
||||
const assertStopCallback = (element: HTMLElement) => {
|
||||
// if the element has the class "keybind" then no need to stop
|
||||
if (element.classList.contains('key-bind')) return false
|
||||
|
||||
@@ -49,11 +55,11 @@ const assertStopCallback = element => {
|
||||
case 'TEXTAREA':
|
||||
return true
|
||||
default:
|
||||
return element.isContentEditable
|
||||
return !!element.isContentEditable
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = event => {
|
||||
const handleKeyDown = (event: LX.KeyEvent) => {
|
||||
// if (assertStopCallback(event.target)) return
|
||||
// event.preventDefault()
|
||||
let keys = eventModifiers(event)
|
||||
@@ -70,10 +76,10 @@ const handleKeyDown = event => {
|
||||
keys.push((event.code.includes('Numpad') ? event.code.replace(/^Numpad(\w{1,3})\w*$/i, 'num$1') : event.key).toLowerCase())
|
||||
break
|
||||
}
|
||||
handleEvent('down', event, keys, assertStopCallback(event.target))
|
||||
handleEvent('down', event, keys, event.target ? assertStopCallback(event.target as HTMLElement) : false)
|
||||
}
|
||||
|
||||
const handleKeyUp = event => {
|
||||
const handleKeyUp = (event: LX.KeyEvent) => {
|
||||
// if (assertStopCallback(event.target)) return
|
||||
event.preventDefault()
|
||||
let keys = eventModifiers(event)
|
||||
@@ -88,12 +94,13 @@ const handleKeyUp = event => {
|
||||
keys.push((event.code.includes('Numpad') ? event.code.replace(/^Numpad(\w{1,3})\w*$/i, 'num$1') : event.key).toLowerCase())
|
||||
break
|
||||
}
|
||||
handleEvent('up', event, keys, assertStopCallback(event.target))
|
||||
handleEvent('up', event, keys, event.target ? assertStopCallback(event.target as HTMLElement) : false)
|
||||
}
|
||||
|
||||
let handleSendEvent
|
||||
type HandleSendEvent = (key: Key, eventKey: EventKey, type: KeyActionType, event: Event, keys: Keys, isEditing: boolean) => void
|
||||
let handleSendEvent: HandleSendEvent
|
||||
|
||||
const bindKey = (handle = () => {}) => {
|
||||
const bindKey = (handle: HandleSendEvent = () => {}) => {
|
||||
handleSendEvent = handle
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
document.addEventListener('keyup', handleKeyUp)
|
||||
@@ -107,7 +114,7 @@ const unbindKey = () => {
|
||||
const clearDownKeys = () => {
|
||||
let keys = Array.from(downKeys)
|
||||
for (let i = keys.length - 1; i > -1; i--) {
|
||||
handleSendEvent(keys[i], keys[i], 'up', null, [keys[i]])
|
||||
handleSendEvent(keys[i], keys[i], 'up', null, [keys[i]], false)
|
||||
}
|
||||
downKeys.clear()
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
const { getNow, TimeoutTools } = require('./utils')
|
||||
|
||||
// const fontFormateRxp = /(?=<\d+,\d+>).*?/g
|
||||
const fontSplitRxp = /(?=<\d+,\d+>).*?/g
|
||||
const timeRxp = /<(\d+),(\d+)>/
|
||||
|
||||
|
||||
// Create animation
|
||||
const createAnimation = (dom, duration) => new window.Animation(new window.KeyframeEffect(dom, [
|
||||
{ backgroundSize: '0 100%' },
|
||||
{ backgroundSize: '100% 100%' },
|
||||
], {
|
||||
duration,
|
||||
easing: 'linear',
|
||||
},
|
||||
), document.timeline)
|
||||
|
||||
|
||||
// https://jsfiddle.net/ceqpnbky/
|
||||
// https://jsfiddle.net/ceqpnbky/1/
|
||||
|
||||
module.exports = class FontPlayer {
|
||||
constructor({ time = 0, lyric = '', extendedLyrics = '', lineClassName = '', fontClassName = '', extendedLrcClassName = '', lineModeClassName = '', shadowContent = false, shadowClassName = '' }) {
|
||||
this.time = time
|
||||
this.lyric = lyric
|
||||
this.extendedLyrics = extendedLyrics
|
||||
|
||||
this.lineClassName = lineClassName
|
||||
this.fontClassName = fontClassName
|
||||
this.extendedLrcClassName = extendedLrcClassName
|
||||
this.lineModeClassName = lineModeClassName
|
||||
this.shadowContent = shadowContent
|
||||
this.shadowClassName = shadowClassName
|
||||
|
||||
this.isPlay = false
|
||||
this.curFontNum = 0
|
||||
this.maxFontNum = 0
|
||||
this._performanceTime = 0
|
||||
this._startTime = 0
|
||||
|
||||
this.fontContent = null
|
||||
|
||||
this.timeoutTools = new TimeoutTools(80)
|
||||
this.waitPlayTimeout = new TimeoutTools(80)
|
||||
|
||||
this._init()
|
||||
}
|
||||
|
||||
_init() {
|
||||
if (this.lyric == null) this.lyric = ''
|
||||
|
||||
this.isLineMode = false
|
||||
|
||||
this.lineContent = document.createElement('div')
|
||||
this.lineContent.time = this.time
|
||||
if (this.lineClassName) this.lineContent.classList.add(this.lineClassName)
|
||||
this.fontContent = document.createElement('div')
|
||||
this.fontContent.style = 'position:relative;display:inline-block;'
|
||||
if (this.fontClassName) this.fontContent.classList.add(this.fontClassName)
|
||||
if (this.shadowContent) {
|
||||
this.fontShadowContent = document.createElement('div')
|
||||
this.fontShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
|
||||
this.fontShadowContent.className = this.shadowClassName
|
||||
this.fontContent.appendChild(this.fontShadowContent)
|
||||
}
|
||||
this.lineContent.appendChild(this.fontContent)
|
||||
for (const lrc of this.extendedLyrics) {
|
||||
const extendedLrcContent = document.createElement('div')
|
||||
extendedLrcContent.style = 'position:relative;display:inline-block;'
|
||||
extendedLrcContent.className = this.extendedLrcClassName
|
||||
extendedLrcContent.textContent = lrc
|
||||
this.lineContent.appendChild(document.createElement('br'))
|
||||
this.lineContent.appendChild(extendedLrcContent)
|
||||
|
||||
if (this.shadowContent) {
|
||||
const extendedLrcShadowContent = document.createElement('div')
|
||||
extendedLrcShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
|
||||
extendedLrcShadowContent.className = this.shadowClassName
|
||||
extendedLrcShadowContent.textContent = lrc
|
||||
extendedLrcContent.appendChild(extendedLrcShadowContent)
|
||||
}
|
||||
}
|
||||
this._parseLyric()
|
||||
}
|
||||
|
||||
_parseLyric() {
|
||||
const fonts = this.lyric.split(fontSplitRxp)
|
||||
// console.log(fonts)
|
||||
|
||||
this.maxFontNum = fonts.length - 1
|
||||
this.fonts = []
|
||||
let text
|
||||
for (const font of fonts) {
|
||||
text = font.replace(timeRxp, '')
|
||||
if (RegExp.$2 == '') return this._handleLineParse()
|
||||
const time = parseInt(RegExp.$2)
|
||||
|
||||
const dom = document.createElement('span')
|
||||
let shadowDom
|
||||
dom.textContent = text
|
||||
const animation = createAnimation(dom, time)
|
||||
this.fontContent.appendChild(dom)
|
||||
|
||||
if (this.shadowContent) {
|
||||
shadowDom = document.createElement('span')
|
||||
shadowDom.textContent = text
|
||||
this.fontShadowContent.appendChild(shadowDom)
|
||||
}
|
||||
// dom.style = shadowDom.style = this.fontStyle
|
||||
// dom.className = shadowDom.className = this.fontClassName
|
||||
|
||||
this.fonts.push({
|
||||
text,
|
||||
startTime: parseInt(RegExp.$1),
|
||||
time,
|
||||
dom,
|
||||
shadowDom,
|
||||
animation,
|
||||
})
|
||||
}
|
||||
// console.log(this.fonts)
|
||||
}
|
||||
|
||||
_handleLineParse() {
|
||||
this.isLineMode = true
|
||||
const dom = document.createElement('span')
|
||||
let shadowDom
|
||||
dom.classList.add(this.lineModeClassName)
|
||||
dom.textContent = this.lyric
|
||||
if (this.shadowContent) {
|
||||
shadowDom = document.createElement('span')
|
||||
shadowDom.textContent = this.lyric
|
||||
this.fontShadowContent.appendChild(shadowDom)
|
||||
}
|
||||
this.fontContent.appendChild(dom)
|
||||
this.fonts.push({
|
||||
text: this.lyric,
|
||||
dom,
|
||||
shadowDom,
|
||||
})
|
||||
}
|
||||
|
||||
_currentTime() {
|
||||
return getNow() - this._performanceTime + this._startTime
|
||||
}
|
||||
|
||||
_findcurFontNum(curTime, startIndex = 0) {
|
||||
const length = this.fonts.length
|
||||
for (let index = startIndex; index < length; index++) if (curTime < this.fonts[index].startTime) return index == 0 ? 0 : index - 1
|
||||
return length - 1
|
||||
}
|
||||
|
||||
_handlePlayMaxFontNum() {
|
||||
let curFont = this.fonts[this.curFontNum]
|
||||
// console.log(curFont.text)
|
||||
const currentTime = this._currentTime()
|
||||
const driftTime = currentTime - curFont.startTime
|
||||
if (currentTime > curFont.startTime + curFont.time) {
|
||||
this._handlePlayFont(curFont, driftTime, true)
|
||||
this.pause()
|
||||
} else {
|
||||
this._handlePlayFont(curFont, driftTime)
|
||||
this.isPlay = false
|
||||
}
|
||||
}
|
||||
|
||||
_handlePlayFont(font, currentTime, toFinishe) {
|
||||
switch (font.animation.playState) {
|
||||
case 'finished':
|
||||
break
|
||||
case 'idle':
|
||||
font.dom.style.backgroundSize = '100% 100%'
|
||||
if (!toFinishe) font.animation.play()
|
||||
break
|
||||
default:
|
||||
if (toFinishe) {
|
||||
font.animation.cancel()
|
||||
} else {
|
||||
font.animation.currentTime = currentTime
|
||||
font.animation.play()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
_handlePlayLine(isPlayed) {
|
||||
this.isPlay = false
|
||||
this.fonts[0].dom.style.backgroundSize = isPlayed ? '100% 100%' : '100% 0'
|
||||
}
|
||||
|
||||
_handlePauseFont(font) {
|
||||
if (font.animation.playState == 'running') font.animation.pause()
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
this.curFontNum++
|
||||
// console.log('curFontNum time', this.fonts[this.curFontNum].time)
|
||||
if (this.curFontNum >= this.maxFontNum) return this._handlePlayMaxFontNum()
|
||||
let curFont = this.fonts[this.curFontNum]
|
||||
// console.log(curFont, nextFont, this.curFontNum, this.maxFontNum)
|
||||
const currentTime = this._currentTime()
|
||||
// console.log(curFont.text)
|
||||
const driftTime = currentTime - curFont.startTime
|
||||
|
||||
// console.log(currentTime, driftTime)
|
||||
|
||||
if (driftTime >= 0 || this.curFontNum == 0) {
|
||||
let nextFont = this.fonts[this.curFontNum + 1]
|
||||
this.delay = nextFont.startTime - curFont.startTime - driftTime
|
||||
if (this.delay > 0) {
|
||||
if (this.isPlay) {
|
||||
this.timeoutTools.start(() => {
|
||||
if (!this.isPlay) return
|
||||
this._refresh()
|
||||
}, this.delay)
|
||||
}
|
||||
this._handlePlayFont(curFont, driftTime)
|
||||
return
|
||||
} else {
|
||||
let newCurLineNum = this._findcurFontNum(currentTime, this.curFontNum + 1)
|
||||
if (newCurLineNum > this.curFontNum) this.curFontNum = newCurLineNum - 1
|
||||
for (let i = 0; i <= this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true)
|
||||
this._refresh()
|
||||
return
|
||||
}
|
||||
} else if (this.curFontNum == 0) {
|
||||
this.curFontNum--
|
||||
if (this.isPlay) {
|
||||
this.waitPlayTimeout.start(() => {
|
||||
if (!this.isPlay) return
|
||||
this._refresh()
|
||||
}, -driftTime)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.curFontNum = this._findcurFontNum(currentTime, this.curFontNum) - 1
|
||||
for (let i = 0; i <= this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true)
|
||||
// this.curFontNum--
|
||||
this._refresh()
|
||||
}
|
||||
|
||||
play(curTime = 0) {
|
||||
// console.log('play', curTime)
|
||||
if (!this.fonts.length) return
|
||||
this.pause()
|
||||
|
||||
if (this.isLineMode) return this._handlePlayLine(true)
|
||||
this.isPlay = true
|
||||
this._performanceTime = getNow()
|
||||
this._startTime = curTime
|
||||
|
||||
this.curFontNum = this._findcurFontNum(curTime)
|
||||
|
||||
for (let i = this.curFontNum; i > -1; i--) {
|
||||
this._handlePlayFont(this.fonts[i], 0, true)
|
||||
}
|
||||
for (let i = this.curFontNum, len = this.fonts.length; i < len; i++) {
|
||||
let font = this.fonts[i]
|
||||
font.animation.cancel()
|
||||
font.dom.style.backgroundSize = '0 100%'
|
||||
}
|
||||
|
||||
this.curFontNum--
|
||||
|
||||
this._refresh()
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (!this.isPlay) return
|
||||
this.isPlay = false
|
||||
this.timeoutTools.clear()
|
||||
this.waitPlayTimeout.clear()
|
||||
this._handlePauseFont(this.fonts[this.curFontNum])
|
||||
if (this.curFontNum === this.maxLine) return
|
||||
const curFontNum = this._findcurFontNum(this._currentTime())
|
||||
if (this.curFontNum === curFontNum) return
|
||||
for (let i = 0; i < this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true)
|
||||
}
|
||||
|
||||
finish() {
|
||||
this.pause()
|
||||
if (this.isLineMode) return this._handlePlayLine(true)
|
||||
|
||||
for (const font of this.fonts) {
|
||||
font.animation.cancel()
|
||||
font.dom.style.backgroundSize = '100% 100%'
|
||||
}
|
||||
this.curFontNum = this.maxFontNum
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.pause()
|
||||
if (this.isLineMode) return this._handlePlayLine(false)
|
||||
for (const font of this.fonts) {
|
||||
font.animation.cancel()
|
||||
font.dom.style.backgroundSize = '0 100%'
|
||||
}
|
||||
this.curFontNum = 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
const LinePlayer = require('./line-player')
|
||||
const FontPlayer = require('./font-player')
|
||||
|
||||
const fontTimeExp = /<(\d+),(\d+)>/g
|
||||
|
||||
module.exports = class Lyric {
|
||||
constructor({
|
||||
lyric = '',
|
||||
extendedLyrics = [],
|
||||
offset = 0,
|
||||
lineClassName = '',
|
||||
fontClassName = 'font',
|
||||
extendedLrcClassName = 'extended',
|
||||
activeLineClassName = 'active',
|
||||
lineModeClassName = 'line',
|
||||
shadowClassName = '',
|
||||
shadowContent = false,
|
||||
onPlay = function() { },
|
||||
onSetLyric = function() { },
|
||||
}) {
|
||||
this.lyric = lyric
|
||||
this.extendedLyrics = extendedLyrics
|
||||
this.offset = offset
|
||||
this.onPlay = onPlay
|
||||
this.onSetLyric = onSetLyric
|
||||
|
||||
this.lineClassName = lineClassName
|
||||
this.fontClassName = fontClassName
|
||||
this.extendedLrcClassName = extendedLrcClassName
|
||||
this.activeLineClassName = activeLineClassName
|
||||
this.lineModeClassName = lineModeClassName
|
||||
this.shadowClassName = shadowClassName
|
||||
this.shadowContent = shadowContent
|
||||
|
||||
this.playingLineNum = -1
|
||||
this.isLineMode = false
|
||||
|
||||
this.linePlayer = new LinePlayer({
|
||||
offset: this.offset,
|
||||
onPlay: this._handleLinePlayerOnPlay,
|
||||
onSetLyric: this._handleLinePlayerOnSetLyric,
|
||||
})
|
||||
}
|
||||
|
||||
_init() {
|
||||
this.playingLineNum = -1
|
||||
this.isLineMode = false
|
||||
|
||||
this.linePlayer.setLyric(this.lyric, this.extendedLyrics)
|
||||
}
|
||||
|
||||
_handleLinePlayerOnPlay = (num, text, curTime) => {
|
||||
if (this.isLineMode) {
|
||||
if (num < this.playingLineNum + 1) {
|
||||
for (let i = this.playingLineNum; i > num - 1; i--) {
|
||||
const font = this._lineFonts[i]
|
||||
font.reset()
|
||||
font.lineContent.classList.remove(this.activeLineClassName)
|
||||
}
|
||||
} else if (num > this.playingLineNum) {
|
||||
for (let i = Math.max(this.playingLineNum, 0); i < num; i++) {
|
||||
const font = this._lineFonts[i]
|
||||
font.reset()
|
||||
font.lineContent.classList.remove(this.activeLineClassName)
|
||||
}
|
||||
} else if (this.playingLineNum > -1) {
|
||||
const font = this._lineFonts[this.playingLineNum]
|
||||
font.reset()
|
||||
font.lineContent.classList.remove(this.activeLineClassName)
|
||||
}
|
||||
} else {
|
||||
if (num < this.playingLineNum + 1) {
|
||||
for (let i = this.playingLineNum; i > num - 1; i--) {
|
||||
const font = this._lineFonts[i]
|
||||
font.lineContent.classList.remove(this.activeLineClassName)
|
||||
font.reset()
|
||||
}
|
||||
} else if (num > this.playingLineNum) {
|
||||
for (let i = Math.max(this.playingLineNum, 0); i < num; i++) {
|
||||
const font = this._lineFonts[i]
|
||||
font.lineContent.classList.remove(this.activeLineClassName)
|
||||
font.finish()
|
||||
}
|
||||
} else if (this.playingLineNum > -1) {
|
||||
const font = this._lineFonts[this.playingLineNum]
|
||||
font.lineContent.classList.remove(this.activeLineClassName)
|
||||
}
|
||||
}
|
||||
this.playingLineNum = num
|
||||
const font = this._lineFonts[num]
|
||||
font.lineContent.classList.add(this.activeLineClassName)
|
||||
font.play(curTime - this._lines[num].time)
|
||||
this.onPlay(num, this._lines[num].text)
|
||||
}
|
||||
|
||||
_handleLinePlayerOnSetLyric = (lyricLines, offset) => {
|
||||
// console.log(lyricLines)
|
||||
// this._lines = lyricsLines
|
||||
this.isLineMode = lyricLines.length && !/^<\d+,\d+>/.test(lyricLines[0].text)
|
||||
|
||||
this._lineFonts = []
|
||||
if (this.isLineMode) {
|
||||
this._lines = lyricLines.map(line => {
|
||||
const fontPlayer = new FontPlayer({
|
||||
time: line.time,
|
||||
lyric: line.text,
|
||||
extendedLyrics: line.extendedLyrics,
|
||||
lineClassName: this.lineClassName,
|
||||
fontClassName: this.fontClassName,
|
||||
extendedLrcClassName: this.extendedLrcClassName,
|
||||
lineModeClassName: this.lineModeClassName,
|
||||
shadowClassName: this.shadowClassName,
|
||||
shadowContent: this.shadowContent,
|
||||
})
|
||||
|
||||
this._lineFonts.push(fontPlayer)
|
||||
return {
|
||||
text: line.text,
|
||||
time: line.time,
|
||||
extendedLyrics: line.extendedLyrics,
|
||||
dom_line: fontPlayer.lineContent,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this._lines = lyricLines.map(line => {
|
||||
const fontPlayer = new FontPlayer({
|
||||
time: line.time,
|
||||
lyric: line.text,
|
||||
extendedLyrics: line.extendedLyrics,
|
||||
lineClassName: this.lineClassName,
|
||||
fontClassName: this.fontClassName,
|
||||
extendedLrcClassName: this.extendedLrcClassName,
|
||||
shadowClassName: this.shadowClassName,
|
||||
shadowContent: this.shadowContent,
|
||||
})
|
||||
|
||||
this._lineFonts.push(fontPlayer)
|
||||
return {
|
||||
text: line.text.replace(fontTimeExp, ''),
|
||||
time: line.time,
|
||||
extendedLyrics: line.extendedLyrics,
|
||||
dom_line: fontPlayer.lineContent,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 如果是逐行歌词,则添加 60ms 的偏移
|
||||
let newOffset = this.isLineMode ? this.offset + 60 : this.offset
|
||||
offset = offset - this.linePlayer.offset + newOffset
|
||||
this.linePlayer.offset = newOffset
|
||||
this.onSetLyric(this._lines, offset)
|
||||
}
|
||||
|
||||
play(curTime) {
|
||||
if (!this.linePlayer) return
|
||||
this.linePlayer.play(curTime)
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (!this.linePlayer) return
|
||||
this.linePlayer.pause()
|
||||
if (this.playingLineNum > -1) this._lineFonts[this.playingLineNum].pause()
|
||||
}
|
||||
|
||||
setLyric(lyric, extendedLyrics) {
|
||||
this.lyric = lyric
|
||||
this.extendedLyrics = extendedLyrics
|
||||
this._init()
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
const { getNow, TimeoutTools } = require('./utils')
|
||||
|
||||
const timeExp = /^\[([\d:.]*)\]{1}/g
|
||||
const tagRegMap = {
|
||||
title: 'ti',
|
||||
artist: 'ar',
|
||||
album: 'al',
|
||||
offset: 'offset',
|
||||
by: 'by',
|
||||
}
|
||||
|
||||
const timeoutTools = new TimeoutTools()
|
||||
|
||||
const parseExtendedLyric = (lrcLinesMap, extendedLyric) => {
|
||||
const extendedLines = extendedLyric.split(/\r\n|\n|\r/)
|
||||
for (let i = 0; i < extendedLines.length; i++) {
|
||||
const line = extendedLines[i].trim()
|
||||
let result = timeExp.exec(line)
|
||||
if (result) {
|
||||
const text = line.replace(timeExp, '').trim()
|
||||
if (text) {
|
||||
const timeStr = RegExp.$1.replace(/(\.\d\d)0$/, '$1')
|
||||
const targetLine = lrcLinesMap[timeStr]
|
||||
if (targetLine) targetLine.extendedLyrics.push(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = class LinePlayer {
|
||||
constructor({ offset = 0, onPlay = function() { }, onSetLyric = function() { } } = {}) {
|
||||
this.tags = {}
|
||||
this.lines = null
|
||||
this.onPlay = onPlay
|
||||
this.onSetLyric = onSetLyric
|
||||
this.isPlay = false
|
||||
this.curLineNum = 0
|
||||
this.maxLine = 0
|
||||
this.offset = offset
|
||||
this._performanceTime = 0
|
||||
this._startTime = 0
|
||||
}
|
||||
|
||||
_init() {
|
||||
if (this.lyric == null) this.lyric = ''
|
||||
if (this.extendedLyrics == null) this.extendedLyrics = []
|
||||
this._initTag()
|
||||
this._initLines()
|
||||
this.onSetLyric(this.lines, this.tags.offset + this.offset)
|
||||
}
|
||||
|
||||
_initTag() {
|
||||
this.tags = {}
|
||||
for (let tag in tagRegMap) {
|
||||
const matches = this.lyric.match(new RegExp(`\\[${tagRegMap[tag]}:([^\\]]*)]`, 'i'))
|
||||
this.tags[tag] = (matches && matches[1]) || ''
|
||||
}
|
||||
if (this.tags.offset) {
|
||||
let offset = parseInt(this.tags.offset)
|
||||
this.tags.offset = Number.isNaN(offset) ? 0 : offset
|
||||
} else {
|
||||
this.tags.offset = 0
|
||||
}
|
||||
}
|
||||
|
||||
_initLines() {
|
||||
this.lines = []
|
||||
const lines = this.lyric.split(/\r\n|\r|\n/)
|
||||
const linesMap = {}
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim()
|
||||
let result = timeExp.exec(line)
|
||||
if (result) {
|
||||
const text = line.replace(timeExp, '').trim()
|
||||
if (text) {
|
||||
const timeStr = RegExp.$1.replace(/(\.\d\d)0$/, '$1')
|
||||
const timeArr = timeStr.split(':')
|
||||
if (timeArr.length < 3) timeArr.unshift(0)
|
||||
if (timeArr[2].indexOf('.') > -1) {
|
||||
timeArr.push(...timeArr[2].split('.'))
|
||||
timeArr.splice(2, 1)
|
||||
}
|
||||
linesMap[timeStr] = {
|
||||
time: parseInt(timeArr[0]) * 60 * 60 * 1000 + parseInt(timeArr[1]) * 60 * 1000 + parseInt(timeArr[2]) * 1000 + parseInt(timeArr[3] || 0),
|
||||
text,
|
||||
extendedLyrics: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const lrc of this.extendedLyrics) parseExtendedLyric(linesMap, lrc)
|
||||
this.lines = Object.values(linesMap)
|
||||
this.lines.sort((a, b) => {
|
||||
return a.time - b.time
|
||||
})
|
||||
this.maxLine = this.lines.length - 1
|
||||
}
|
||||
|
||||
_currentTime() {
|
||||
return getNow() - this._performanceTime + this._startTime
|
||||
}
|
||||
|
||||
_findCurLineNum(curTime, startIndex = 0) {
|
||||
if (curTime <= 0) return 0
|
||||
const length = this.lines.length
|
||||
for (let index = startIndex; index < length; index++) if (curTime <= this.lines[index].time) return index === 0 ? 0 : index - 1
|
||||
return length - 1
|
||||
}
|
||||
|
||||
_handleMaxLine() {
|
||||
this.onPlay(this.curLineNum, this.lines[this.curLineNum].text, this._currentTime())
|
||||
this.pause()
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
this.curLineNum++
|
||||
// console.log('curLineNum time', this.lines[this.curLineNum].time)
|
||||
if (this.curLineNum >= this.maxLine) return this._handleMaxLine()
|
||||
|
||||
let curLine = this.lines[this.curLineNum]
|
||||
|
||||
const currentTime = this._currentTime()
|
||||
const driftTime = currentTime - curLine.time
|
||||
|
||||
if (driftTime >= 0 || this.curLineNum === 0) {
|
||||
let nextLine = this.lines[this.curLineNum + 1]
|
||||
this.delay = nextLine.time - curLine.time - driftTime
|
||||
|
||||
if (this.delay > 0) {
|
||||
if (this.isPlay) {
|
||||
timeoutTools.start(() => {
|
||||
if (!this.isPlay) return
|
||||
this._refresh()
|
||||
}, this.delay)
|
||||
}
|
||||
this.onPlay(this.curLineNum, curLine.text, currentTime)
|
||||
return
|
||||
} else {
|
||||
let newCurLineNum = this._findCurLineNum(currentTime, this.curLineNum + 1)
|
||||
if (newCurLineNum > this.curLineNum) this.curLineNum = newCurLineNum - 1
|
||||
this._refresh()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.curLineNum = this._findCurLineNum(currentTime, this.curLineNum) - 1
|
||||
this._refresh()
|
||||
}
|
||||
|
||||
play(curTime = 0) {
|
||||
if (!this.lines.length) return
|
||||
this.pause()
|
||||
this.isPlay = true
|
||||
|
||||
this._performanceTime = getNow() - parseInt(this.tags.offset + this.offset)
|
||||
this._startTime = curTime
|
||||
|
||||
this.curLineNum = this._findCurLineNum(this._currentTime()) - 1
|
||||
|
||||
this._refresh()
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (!this.isPlay) return
|
||||
this.isPlay = false
|
||||
timeoutTools.clear()
|
||||
if (this.curLineNum === this.maxLine) return
|
||||
const currentTime = this._currentTime()
|
||||
const curLineNum = this._findCurLineNum(currentTime)
|
||||
if (this.curLineNum !== curLineNum) {
|
||||
this.curLineNum = curLineNum
|
||||
this.onPlay(curLineNum, this.lines[curLineNum].text, currentTime)
|
||||
}
|
||||
}
|
||||
|
||||
setLyric(lyric, extendedLyrics) {
|
||||
// console.log(extendedLyrics)
|
||||
if (this.isPlay) this.pause()
|
||||
this.lyric = lyric
|
||||
this.extendedLyrics = extendedLyrics
|
||||
this._init()
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
|
||||
const getNow = exports.getNow = typeof performance == 'object' && window.performance.now ? window.performance.now.bind(window.performance) : Date.now.bind(Date)
|
||||
|
||||
exports.TimeoutTools = class TimeoutTools {
|
||||
constructor(thresholdTime = 200) {
|
||||
this.invokeTime = 0
|
||||
this.animationFrameId = null
|
||||
this.timeoutId = null
|
||||
this.callback = null
|
||||
this.thresholdTime = thresholdTime
|
||||
}
|
||||
|
||||
run() {
|
||||
this.animationFrameId = window.requestAnimationFrame(() => {
|
||||
this.animationFrameId = null
|
||||
let diff = this.invokeTime - getNow()
|
||||
// console.log('diff', diff)
|
||||
if (diff > 0) {
|
||||
if (diff < this.thresholdTime) return this.run()
|
||||
return this.timeoutId = setTimeout(() => {
|
||||
this.timeoutId = null
|
||||
this.run()
|
||||
}, diff - this.thresholdTime)
|
||||
}
|
||||
|
||||
// console.log('diff', diff)
|
||||
this.callback(diff)
|
||||
})
|
||||
}
|
||||
|
||||
start(callback = () => {}, timeout = 0) {
|
||||
// console.log(timeout)
|
||||
this.callback = callback
|
||||
this.invokeTime = getNow() + timeout
|
||||
|
||||
this.run()
|
||||
}
|
||||
|
||||
clear() {
|
||||
if (this.animationFrameId) {
|
||||
window.cancelAnimationFrame(this.animationFrameId)
|
||||
this.animationFrameId = null
|
||||
}
|
||||
if (this.timeoutId) {
|
||||
window.clearTimeout(this.timeoutId)
|
||||
this.timeoutId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,4 @@ export const requestMsg = {
|
||||
// unachievable: '哦No😱...接口无法访问了!已帮你切换到临时接口,重试下看能不能播放吧~',
|
||||
notConnectNetwork: '无法连接到服务器',
|
||||
cancelRequest: '取消http请求',
|
||||
}
|
||||
} as const
|
||||
@@ -0,0 +1,191 @@
|
||||
import { checkPath, joinPath, extname, basename, readFile, getFileStats } from '@common/utils/nodejs'
|
||||
import { formatPlayTime } from '@common/utils/common'
|
||||
|
||||
export const checkDownloadFileAvailable = async(musicInfo: LX.Download.ListItem, savePath: string): Promise<boolean> => {
|
||||
return musicInfo.isComplate && !/\.ape$/.test(musicInfo.metadata.fileName) &&
|
||||
(await checkPath(musicInfo.metadata.filePath) || await checkPath(joinPath(savePath, musicInfo.metadata.fileName)))
|
||||
}
|
||||
|
||||
export const checkLocalFileAvailable = async(musicInfo: LX.Music.MusicInfoLocal): Promise<boolean> => {
|
||||
return await checkPath(musicInfo.meta.filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查音乐文件是否存在
|
||||
* @param musicInfo
|
||||
* @param savePath
|
||||
*/
|
||||
export const checkMusicFileAvailable = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, savePath: string): Promise<boolean> => {
|
||||
if ('progress' in musicInfo) {
|
||||
return await checkDownloadFileAvailable(musicInfo, savePath)
|
||||
} else if (musicInfo.source == 'local') {
|
||||
return await checkLocalFileAvailable(musicInfo)
|
||||
} else return true
|
||||
}
|
||||
|
||||
export const getDownloadFilePath = async(musicInfo: LX.Download.ListItem, savePath: string): Promise<string> => {
|
||||
if (musicInfo.isComplate && !/\.ape$/.test(musicInfo.metadata.fileName)) {
|
||||
if (await checkPath(musicInfo.metadata.filePath)) return musicInfo.metadata.filePath
|
||||
const path = joinPath(savePath, musicInfo.metadata.fileName)
|
||||
if (await checkPath(path)) return path
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export const getLocalFilePath = async(musicInfo: LX.Music.MusicInfoLocal): Promise<string> => {
|
||||
return await checkPath(musicInfo.meta.filePath) ? musicInfo.meta.filePath : ''
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取音乐文件路径
|
||||
* @param musicInfo
|
||||
* @param savePath
|
||||
* @returns
|
||||
*/
|
||||
export const getMusicFilePath = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, savePath: string): Promise<string> => {
|
||||
if ('progress' in musicInfo) {
|
||||
return await getDownloadFilePath(musicInfo, savePath)
|
||||
} else if (musicInfo.source == 'local') {
|
||||
return await getLocalFilePath(musicInfo)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建本地音乐信息对象
|
||||
* @param path 文件路径
|
||||
* @returns
|
||||
*/
|
||||
export const createLocalMusicInfo = async(path: string): Promise<LX.Music.MusicInfoLocal | null> => {
|
||||
if (!await checkPath(path)) return null
|
||||
const { parseFile } = await import('music-metadata')
|
||||
|
||||
let metadata
|
||||
try {
|
||||
metadata = await parseFile(path)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return null
|
||||
}
|
||||
|
||||
// console.log(metadata)
|
||||
let ext = extname(path)
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
let name = (metadata.common.title || basename(path, ext)).trim()
|
||||
let singer = metadata.common.artists?.length ? metadata.common.artists.map(a => a.trim()).join('、') : ''
|
||||
let interval = metadata.format.duration ? formatPlayTime(metadata.format.duration) : ''
|
||||
let albumName = metadata.common.album?.trim() ?? ''
|
||||
|
||||
return {
|
||||
id: path,
|
||||
name,
|
||||
singer,
|
||||
source: 'local',
|
||||
interval,
|
||||
meta: {
|
||||
albumName,
|
||||
filePath: path,
|
||||
songId: path,
|
||||
picUrl: '',
|
||||
ext: ext.replace(/^\./, ''),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let prevFileInfo: {
|
||||
path: string
|
||||
promise: Promise<LX.MusicMetadataModule.IAudioMetadata | null>
|
||||
} = {
|
||||
path: '',
|
||||
promise: Promise.resolve(null),
|
||||
}
|
||||
const getFileMetadata = async(path: string) => {
|
||||
if (prevFileInfo.path == path) return prevFileInfo.promise
|
||||
prevFileInfo.path = path
|
||||
return prevFileInfo.promise = checkPath(path).then(async(isExist) => {
|
||||
return isExist ? import('music-metadata').then(async({ parseFile }) => parseFile(path)) : null
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 获取歌曲文件封面图片
|
||||
* @param path 路径
|
||||
*/
|
||||
export const getLocalMusicFilePic = async(path: string) => {
|
||||
const metadata = await getFileMetadata(path)
|
||||
if (!metadata) return null
|
||||
const { selectCover } = await import('music-metadata')
|
||||
return selectCover(metadata.common.picture)
|
||||
}
|
||||
|
||||
const timeExp = /^\[([\d:.]*)\]{1}/
|
||||
/**
|
||||
* 解析歌词文件,分离可能存在的翻译、罗马音歌词
|
||||
* @param lrc 歌词内容
|
||||
* @returns
|
||||
*/
|
||||
export const parseLyric = (lrc: string): LX.Music.LyricInfo => {
|
||||
const lines = lrc.split(/\r\n|\r|\n/)
|
||||
const lyrics: string[][] = []
|
||||
const map = new Map<string, number>()
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim()
|
||||
let result = timeExp.exec(line)
|
||||
if (result) {
|
||||
const index = map.get(result[1]) ?? 0
|
||||
if (!lyrics[index]) lyrics[index] = []
|
||||
lyrics[index].push(line)
|
||||
map.set(result[1], index + 1)
|
||||
} else {
|
||||
if (!lyrics[0]) lyrics[0] = []
|
||||
lyrics[0].push(line)
|
||||
}
|
||||
}
|
||||
const lyricInfo: LX.Music.LyricInfo = {
|
||||
lyric: lyrics[0].join('\n'),
|
||||
tlyric: '',
|
||||
}
|
||||
if (lyrics[1]) lyricInfo.tlyric = lyrics[1].join('\n')
|
||||
if (lyrics[2]) lyricInfo.rlyric = lyrics[2].join('\n')
|
||||
|
||||
return lyricInfo
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取歌曲文件歌词
|
||||
* @param path 路径
|
||||
*/
|
||||
export const getLocalMusicFileLyric = async(path: string): Promise<string | null> => {
|
||||
// 尝试读取同目录下的同名lrc文件
|
||||
const lrcPath = path.replace(new RegExp('\\' + extname(path) + '$'), '.lrc')
|
||||
const stats = await getFileStats(lrcPath)
|
||||
// console.log(lrcPath, stats)
|
||||
if (stats && stats.size < 1024 * 1024 * 10) {
|
||||
const lrcBuf = await readFile(lrcPath)
|
||||
const { detect } = await import('jschardet')
|
||||
const { confidence, encoding } = detect(lrcBuf)
|
||||
console.log('lrc file encoding', confidence, encoding)
|
||||
if (confidence > 0.8) {
|
||||
const iconv = await import('iconv-lite')
|
||||
if (iconv.encodingExists(encoding)) {
|
||||
const lrc = iconv.decode(lrcBuf, encoding)
|
||||
if (lrc) return lrc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试读取文件内歌词
|
||||
const metadata = await getFileMetadata(path)
|
||||
if (!metadata) return null
|
||||
if (metadata.common.lyrics?.length && metadata.common.lyrics[0].length > 10) {
|
||||
return metadata.common.lyrics[0]
|
||||
}
|
||||
// console.log(metadata)
|
||||
for (const info of Object.values(metadata.native)) {
|
||||
const ust = info.find(i => i.id == 'USLT')
|
||||
if (ust && ust.value.text.length > 10) return ust.value.text
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import apiSourceInfo from './api-source-info'
|
||||
import { apiSource, userApi } from '@renderer/core/share'
|
||||
import { apiSource, userApi } from '@renderer/store'
|
||||
|
||||
const apiList = {}
|
||||
const supportQuality = {}
|
||||
+3
-3
@@ -62,15 +62,15 @@ export default {
|
||||
})
|
||||
return list
|
||||
},
|
||||
search(str, page = 1, { limit } = {}, retryNum = 0) {
|
||||
search(str, page = 1, limit, retryNum = 0) {
|
||||
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
|
||||
if (limit == null) limit = this.limit
|
||||
|
||||
return this.musicSearch(str, page, limit).then(result => {
|
||||
if (!result || result.error_code !== 22000) return this.search(str, page, { limit }, retryNum)
|
||||
if (!result || result.error_code !== 22000) return this.search(str, page, limit, retryNum)
|
||||
let list = this.handleResult(result.result.song_info.song_list)
|
||||
|
||||
if (list == null) return this.search(str, page, { limit }, retryNum)
|
||||
if (list == null) return this.search(str, page, limit, retryNum)
|
||||
|
||||
this.total = result.result.song_info.total
|
||||
this.page = page
|
||||
@@ -1,9 +1,9 @@
|
||||
import kw from './kw'
|
||||
import kg from './kg'
|
||||
import tx from './tx'
|
||||
import wy from './wy'
|
||||
import mg from './mg'
|
||||
import bd from './bd'
|
||||
import kw from './kw/index'
|
||||
import kg from './kg/index'
|
||||
import tx from './tx/index'
|
||||
import wy from './wy/index'
|
||||
import mg from './mg/index'
|
||||
import bd from './bd/index'
|
||||
import xm from './xm'
|
||||
import { supportQuality } from './api-source'
|
||||
|
||||
@@ -59,9 +59,12 @@ export default {
|
||||
},
|
||||
supportQuality,
|
||||
|
||||
async findMusic(musicInfo) {
|
||||
async findMusic({ name, singer, albumName, interval, source: s }) {
|
||||
const tasks = []
|
||||
const sortSingle = singer => singer.includes('、') ? singer.split('、').sort((a, b) => a.charCodeAt(0) - b.charCodeAt(0)).join('、') : singer
|
||||
const singersRxp = /、|&|;|;|\/|,|,|\|/
|
||||
const sortSingle = singer => singersRxp.test(singer)
|
||||
? singer.split(singersRxp).sort((a, b) => a.localeCompare(b)).join('、')
|
||||
: singer
|
||||
const sortMusic = (arr, callback) => {
|
||||
const tempResult = []
|
||||
for (let i = arr.length - 1; i > -1; i--) {
|
||||
@@ -78,35 +81,36 @@ export default {
|
||||
return tempResult
|
||||
}
|
||||
const trimStr = str => typeof str == 'string' ? str.trim() : str
|
||||
const sortedSinger = String(sortSingle(musicInfo.singer)).toLowerCase()
|
||||
const musicName = trimStr(musicInfo.name)
|
||||
const lowerCaseName = String(musicName).toLowerCase()
|
||||
const lowerCaseAlbumName = String(musicInfo.albumName).toLowerCase()
|
||||
const filterStr = str => typeof str == 'string' ? str.replace(/\s|'|\.|,|,|&|"|、|\(|\)|(|)|`|~|-|<|>|\||\/|\]|\[/g, '') : str
|
||||
const musicName = trimStr(name)
|
||||
const sortedSinger = filterStr(String(sortSingle(singer)).toLowerCase())
|
||||
const lowerCaseName = filterStr(String(musicName).toLowerCase())
|
||||
const lowerCaseAlbumName = filterStr(String(albumName).toLowerCase())
|
||||
const excludeSource = ['xm']
|
||||
for (const source of sources.sources) {
|
||||
if (!sources[source.id].musicSearch || source.id === musicInfo.source || excludeSource.includes(source.id)) continue
|
||||
if (!sources[source.id].musicSearch || source.id == s || excludeSource.includes(source.id)) continue
|
||||
|
||||
tasks.push(sources[source.id].musicSearch.search(`${musicName} ${musicInfo.singer || ''}`.trim(), 1, { limit: 25 }).then(res => {
|
||||
tasks.push(sources[source.id].musicSearch.search(`${musicName} ${singer || ''}`.trim(), 1, 25).then(res => {
|
||||
for (const item of res.list) {
|
||||
item.sortedSinger = String(sortSingle(item.singer)).toLowerCase()
|
||||
item.name = trimStr(item.name)
|
||||
item.lowerCaseName = String(item.name).toLowerCase()
|
||||
item.lowerCaseAlbumName = String(item.albumName).toLowerCase()
|
||||
// console.log(lowerCaseName, item.lowerCaseName)
|
||||
item.sortedSinger = filterStr(String(sortSingle(item.singer)).toLowerCase())
|
||||
item.lowerCaseName = filterStr(String(item.name ?? '').toLowerCase())
|
||||
item.lowerCaseAlbumName = filterStr(String(item.albumName ?? '').toLowerCase())
|
||||
// console.log(lowerCaseName, item.lowerCaseName, item.source)
|
||||
if (
|
||||
(
|
||||
item.sortedSinger === sortedSinger && item.lowerCaseName === lowerCaseName
|
||||
item.sortedSinger == sortedSinger && item.lowerCaseName == lowerCaseName
|
||||
) ||
|
||||
(
|
||||
item.interval === musicInfo.interval && item.lowerCaseName === lowerCaseName &&
|
||||
(interval ? item.interval == interval : true) && item.lowerCaseName == lowerCaseName &&
|
||||
(item.sortedSinger.includes(sortedSinger) || sortedSinger.includes(item.sortedSinger))
|
||||
) ||
|
||||
(
|
||||
item.lowerCaseName === lowerCaseName && item.lowerCaseAlbumName === lowerCaseAlbumName &&
|
||||
item.interval === musicInfo.interval
|
||||
item.lowerCaseName == lowerCaseName && (lowerCaseAlbumName ? item.lowerCaseAlbumName == lowerCaseAlbumName : true) &&
|
||||
(interval ? item.interval == interval : true)
|
||||
) ||
|
||||
(
|
||||
item.lowerCaseName === lowerCaseName && item.lowerCaseAlbumName === lowerCaseAlbumName &&
|
||||
item.lowerCaseName == lowerCaseName && (lowerCaseAlbumName ? item.lowerCaseAlbumName == lowerCaseAlbumName : true) &&
|
||||
(item.sortedSinger.includes(sortedSinger) || sortedSinger.includes(item.sortedSinger))
|
||||
)
|
||||
) {
|
||||
@@ -119,10 +123,10 @@ export default {
|
||||
const result = (await Promise.all(tasks)).filter(s => s)
|
||||
const newResult = []
|
||||
if (result.length) {
|
||||
newResult.push(...sortMusic(result, item => item.sortedSinger === sortedSinger && item.lowerCaseName === lowerCaseName && item.interval === musicInfo.interval))
|
||||
newResult.push(...sortMusic(result, item => item.lowerCaseName === lowerCaseName && item.sortedSinger === sortedSinger && item.lowerCaseAlbumName === lowerCaseAlbumName))
|
||||
newResult.push(...sortMusic(result, item => item.sortedSinger === sortedSinger && item.lowerCaseName === lowerCaseName))
|
||||
newResult.push(...sortMusic(result, item => item.sortedSinger === sortedSinger && item.interval === musicInfo.interval))
|
||||
newResult.push(...sortMusic(result, item => item.sortedSinger == sortedSinger && item.lowerCaseName == lowerCaseName && item.interval == interval))
|
||||
newResult.push(...sortMusic(result, item => item.lowerCaseName == lowerCaseName && item.sortedSinger == sortedSinger && item.lowerCaseAlbumName == lowerCaseAlbumName))
|
||||
newResult.push(...sortMusic(result, item => item.sortedSinger == sortedSinger && item.lowerCaseName == lowerCaseName))
|
||||
newResult.push(...sortMusic(result, item => item.sortedSinger == sortedSinger && item.interval == interval))
|
||||
for (const item of result) {
|
||||
delete item.sortedSinger
|
||||
delete item.lowerCaseName
|
||||
@@ -1,5 +1,5 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { decodeName, dateFormat2 } from '../../'
|
||||
import { decodeName, dateFormat2 } from '../../index'
|
||||
|
||||
export default {
|
||||
_requestObj: null,
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { decodeName } from '../..'
|
||||
import { decodeName } from '../../index'
|
||||
|
||||
export default {
|
||||
_requestObj: null,
|
||||
@@ -1,6 +1,6 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { decodeLyric } from './util'
|
||||
import { decodeName } from '../..'
|
||||
import { decodeName } from '../../index'
|
||||
|
||||
const headExp = /^.*\[id:\$\w+\]\n/
|
||||
|
||||
@@ -9,14 +9,19 @@ const parseLyric = str => {
|
||||
if (headExp.test(str)) str = str.replace(headExp, '')
|
||||
let trans = str.match(/\[language:([\w=\\/+]+)\]/)
|
||||
let lyric
|
||||
let rlyric
|
||||
let tlyric
|
||||
if (trans) {
|
||||
str = str.replace(/\[language:[\w=\\/+]+\]\n/, '')
|
||||
let json = JSON.parse(Buffer.from(trans[1], 'base64').toString())
|
||||
for (const item of json.content) {
|
||||
if (item.type == 1) {
|
||||
tlyric = item.lyricContent
|
||||
break
|
||||
switch (item.type) {
|
||||
case 0:
|
||||
rlyric = item.lyricContent
|
||||
break
|
||||
case 1:
|
||||
tlyric = item.lyricContent
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,17 +35,22 @@ const parseLyric = str => {
|
||||
time %= 60
|
||||
let s = parseInt(time).toString().padStart(2, '0')
|
||||
time = `${m}:${s}.${ms}`
|
||||
if (tlyric) tlyric[i] = `[${time}]${tlyric[i++][0]}`
|
||||
if (rlyric) rlyric[i] = `[${time}]${rlyric[i]?.join('') ?? ''}`
|
||||
if (tlyric) tlyric[i] = `[${time}]${tlyric[i]?.join('') ?? ''}`
|
||||
i++
|
||||
return str.replace(result[1], time)
|
||||
})
|
||||
rlyric = rlyric ? rlyric.join('\n') : ''
|
||||
tlyric = tlyric ? tlyric.join('\n') : ''
|
||||
lxlyric = lxlyric.replace(/<(\d+,\d+),\d+>/g, '<$1>')
|
||||
lxlyric = decodeName(lxlyric)
|
||||
lyric = lxlyric.replace(/<\d+,\d+>/g, '')
|
||||
rlyric = decodeName(rlyric)
|
||||
tlyric = decodeName(tlyric)
|
||||
return {
|
||||
lyric,
|
||||
tlyric,
|
||||
rlyric,
|
||||
lxlyric,
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -77,15 +77,15 @@ export default {
|
||||
})
|
||||
return list
|
||||
},
|
||||
search(str, page = 1, { limit } = {}, retryNum = 0) {
|
||||
search(str, page = 1, limit, retryNum = 0) {
|
||||
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
|
||||
if (limit == null) limit = this.limit
|
||||
// http://newlyric.kuwo.cn/newlyric.lrc?62355680
|
||||
return this.musicSearch(str, page, limit).then(result => {
|
||||
if (!result || result.errcode !== 0) return this.search(str, page, { limit }, retryNum)
|
||||
if (!result || result.errcode !== 0) return this.search(str, page, limit, retryNum)
|
||||
let list = this.handleResult(result.data.info)
|
||||
|
||||
if (list == null) return this.search(str, page, { limit }, retryNum)
|
||||
if (list == null) return this.search(str, page, limit, retryNum)
|
||||
|
||||
this.total = result.data.total
|
||||
this.page = page
|
||||
@@ -151,6 +151,7 @@ export default {
|
||||
name: item.specialname,
|
||||
time: item.publish_time || item.publishtime,
|
||||
img: item.img || item.imgurl,
|
||||
total: item.songcount,
|
||||
grade: item.grade,
|
||||
desc: item.intro,
|
||||
source: 'kg',
|
||||
@@ -723,6 +724,35 @@ export default {
|
||||
}
|
||||
return `https://www.kugou.com/yy/special/single/${id}.html`
|
||||
},
|
||||
|
||||
search(text, page, limit = 20) {
|
||||
// http://msearchretry.kugou.com/api/v3/search/special?version=9209&keyword=%E5%91%A8%E6%9D%B0%E4%BC%A6&pagesize=20&filter=0&page=1&sver=2&with_res_tag=0
|
||||
// return httpFetch(`http://ioscdn.kugou.com/api/v3/search/special?keyword=${encodeURIComponent(text)}&page=${page}&pagesize=${limit}&showtype=10&plat=2&version=7910&correct=1&sver=5`)
|
||||
return httpFetch(`http://msearchretry.kugou.com/api/v3/search/special?keyword=${encodeURIComponent(text)}&page=${page}&pagesize=${limit}&showtype=10&filter=0&version=7910&sver=2`)
|
||||
.promise.then(({ body }) => {
|
||||
if (body.errcode != 0) throw new Error('filed')
|
||||
// console.log(body.data.info)
|
||||
return {
|
||||
list: body.data.info.map(item => {
|
||||
return {
|
||||
play_count: this.formatPlayCount(item.playcount),
|
||||
id: 'id_' + item.specialid,
|
||||
author: item.nickname,
|
||||
name: item.specialname,
|
||||
time: item.publishtime,
|
||||
img: item.imgurl,
|
||||
grade: item.grade,
|
||||
desc: item.intro,
|
||||
total: item.songcount,
|
||||
source: 'kg',
|
||||
}
|
||||
}),
|
||||
limit,
|
||||
total: body.data.total,
|
||||
source: 'kg',
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// getList
|
||||
@@ -44,8 +44,8 @@ export default {
|
||||
}
|
||||
}
|
||||
if (formats.includes('HIRFLAC')) {
|
||||
types.push({ type: 'flac32bit', size: null })
|
||||
_types.flac32bit = {
|
||||
types.push({ type: 'flac24bit', size: null })
|
||||
_types.flac24bit = {
|
||||
size: null,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { dateFormat2 } from '../../'
|
||||
import { dateFormat2 } from '../../index'
|
||||
|
||||
export default {
|
||||
_requestObj: null,
|
||||
+2
-2
@@ -116,8 +116,8 @@ export default {
|
||||
}
|
||||
}
|
||||
if (formats.includes('HIRFLAC')) {
|
||||
types.push({ type: 'flac32bit', size: null })
|
||||
_types.flac32bit = {
|
||||
types.push({ type: 'flac24bit', size: null })
|
||||
_types.flac24bit = {
|
||||
size: null,
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -47,8 +47,8 @@ export default {
|
||||
if (info) {
|
||||
switch (info[2]) {
|
||||
case '4000':
|
||||
types.push({ type: 'flac32bit', size: info[4] })
|
||||
_types.flac32bit = {
|
||||
types.push({ type: 'flac24bit', size: info[4] })
|
||||
_types.flac24bit = {
|
||||
size: info[4].toLocaleUpperCase(),
|
||||
}
|
||||
break
|
||||
@@ -100,16 +100,16 @@ export default {
|
||||
// console.log(result)
|
||||
return result
|
||||
},
|
||||
search(str, page = 1, { limit } = {}, retryNum = 0) {
|
||||
search(str, page = 1, limit, retryNum = 0) {
|
||||
if (retryNum > 2) return Promise.reject(new Error('try max num'))
|
||||
if (limit == null) limit = this.limit
|
||||
// http://newlyric.kuwo.cn/newlyric.lrc?62355680
|
||||
return this.musicSearch(str, page, limit).then(({ body: result }) => {
|
||||
// console.log(result)
|
||||
if (!result || (result.TOTAL !== '0' && result.SHOW === '0')) return this.search(str, page, { limit }, ++retryNum)
|
||||
if (!result || (result.TOTAL !== '0' && result.SHOW === '0')) return this.search(str, page, limit, ++retryNum)
|
||||
let list = this.handleResult(result.abslist)
|
||||
|
||||
if (list == null) return this.search(str, page, { limit })
|
||||
if (list == null) return this.search(str, page, limit, ++retryNum)
|
||||
|
||||
this.total = parseInt(result.TOTAL)
|
||||
this.page = page
|
||||
+30
-4
@@ -1,6 +1,6 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { formatPlayTime, decodeName } from '../../index'
|
||||
import { formatSinger } from './util'
|
||||
import { formatSinger, objStr2JSON } from './util'
|
||||
import album from './album'
|
||||
|
||||
export default {
|
||||
@@ -108,7 +108,7 @@ export default {
|
||||
source: 'kw',
|
||||
}
|
||||
} else if (!body.length) {
|
||||
return this.getList(sortId, id, type, page, ++tryNum)
|
||||
return this.getList(sortId, tagId, page, ++tryNum)
|
||||
}
|
||||
return {
|
||||
list: this.filterList2(body),
|
||||
@@ -252,8 +252,8 @@ export default {
|
||||
if (info) {
|
||||
switch (info[2]) {
|
||||
case '4000':
|
||||
types.push({ type: 'flac32bit', size: info[4] })
|
||||
_types.flac32bit = {
|
||||
types.push({ type: 'flac24bit', size: info[4] })
|
||||
_types.flac24bit = {
|
||||
size: info[4].toLocaleUpperCase(),
|
||||
}
|
||||
break
|
||||
@@ -309,6 +309,32 @@ export default {
|
||||
}
|
||||
return `http://www.kuwo.cn/playlist_detail/${id}`
|
||||
},
|
||||
|
||||
search(text, page, limit = 20) {
|
||||
return httpFetch(`http://search.kuwo.cn/r.s?all=${encodeURIComponent(text)}&pn=${page - 1}&rn=${limit}&rformat=json&encoding=utf8&ver=mbox&vipver=MUSIC_8.7.7.0_BCS37&plat=pc&devid=28156413&ft=playlist&pay=0&needliveshow=0`)
|
||||
.promise.then(({ body }) => {
|
||||
body = objStr2JSON(body)
|
||||
// console.log(body)
|
||||
return {
|
||||
list: body.abslist.map(item => {
|
||||
return {
|
||||
play_count: this.formatPlayCount(item.playcnt),
|
||||
id: item.playlistid,
|
||||
author: decodeName(item.nickname),
|
||||
name: decodeName(item.name),
|
||||
total: item.songnum,
|
||||
// time: item.publish_time,
|
||||
img: item.pic,
|
||||
desc: decodeName(item.intro),
|
||||
source: 'kw',
|
||||
}
|
||||
}),
|
||||
limit,
|
||||
total: parseInt(body.TOTAL),
|
||||
source: 'kw',
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// getList
|
||||
@@ -1,31 +1,38 @@
|
||||
import { httpGet, httpFetch } from '../../request'
|
||||
import { rendererInvoke, NAMES } from '@common/ipc'
|
||||
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
|
||||
import { rendererInvoke } from '@common/rendererIpc'
|
||||
|
||||
const kw_token = {
|
||||
token: null,
|
||||
isGetingToken: false,
|
||||
}
|
||||
|
||||
const translationMap = {
|
||||
"{'": '{"',
|
||||
"'}\n": '"}',
|
||||
"'}": '"}',
|
||||
"':'": '":"',
|
||||
"','": '","',
|
||||
"':{'": '":{"',
|
||||
"':['": '":["',
|
||||
"'}],'": '"}],"',
|
||||
"':[{'": '":[{"',
|
||||
"'},'": '"},"',
|
||||
"'},{'": '"},{"',
|
||||
"':[],'": '":[],"',
|
||||
"':{},'": '":{},"',
|
||||
}
|
||||
// const translationMap = {
|
||||
// "{'": '{"',
|
||||
// "'}\n": '"}',
|
||||
// "'}": '"}',
|
||||
// "':'": '":"',
|
||||
// "','": '","',
|
||||
// "':{'": '":{"',
|
||||
// "':['": '":["',
|
||||
// "'}],'": '"}],"',
|
||||
// "':[{'": '":[{"',
|
||||
// "'},'": '"},"',
|
||||
// "'},{'": '"},{"',
|
||||
// "':[],'": '":[],"',
|
||||
// "':{},'": '":{},"',
|
||||
// "'}]}": '"}]}',
|
||||
// }
|
||||
|
||||
// export const objStr2JSON = str => {
|
||||
// return JSON.parse(str.replace(/(^{'|'}\n$|'}$|':'|','|':\[{'|'}\],'|':{'|'},'|'},{'|':\['|':\[\],'|':{},'|'}]})/g, s => translationMap[s]))
|
||||
// }
|
||||
|
||||
export const objStr2JSON = str => {
|
||||
return JSON.parse(str.replace(/(^{'|'}\n$|'}$|':'|','|':\[{'|'}\],'|':{'|'},'|'},{'|':\['|':\[\],'|':{},')/g, s => translationMap[s]))
|
||||
return JSON.parse(str.replace(/('(?=(,\s*')))|('(?=:))|((?<=([:,]\s*))')|((?<={)')|('(?=}))/g, '"'))
|
||||
}
|
||||
|
||||
|
||||
export const formatSinger = rawData => rawData.replace(/&/g, '、')
|
||||
|
||||
export const matchToken = headers => {
|
||||
@@ -54,7 +61,7 @@ export const getToken = (retryNum = 0) => new Promise((resolve, reject) => {
|
||||
})
|
||||
})
|
||||
|
||||
export const decodeLyric = base64Data => rendererInvoke(NAMES.mainWindow.handle_kw_decode_lyric, base64Data)
|
||||
export const decodeLyric = base64Data => rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.handle_kw_decode_lyric, base64Data)
|
||||
|
||||
export const tokenRequest = async(url, options = {}) => {
|
||||
let token = kw_token.token
|
||||
@@ -1,6 +1,6 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import album from './album'
|
||||
import { dateFormat2 } from '../../'
|
||||
import { dateFormat2 } from '../../index'
|
||||
|
||||
export default {
|
||||
_requestObj: null,
|
||||
+2
-2
@@ -125,8 +125,8 @@ export default {
|
||||
break
|
||||
case 'ZQ':
|
||||
size = sizeFormate(type.size ?? type.androidSize)
|
||||
types.push({ type: 'flac32bit', size })
|
||||
_types.flac32bit = {
|
||||
types.push({ type: 'flac24bit', size })
|
||||
_types.flac24bit = {
|
||||
size,
|
||||
}
|
||||
break
|
||||
+4
-4
@@ -69,8 +69,8 @@ export default {
|
||||
break
|
||||
case 'ZQ':
|
||||
size = sizeFormate(type.size ?? type.androidSize)
|
||||
types.push({ type: 'flac32bit', size })
|
||||
_types.flac32bit = {
|
||||
types.push({ type: 'flac24bit', size })
|
||||
_types.flac24bit = {
|
||||
size,
|
||||
}
|
||||
break
|
||||
@@ -107,7 +107,7 @@ export default {
|
||||
})
|
||||
return list
|
||||
},
|
||||
search(str, page = 1, { limit } = {}, retryNum = 0) {
|
||||
search(str, page = 1, limit, retryNum = 0) {
|
||||
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
|
||||
if (limit == null) limit = this.limit
|
||||
// http://newlyric.kuwo.cn/newlyric.lrc?62355680
|
||||
@@ -117,7 +117,7 @@ export default {
|
||||
const songResultData = result.songResultData || { result: [], totalCount: 0 }
|
||||
|
||||
let list = this.handleResult(songResultData.result)
|
||||
if (list == null) return this.search(str, page, { limit }, retryNum)
|
||||
if (list == null) return this.search(str, page, limit, retryNum)
|
||||
|
||||
this.total = parseInt(songResultData.totalCount)
|
||||
this.page = page
|
||||
+37
-5
@@ -134,7 +134,7 @@ export default {
|
||||
return Promise.reject(new Error('link get failed'))
|
||||
},
|
||||
|
||||
getListDetail(id, page) { // 获取歌曲列表内的音乐
|
||||
getListDetail(id, page, retryNum = 0) { // 获取歌曲列表内的音乐
|
||||
// https://h5.nf.migu.cn/app/v4/p/share/playlist/index.html?id=184187437&channel=0146921
|
||||
// http://c.migu.cn/00bTY6?ifrom=babddaadfde4ebeda289d671ab62f236
|
||||
if (/playlist\/index\.html\?/.test(id)) {
|
||||
@@ -147,8 +147,8 @@ export default {
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
this.getListDetailList(id, page),
|
||||
this.getListDetailInfo(id),
|
||||
this.getListDetailList(id, page, retryNum),
|
||||
this.getListDetailInfo(id, retryNum),
|
||||
]).then(([listData, info]) => {
|
||||
listData.info = info
|
||||
return listData
|
||||
@@ -189,8 +189,8 @@ export default {
|
||||
break
|
||||
case 'ZQ':
|
||||
size = sizeFormate(type.size ?? type.androidSize)
|
||||
types.push({ type: 'flac32bit', size })
|
||||
_types.flac32bit = {
|
||||
types.push({ type: 'flac24bit', size })
|
||||
_types.flac24bit = {
|
||||
size,
|
||||
}
|
||||
break
|
||||
@@ -361,6 +361,38 @@ export default {
|
||||
}
|
||||
return `https://music.migu.cn/v3/music/playlist/${id}`
|
||||
},
|
||||
|
||||
search(text, page, limit = 20) {
|
||||
// return httpFetch(`http://search.kuwo.cn/r.s?all=${encodeURIComponent(text)}&pn=${page - 1}&rn=${limit}&rformat=json&encoding=utf8&ver=mbox&vipver=MUSIC_8.7.7.0_BCS37&plat=pc&devid=28156413&ft=playlist&pay=0&needliveshow=0`)
|
||||
return httpFetch(`http://pd.musicapp.migu.cn/MIGUM2.0/v1.0/content/search_all.do?ua=Android_migu&version=5.0.1&text=${encodeURIComponent(text)}&pageNo=${page}&pageSize=${limit}&searchSwitch=%7B%22song%22%3A0%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A0%2C%22mvSong%22%3A0%2C%22bestShow%22%3A1%2C%22songlist%22%3A1%2C%22lyricSong%22%3A0%7D`, {
|
||||
headers: {
|
||||
'User-Agent': 'okhttp/3.9.1',
|
||||
osVersion: 'android 7.0',
|
||||
},
|
||||
// family: 6,
|
||||
})
|
||||
.promise.then(({ body }) => {
|
||||
// console.log(body)
|
||||
if (body.code != this.successCode) throw new Error('failed')
|
||||
return {
|
||||
list: body.songListResultData.result.map(item => {
|
||||
return {
|
||||
play_count: this.formatPlayCount(item.playcount),
|
||||
id: item.id,
|
||||
// author: item.createName,
|
||||
name: item.name,
|
||||
// time: item.createTime,
|
||||
img: item.musicListPicUrl,
|
||||
total: item.musicNum,
|
||||
source: 'mg',
|
||||
}
|
||||
}),
|
||||
limit,
|
||||
total: parseInt(body.songListResultData.totalCount),
|
||||
source: 'mg',
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// getList
|
||||
@@ -1,5 +1,5 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { dateFormat2 } from '../../'
|
||||
import { dateFormat2 } from '../../index'
|
||||
|
||||
const emojis = {
|
||||
e400846: '😘',
|
||||
+2
-2
@@ -139,8 +139,8 @@ export default {
|
||||
}
|
||||
if (item.file.size_hires !== 0) {
|
||||
let size = sizeFormate(item.file.size_hires)
|
||||
types.push({ type: 'flac32bit', size })
|
||||
_types.flac32bit = {
|
||||
types.push({ type: 'flac24bit', size })
|
||||
_types.flac24bit = {
|
||||
size,
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -84,8 +84,8 @@ export default {
|
||||
}
|
||||
if (file.size_hires !== 0) {
|
||||
let size = sizeFormate(file.size_hires)
|
||||
types.push({ type: 'flac32bit', size })
|
||||
_types.flac32bit = {
|
||||
types.push({ type: 'flac24bit', size })
|
||||
_types.flac24bit = {
|
||||
size,
|
||||
}
|
||||
}
|
||||
@@ -118,7 +118,7 @@ export default {
|
||||
// console.log(list)
|
||||
return list
|
||||
},
|
||||
search(str, page = 1, { limit } = {}) {
|
||||
search(str, page = 1, limit) {
|
||||
if (limit == null) limit = this.limit
|
||||
// http://newlyric.kuwo.cn/newlyric.lrc?62355680
|
||||
return this.musicSearch(str, page, limit).then(({ body, meta }) => {
|
||||
+34
-2
@@ -262,8 +262,8 @@ export default {
|
||||
}
|
||||
if (item.file.size_hires !== 0) {
|
||||
let size = sizeFormate(item.file.size_hires)
|
||||
types.push({ type: 'flac32bit', size })
|
||||
_types.flac32bit = {
|
||||
types.push({ type: 'flac24bit', size })
|
||||
_types.flac24bit = {
|
||||
size,
|
||||
}
|
||||
}
|
||||
@@ -299,6 +299,38 @@ export default {
|
||||
|
||||
return `https://y.qq.com/n/ryqq/playlist/${id}`
|
||||
},
|
||||
|
||||
search(text, page, limit = 20) {
|
||||
return httpFetch(`http://c.y.qq.com/soso/fcgi-bin/client_music_search_songlist?page_no=${page - 1}&num_per_page=${limit}&format=json&query=${encodeURIComponent(text)}&remoteplace=txt.yqq.playlist&inCharset=utf8&outCharset=utf-8`, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',
|
||||
Referer: 'http://y.qq.com/portal/search.html',
|
||||
},
|
||||
})
|
||||
.promise.then(({ body }) => {
|
||||
if (body.code != 0) throw new Error('filed')
|
||||
// console.log(body.data.list)
|
||||
return {
|
||||
list: body.data.list.map(item => {
|
||||
return {
|
||||
play_count: this.formatPlayCount(item.listennum),
|
||||
id: item.dissid,
|
||||
author: item.creator.name,
|
||||
name: item.dissname,
|
||||
time: item.createtime,
|
||||
img: item.imgurl,
|
||||
// grade: item.favorcnt / 10,
|
||||
total: item.song_count,
|
||||
desc: item.introduction,
|
||||
source: 'tx',
|
||||
}
|
||||
}),
|
||||
limit,
|
||||
total: body.data.sum,
|
||||
source: 'tx',
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// getList
|
||||
@@ -1,25 +1,5 @@
|
||||
import crypto from 'crypto'
|
||||
import dns from 'dns'
|
||||
import { qualityList } from '@renderer/core/share'
|
||||
|
||||
|
||||
/**
|
||||
* 获取音乐音质
|
||||
* @param {*} info
|
||||
* @param {*} type
|
||||
*/
|
||||
|
||||
const types = ['flac32bit', 'flac', 'wav', 'ape', '320k', '192k', '128k']
|
||||
export const getMusicType = (info, type) => {
|
||||
let list = qualityList.value[info.source]
|
||||
if (!list) return '128k'
|
||||
if (!list.includes(type)) type = list[list.length - 1]
|
||||
const rangeType = types.slice(types.indexOf(type))
|
||||
for (const type of rangeType) {
|
||||
if (info._types[type]) return type
|
||||
}
|
||||
return '128k'
|
||||
}
|
||||
|
||||
export const toMD5 = str => crypto.createHash('md5').update(str).digest('hex')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { weapi } from './utils/crypto'
|
||||
import { dateFormat2 } from '../../'
|
||||
import { dateFormat2 } from '../../index'
|
||||
|
||||
let cursorTools = {
|
||||
cache: {},
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { httpFetch } from '../../request'
|
||||
import { weapi } from './utils/crypto'
|
||||
import { formatPlayTime, sizeFormate } from '../..'
|
||||
import { formatPlayTime, sizeFormate } from '../../index'
|
||||
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/song_detail.js
|
||||
|
||||
export default {
|
||||
+4
-4
@@ -2,7 +2,7 @@
|
||||
// import { weapi } from './utils/crypto'
|
||||
import { sizeFormate, formatPlayTime } from '../../index'
|
||||
// import musicDetailApi from './musicDetail'
|
||||
import { eapiRequest } from './utils'
|
||||
import { eapiRequest } from './utils/index'
|
||||
|
||||
export default {
|
||||
limit: 30,
|
||||
@@ -73,16 +73,16 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
search(str, page = 1, { limit } = {}, retryNum = 0) {
|
||||
search(str, page = 1, limit, retryNum = 0) {
|
||||
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
|
||||
if (limit == null) limit = this.limit
|
||||
return this.musicSearch(str, page, limit).then(result => {
|
||||
// console.log(result)
|
||||
if (!result || result.code !== 200) return this.search(str, page, { limit }, retryNum)
|
||||
if (!result || result.code !== 200) return this.search(str, page, limit, retryNum)
|
||||
let list = this.handleResult(result.result.songs || [])
|
||||
// console.log(list)
|
||||
|
||||
if (list == null) return this.search(str, page, { limit }, retryNum)
|
||||
if (list == null) return this.search(str, page, limit, retryNum)
|
||||
|
||||
this.total = result.result.songCount || 0
|
||||
this.page = page
|
||||
@@ -7,6 +7,7 @@ import { weapi, linuxapi } from './utils/crypto'
|
||||
import { httpFetch } from '../../request'
|
||||
import { formatPlayTime, sizeFormate } from '../../index'
|
||||
import musicDetailApi from './musicDetail'
|
||||
import { eapiRequest } from './utils/index'
|
||||
|
||||
export default {
|
||||
_requestObj_tags: null,
|
||||
@@ -297,6 +298,26 @@ export default {
|
||||
const { id } = await this.getListId(rawId)
|
||||
return `https://music.163.com/#/playlist?id=${id}`
|
||||
},
|
||||
|
||||
search(text, page, limit = 20) {
|
||||
return eapiRequest('/api/cloudsearch/pc', {
|
||||
s: text,
|
||||
type: 1000, // 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频
|
||||
limit,
|
||||
total: page == 1,
|
||||
offset: limit * (page - 1),
|
||||
})
|
||||
.promise.then(({ body }) => {
|
||||
if (body.code != this.successCode) throw new Error('filed')
|
||||
// console.log(body)
|
||||
return {
|
||||
list: this.filterList(body.result.playlists),
|
||||
limit,
|
||||
total: body.result.playlistCount,
|
||||
source: 'wy',
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// getList
|
||||
@@ -0,0 +1,91 @@
|
||||
import { throttle } from '@common/utils/common'
|
||||
import Pickr from '@simonwep/pickr'
|
||||
import '@simonwep/pickr/dist/themes/classic.min.css'
|
||||
|
||||
export interface PickrTools {
|
||||
pickr: Pickr | null
|
||||
create: (dom: HTMLElement, color: string, swatches: string[] | null, change: (color: string) => void, reset: () => void) => PickrTools
|
||||
destroy: () => void
|
||||
setColor: (color: string) => void
|
||||
}
|
||||
|
||||
export const pickrTools: PickrTools = {
|
||||
pickr: null,
|
||||
create(dom, color, swatches, change, reset) {
|
||||
const pickrTools: PickrTools = Object.create(this)
|
||||
|
||||
pickrTools.pickr = Pickr.create({
|
||||
el: dom,
|
||||
default: color,
|
||||
theme: 'classic', // or 'monolith', or 'nano'
|
||||
defaultRepresentation: 'RGBA',
|
||||
autoReposition: false,
|
||||
closeWithKey: '',
|
||||
appClass: 'color-picker',
|
||||
comparison: false,
|
||||
useAsButton: true,
|
||||
|
||||
swatches,
|
||||
|
||||
components: {
|
||||
|
||||
// Main components
|
||||
preview: true,
|
||||
opacity: true,
|
||||
hue: true,
|
||||
|
||||
// Input / output Options
|
||||
interaction: {
|
||||
hex: true,
|
||||
rgba: true,
|
||||
input: true,
|
||||
cancel: true,
|
||||
// save: true,
|
||||
},
|
||||
},
|
||||
|
||||
i18n: {
|
||||
// Strings visible in the UI
|
||||
'ui:dialog': ' ',
|
||||
'btn:toggle': window.i18n.t('theme_edit_modal__pick_color'),
|
||||
'btn:swatch': ' ',
|
||||
'btn:last-color': window.i18n.t('theme_edit_modal__pick_last_color'),
|
||||
'btn:save': window.i18n.t('theme_edit_modal__pick_save'),
|
||||
'btn:cancel': window.i18n.t('theme_edit_modal__pick_cancel'),
|
||||
|
||||
// Strings used for aria-labels
|
||||
'aria:btn:save': ' ',
|
||||
'aria:btn:cancel': ' ',
|
||||
'aria:input': ' ',
|
||||
'aria:palette': ' ',
|
||||
'aria:hue': '',
|
||||
'aria:opacity': ' ',
|
||||
},
|
||||
})
|
||||
|
||||
let swatchselectColor: any
|
||||
|
||||
const throttleChange = throttle((color: any, source: string) => {
|
||||
if (source == 'swatch' && swatchselectColor !== color) return
|
||||
change(color.toRGBA().toString())
|
||||
})
|
||||
pickrTools.pickr.on('swatchselect', (color: any) => {
|
||||
swatchselectColor = color
|
||||
}).on('change', throttleChange).on('cancel', () => {
|
||||
change(color)
|
||||
console.log('cancel')
|
||||
}).on('cancel', () => {
|
||||
reset()
|
||||
})
|
||||
|
||||
return pickrTools
|
||||
},
|
||||
destroy() {
|
||||
if (!this.pickr) return
|
||||
this.pickr.destroyAndRemove()
|
||||
this.pickr = null
|
||||
},
|
||||
setColor(color) {
|
||||
this.pickr?.setColor(color)
|
||||
},
|
||||
}
|
||||
@@ -2,11 +2,34 @@ import needle from 'needle'
|
||||
// import progress from 'request-progress'
|
||||
import { debugRequest } from './env'
|
||||
import { requestMsg } from './message'
|
||||
import { bHh } from './music/options'
|
||||
import { bHh } from './musicSdk/options'
|
||||
import { deflateRaw } from 'zlib'
|
||||
import { getRequestAgent } from './index'
|
||||
import { proxy } from '@renderer/store'
|
||||
import { httpOverHttp, httpsOverHttp } from 'tunnel'
|
||||
// import fs from 'fs'
|
||||
|
||||
const httpsRxp = /^https:/
|
||||
const getRequestAgent = url => {
|
||||
let options
|
||||
if (proxy.enable && proxy.host) {
|
||||
options = {
|
||||
proxy: {
|
||||
host: proxy.host,
|
||||
port: proxy.port,
|
||||
},
|
||||
}
|
||||
} else if (proxy.envProxy) {
|
||||
options = {
|
||||
proxy: {
|
||||
host: proxy.envProxy.host,
|
||||
port: proxy.envProxy.port,
|
||||
},
|
||||
}
|
||||
}
|
||||
return options ? (httpsRxp.test(url) ? httpsOverHttp : httpOverHttp)(options) : undefined
|
||||
}
|
||||
|
||||
|
||||
const request = (url, options, callback) => {
|
||||
let data
|
||||
if (options.body) {
|
||||
@@ -49,12 +72,20 @@ const defaultHeaders = {
|
||||
const buildHttpPromose = (url, options) => {
|
||||
let obj = {
|
||||
isCancelled: false,
|
||||
cancelHttp: () => {
|
||||
if (!obj.requestObj) return obj.isCancelled = true
|
||||
cancelHttp(obj.requestObj)
|
||||
obj.requestObj = null
|
||||
obj.promise = obj.cancelHttp = null
|
||||
obj.cancelFn(new Error(requestMsg.cancelRequest))
|
||||
obj.cancelFn = null
|
||||
},
|
||||
}
|
||||
obj.promise = new Promise((resolve, reject) => {
|
||||
obj.cancelFn = reject
|
||||
debugRequest && console.log(`\n---send request------${url}------------`)
|
||||
fetchData(url, options.method, options, (err, resp, body) => {
|
||||
// options.isShowProgress && window.api.hideProgress()
|
||||
// options.isShowProgress && window.api.hideProgress()
|
||||
debugRequest && console.log(`\n---response------${url}------------`)
|
||||
debugRequest && console.log(body)
|
||||
obj.requestObj = null
|
||||
@@ -66,14 +97,6 @@ const buildHttpPromose = (url, options) => {
|
||||
if (obj.isCancelled) obj.cancelHttp()
|
||||
})
|
||||
})
|
||||
obj.cancelHttp = () => {
|
||||
if (!obj.requestObj) return obj.isCancelled = true
|
||||
cancelHttp(obj.requestObj)
|
||||
obj.requestObj = null
|
||||
obj.promise = obj.cancelHttp = null
|
||||
obj.cancelFn(new Error(requestMsg.cancelRequest))
|
||||
obj.cancelFn = null
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 Shigma
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,10 @@
|
||||
# simplify-chinese
|
||||
|
||||
Convert chinese characters between simplified form and tranditional form / 汉字简繁体转换工具。
|
||||
|
||||
```js
|
||||
const { simplify, tranditionalize } = require('simplify-chinese')
|
||||
|
||||
console.log(simplify('窩窩頭')) // 窝窝头
|
||||
console.log(tranditionalize('窝窝头')) // 窩窩頭
|
||||
```
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user