完成同步功能
This commit is contained in:
@@ -2,7 +2,7 @@ const path = require('path')
|
||||
const os = require('os')
|
||||
|
||||
const defaultSetting = {
|
||||
version: '1.0.42',
|
||||
version: '1.0.43',
|
||||
player: {
|
||||
togglePlayMethod: 'listLoop',
|
||||
highQuality: false,
|
||||
@@ -85,6 +85,10 @@ const defaultSetting = {
|
||||
isToTray: false,
|
||||
themeId: 0,
|
||||
},
|
||||
sync: {
|
||||
enable: false,
|
||||
port: 23332,
|
||||
},
|
||||
windowSizeId: 2,
|
||||
themeId: 0,
|
||||
langId: null,
|
||||
|
||||
@@ -66,6 +66,13 @@ const names = {
|
||||
get_music_url: 'get_music_url',
|
||||
save_music_url: 'save_music_url',
|
||||
clear_music_url: 'clear_music_url',
|
||||
|
||||
sync_enable: 'sync_enable',
|
||||
sync_status: 'sync_status',
|
||||
sync_get_status: 'sync_get_status',
|
||||
sync_generate_code: 'sync_generate_code',
|
||||
sync_action_list: 'sync_action_list',
|
||||
sync_list: 'sync_list',
|
||||
},
|
||||
winLyric: {
|
||||
close: 'close',
|
||||
|
||||
@@ -10,6 +10,10 @@ class Common extends EventEmitter {
|
||||
configStatus(name) {
|
||||
this.emit(COMMON_EVENT_NAME.configStatus, name)
|
||||
}
|
||||
|
||||
saveMyList(data) {
|
||||
this.emit(COMMON_EVENT_NAME.saveMyList, data)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Common
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
exports.common = {
|
||||
initConfig: 'initConfig',
|
||||
configStatus: 'config',
|
||||
saveMyList: 'saveMyList',
|
||||
}
|
||||
|
||||
exports.mainWindow = {
|
||||
|
||||
@@ -7,6 +7,7 @@ const WinLyric = require('./WinLyric')
|
||||
const HotKey = require('./HotKey')
|
||||
|
||||
const { Event: UserApi } = require('../modules/userApi')
|
||||
const { Event: Sync } = require('../modules/sync')
|
||||
|
||||
if (!global.lx_event.common) global.lx_event.common = new Common()
|
||||
if (!global.lx_event.mainWindow) global.lx_event.mainWindow = new MainWindow()
|
||||
@@ -15,3 +16,4 @@ if (!global.lx_event.winLyric) global.lx_event.winLyric = new WinLyric()
|
||||
if (!global.lx_event.hotKey) global.lx_event.hotKey = new HotKey()
|
||||
|
||||
if (!global.lx_event.userApi) global.lx_event.userApi = new UserApi()
|
||||
if (!global.lx_event.sync) global.lx_event.sync = new Sync()
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
const { EventEmitter } = require('events')
|
||||
const SYNC_EVENT_NAME = require('./name')
|
||||
|
||||
class Sync extends EventEmitter {
|
||||
status(status) {
|
||||
this.emit(SYNC_EVENT_NAME.status, status)
|
||||
}
|
||||
|
||||
sync_list(data) {
|
||||
this.emit(SYNC_EVENT_NAME.sync_list, data)
|
||||
}
|
||||
|
||||
sync_handle_list(data) {
|
||||
this.emit(SYNC_EVENT_NAME.sync_handle_list, data)
|
||||
}
|
||||
|
||||
action_list(data) {
|
||||
this.emit(SYNC_EVENT_NAME.sync_action_list, data)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Sync
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
sync_action_list: 'sync_action_list',
|
||||
sync_list: 'sync_list',
|
||||
sync_handle_list: 'sync_handle_list',
|
||||
status: 'status',
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
const Event = require('./event/event')
|
||||
const eventNames = require('./event/name')
|
||||
const modules = require('./modules')
|
||||
const { startServer, stopServer, getStatus, generateCode } = require('./server/server')
|
||||
|
||||
|
||||
module.exports = {
|
||||
startServer,
|
||||
stopServer,
|
||||
getStatus,
|
||||
generateCode,
|
||||
Event,
|
||||
eventNames,
|
||||
modules,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
exports.list = require('./list')
|
||||
@@ -0,0 +1,41 @@
|
||||
const { encryptMsg, decryptMsg } = require('../server/utils')
|
||||
let io
|
||||
|
||||
const handleListAction = ({ action, data }) => {
|
||||
// console.log(action, data)
|
||||
global.lx_event.sync.action_list({ action, data })
|
||||
}
|
||||
|
||||
// const addMusic = (orderId, callback) => {
|
||||
// // ...
|
||||
// }
|
||||
|
||||
const broadcast = async(action, data, excludeIds = []) => {
|
||||
if (!io) return
|
||||
const sockets = await io.fetchSockets()
|
||||
for (const socket of sockets) {
|
||||
if (excludeIds.includes(socket.data.keyInfo.clientId)) continue
|
||||
socket.emit(action, encryptMsg(socket.data.keyInfo, data))
|
||||
}
|
||||
}
|
||||
|
||||
exports.sendListAction = (action, data) => {
|
||||
// io.sockets
|
||||
return broadcast('list:action', JSON.stringify({ action, data }))
|
||||
}
|
||||
|
||||
exports.registerListHandler = (_io, socket) => {
|
||||
io = _io
|
||||
socket.on('list:action', msg => {
|
||||
// console.log(msg)
|
||||
msg = decryptMsg(socket.data.keyInfo, msg)
|
||||
if (!msg) return
|
||||
handleListAction(JSON.parse(msg))
|
||||
broadcast('list:action', msg, [socket.data.keyInfo.clientId])
|
||||
// socket.broadcast.emit('list:action', { action: 'list_remove', data: { id: 'default', index: 0 } })
|
||||
})
|
||||
// socket.on('list:add', addMusic)
|
||||
}
|
||||
exports.unregisterListHandler = () => {
|
||||
io = null
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
const { aesEncrypt, aesDecrypt, createClientKeyInfo, getClientKeyInfo, setClientKeyInfo } = require('./utils')
|
||||
|
||||
const authMsg = 'lx-music auth::'
|
||||
const helloMsg = 'Hello~::^-^::'
|
||||
|
||||
exports.authCode = async(req, res, authCode) => {
|
||||
let code = 401
|
||||
let msg = 'Forbidden'
|
||||
// console.log(req.headers)
|
||||
if (req.headers.m) {
|
||||
label:
|
||||
if (req.headers.i) {
|
||||
const keyInfo = getClientKeyInfo(req.headers.i)
|
||||
if (!keyInfo) break label
|
||||
let text
|
||||
try {
|
||||
text = aesDecrypt(req.headers.m, keyInfo.key, keyInfo.iv)
|
||||
} catch (err) {
|
||||
break label
|
||||
}
|
||||
console.log(text)
|
||||
if (text.startsWith(authMsg)) {
|
||||
code = 200
|
||||
const deviceName = text.replace(authMsg, '') || 'Unknown'
|
||||
if (deviceName != keyInfo.deviceName) {
|
||||
keyInfo.deviceName = deviceName
|
||||
setClientKeyInfo(keyInfo)
|
||||
}
|
||||
msg = aesEncrypt(helloMsg, keyInfo.key, keyInfo.iv)
|
||||
}
|
||||
} else {
|
||||
let key = ''.padStart(16, Buffer.from(authCode).toString('hex'))
|
||||
const iv = Buffer.from(key.split('').reverse().join('')).toString('base64')
|
||||
key = Buffer.from(key).toString('base64')
|
||||
// console.log(authCode, key, iv)
|
||||
let text
|
||||
try {
|
||||
text = aesDecrypt(req.headers.m, key, iv)
|
||||
} catch (err) {
|
||||
break label
|
||||
}
|
||||
console.log(text)
|
||||
if (text.startsWith(authMsg)) {
|
||||
code = 200
|
||||
const deviceName = text.replace(authMsg, '') || 'Unknown'
|
||||
msg = aesEncrypt(JSON.stringify(createClientKeyInfo(deviceName)), key, iv)
|
||||
}
|
||||
}
|
||||
}
|
||||
res.writeHead(code)
|
||||
res.end(msg)
|
||||
}
|
||||
|
||||
exports.authConnect = async req => {
|
||||
const { i, t } = req._query
|
||||
label:
|
||||
if (i && t) {
|
||||
const keyInfo = getClientKeyInfo(i)
|
||||
if (!keyInfo) break label
|
||||
let text
|
||||
try {
|
||||
text = aesDecrypt(t, keyInfo.key, keyInfo.iv)
|
||||
} catch (err) {
|
||||
break label
|
||||
}
|
||||
if (text == 'lx-music connect') return
|
||||
}
|
||||
throw new Error('failed')
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const { startServer, stopServer, getStatus } = require('./server')
|
||||
|
||||
module.exports = {
|
||||
startServer,
|
||||
stopServer,
|
||||
getStatus,
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
const http = require('http')
|
||||
const sio = require('socket.io')
|
||||
const { createHttpTerminator } = require('http-terminator')
|
||||
const modules = require('../modules')
|
||||
const { authCode, authConnect } = require('./auth')
|
||||
const { getAddress, getServerId, generateCode, getClientKeyInfo } = require('./utils')
|
||||
const syncList = require('./syncList')
|
||||
|
||||
|
||||
let status = {
|
||||
status: false,
|
||||
message: '',
|
||||
address: [],
|
||||
code: '',
|
||||
devices: [],
|
||||
}
|
||||
|
||||
const handleConnection = (io, socket) => {
|
||||
console.log('connection')
|
||||
// console.log(socket.handshake.query)
|
||||
for (const module of Object.values(modules)) {
|
||||
module.registerListHandler(io, socket)
|
||||
}
|
||||
}
|
||||
|
||||
const authConnection = (req, callback) => {
|
||||
// console.log(req.headers)
|
||||
// // console.log(req.auth)
|
||||
// console.log(req._query.authCode)
|
||||
authConnect(req).then(() => {
|
||||
callback(null, true)
|
||||
}).catch(err => {
|
||||
callback(err, false)
|
||||
})
|
||||
}
|
||||
|
||||
let httpTerminator = null
|
||||
let io = null
|
||||
|
||||
const handleStartServer = (port = 9527) => new Promise((resolve, reject) => {
|
||||
const httpServer = http.createServer((req, res) => {
|
||||
// console.log(req.url)
|
||||
let code
|
||||
let msg
|
||||
switch (req.url) {
|
||||
case '/hello':
|
||||
code = 200
|
||||
msg = 'Hello~::^-^::'
|
||||
break
|
||||
case '/id':
|
||||
code = 200
|
||||
msg = 'OjppZDo6' + getServerId()
|
||||
break
|
||||
case '/ah':
|
||||
authCode(req, res, status.code)
|
||||
break
|
||||
default:
|
||||
code = 401
|
||||
msg = 'Forbidden'
|
||||
break
|
||||
}
|
||||
if (!code) return
|
||||
res.writeHead(code)
|
||||
res.end(msg)
|
||||
})
|
||||
httpTerminator = createHttpTerminator({
|
||||
server: httpServer,
|
||||
})
|
||||
io = sio(httpServer, {
|
||||
path: '/sync',
|
||||
serveClient: false,
|
||||
connectTimeout: 10000,
|
||||
pingTimeout: 30000,
|
||||
maxHttpBufferSize: 3e6,
|
||||
allowRequest: authConnection,
|
||||
transports: ['websocket'],
|
||||
})
|
||||
|
||||
io.on('connection', async socket => {
|
||||
socket.on('disconnect', reason => {
|
||||
console.log('disconnect', reason)
|
||||
status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo.clientId), 1)
|
||||
global.lx_event.sync.status(status)
|
||||
})
|
||||
const keyInfo = getClientKeyInfo(socket.handshake.query.i)
|
||||
// socket.lx_keyInfo = keyInfo
|
||||
socket.data.keyInfo = keyInfo
|
||||
try {
|
||||
await syncList(io, socket)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return
|
||||
}
|
||||
status.devices.push(keyInfo)
|
||||
handleConnection(io, socket, keyInfo)
|
||||
global.lx_event.sync.status(status)
|
||||
})
|
||||
|
||||
httpServer.on('error', error => {
|
||||
console.log(error)
|
||||
reject(error)
|
||||
})
|
||||
|
||||
httpServer.on('listening', () => {
|
||||
const addr = httpServer.address()
|
||||
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
|
||||
console.info(`Listening on ${bind}`)
|
||||
resolve()
|
||||
})
|
||||
|
||||
httpServer.listen(port)
|
||||
})
|
||||
|
||||
const handleStopServer = async() => {
|
||||
if (!httpTerminator) return
|
||||
await io.close()
|
||||
await httpTerminator.terminate().catch(() => {})
|
||||
io = null
|
||||
httpTerminator = null
|
||||
}
|
||||
|
||||
exports.stopServer = async() => {
|
||||
if (!status.status) return
|
||||
console.log('stoping sync server...')
|
||||
return handleStopServer().then(() => {
|
||||
console.log('sync server stoped')
|
||||
status.status = false
|
||||
status.message = ''
|
||||
status.address = []
|
||||
status.code = ''
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
status.message = err.message
|
||||
}).finally(() => {
|
||||
global.lx_event.sync.status(status)
|
||||
})
|
||||
}
|
||||
exports.startServer = async port => {
|
||||
if (status.status) await handleStopServer()
|
||||
|
||||
console.log('starting sync server...')
|
||||
return handleStartServer(port).then(() => {
|
||||
console.log('sync server started')
|
||||
status.status = true
|
||||
status.message = ''
|
||||
status.address = getAddress()
|
||||
status.code = generateCode()
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
status.status = false
|
||||
status.message = err.message
|
||||
status.address = []
|
||||
status.code = ''
|
||||
}).finally(() => {
|
||||
global.lx_event.sync.status(status)
|
||||
})
|
||||
}
|
||||
|
||||
exports.getStatus = () => status
|
||||
|
||||
exports.generateCode = async() => {
|
||||
status.code = generateCode()
|
||||
global.lx_event.sync.status(status)
|
||||
return status.code
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const fsPromises = fs.promises
|
||||
const { app } = require('electron')
|
||||
const { encryptMsg, decryptMsg } = require('./utils')
|
||||
const SYNC_EVENT_NAMES = require('../event/name')
|
||||
const { common: COMMON_EVENT_NAME } = require('@main/events/_name')
|
||||
const { throttle } = require('@common/utils')
|
||||
|
||||
let io
|
||||
// const checkFile = path => {
|
||||
// fsPromises.access(path, fs.constants.R_OK | fs.constants.W_OK)
|
||||
// .then(() => console.log('can access'))
|
||||
// .catch(() => console.error('cannot access'))
|
||||
// }
|
||||
|
||||
const getRemoteListData = socket => new Promise((resolve, reject) => {
|
||||
console.log('getRemoteListData')
|
||||
const handleError = reason => {
|
||||
reject(new Error(reason))
|
||||
}
|
||||
const handleSuccess = enData => {
|
||||
socket.removeListener('disconnect', handleError)
|
||||
socket.removeListener('list:sync', handleSuccess)
|
||||
console.log('getRemoteListData', 'handleSuccess')
|
||||
const data = JSON.parse(decryptMsg(socket.data.keyInfo, enData))
|
||||
if (!data) return reject(new Error('Get remote list data failed'))
|
||||
if (data.action != 'getData') return
|
||||
resolve(data.data)
|
||||
}
|
||||
|
||||
socket.on('disconnect', handleError)
|
||||
socket.on('list:sync', handleSuccess)
|
||||
socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({ action: 'getData', data: 'all' })))
|
||||
})
|
||||
|
||||
const getLocalListData = () => new Promise((resolve, reject) => {
|
||||
const handleSuccess = ({ action, data }) => {
|
||||
if (action !== 'getData') return
|
||||
global.lx_event.sync.off(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
|
||||
resolve(data)
|
||||
}
|
||||
global.lx_event.sync.on(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
|
||||
global.lx_event.sync.sync_list({
|
||||
action: 'getData',
|
||||
})
|
||||
})
|
||||
const getSyncMode = keyInfo => new Promise((resolve, reject) => {
|
||||
const handleSuccess = ({ action, data }) => {
|
||||
if (action !== 'selectMode') return
|
||||
global.lx_event.sync.off(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
|
||||
resolve(data)
|
||||
}
|
||||
global.lx_event.sync.on(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
|
||||
global.lx_event.sync.sync_list({
|
||||
action: 'selectMode',
|
||||
data: keyInfo,
|
||||
})
|
||||
})
|
||||
|
||||
const finishedSync = socket => {
|
||||
return socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({
|
||||
action: 'finished',
|
||||
})))
|
||||
}
|
||||
|
||||
const setLocalList = listData => {
|
||||
global.lx_event.sync.sync_list({
|
||||
action: 'setData',
|
||||
data: listData,
|
||||
})
|
||||
}
|
||||
const setRemotelList = async(socket, listData) => {
|
||||
if (!io) return
|
||||
const sockets = await io.fetchSockets()
|
||||
for (const socket of sockets) {
|
||||
// if (excludeIds.includes(socket.data.keyInfo.clientId)) continue
|
||||
socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({ action: 'setData', data: listData })))
|
||||
}
|
||||
}
|
||||
|
||||
let writeFilePromises = {}
|
||||
const updateSnapshot = (path, data) => {
|
||||
console.log('updateSnapshot', path)
|
||||
let writeFilePromise = writeFilePromises[path] || Promise.resolve()
|
||||
return writeFilePromise.then(() => {
|
||||
writeFilePromise = writeFilePromises[path] = fsPromises.writeFile(path, data)
|
||||
return writeFilePromise
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const createListDataObj = listData => {
|
||||
const listDataObj = {}
|
||||
for (const list of listData.userList) listDataObj[list.id] = list
|
||||
return listDataObj
|
||||
}
|
||||
|
||||
const handleMergeList = (sourceList, targetList, addMusicLocationType) => {
|
||||
let newList
|
||||
switch (addMusicLocationType) {
|
||||
case 'top':
|
||||
newList = [...targetList.list, ...sourceList.list]
|
||||
break
|
||||
case 'bottom':
|
||||
default:
|
||||
newList = [...sourceList.list, ...targetList.list]
|
||||
break
|
||||
}
|
||||
const map = {}
|
||||
const ids = []
|
||||
switch (addMusicLocationType) {
|
||||
case 'top':
|
||||
newList = [...targetList.list, ...sourceList.list]
|
||||
for (let i = newList.length - 1; i > -1; i--) {
|
||||
const item = newList[i]
|
||||
if (map[item.songmid]) continue
|
||||
ids.unshift(item.songmid)
|
||||
map[item.songmid] = item
|
||||
}
|
||||
break
|
||||
case 'bottom':
|
||||
default:
|
||||
newList = [...sourceList.list, ...targetList.list]
|
||||
for (const item of newList) {
|
||||
if (map[item.songmid]) continue
|
||||
ids.push(item.songmid)
|
||||
map[item.songmid] = item
|
||||
}
|
||||
break
|
||||
}
|
||||
return {
|
||||
...sourceList,
|
||||
list: ids.map(id => map[id]),
|
||||
}
|
||||
}
|
||||
const mergeList = (sourceListData, targetListData) => {
|
||||
const addMusicLocationType = global.appSetting.list.addMusicLocationType
|
||||
const newListData = {}
|
||||
newListData.defaultList = handleMergeList(sourceListData.defaultList, targetListData.defaultList, addMusicLocationType)
|
||||
newListData.loveList = handleMergeList(sourceListData.loveList, targetListData.loveList, addMusicLocationType)
|
||||
|
||||
const listDataObj = createListDataObj(sourceListData)
|
||||
newListData.userList = [...sourceListData.userList]
|
||||
|
||||
for (const list of targetListData.userList) {
|
||||
const targetList = listDataObj[list.id]
|
||||
if (targetList) {
|
||||
targetList.list = handleMergeList(targetList, list, addMusicLocationType).list
|
||||
} else {
|
||||
newListData.userList.push(list)
|
||||
}
|
||||
}
|
||||
|
||||
return newListData
|
||||
}
|
||||
const overwriteList = (sourceListData, targetListData) => {
|
||||
const newListData = {}
|
||||
newListData.defaultList = sourceListData.defaultList
|
||||
newListData.loveList = sourceListData.loveList
|
||||
|
||||
const listDataObj = createListDataObj(sourceListData)
|
||||
newListData.userList = [...sourceListData.userList]
|
||||
|
||||
for (const list of targetListData.userList) {
|
||||
const targetList = listDataObj[list.id]
|
||||
if (targetList) continue
|
||||
newListData.userList.push(list)
|
||||
}
|
||||
|
||||
return newListData
|
||||
}
|
||||
|
||||
const handleMergeListData = async socket => {
|
||||
let isSelectingMode = false
|
||||
const handleDisconnect = () => {
|
||||
if (!isSelectingMode) return
|
||||
global.lx_event.sync.sync_list({
|
||||
action: 'closeSelectMode',
|
||||
})
|
||||
}
|
||||
socket.on('disconnect', handleDisconnect)
|
||||
isSelectingMode = true
|
||||
const mode = await getSyncMode(socket.data.keyInfo)
|
||||
isSelectingMode = false
|
||||
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
|
||||
console.log('handleMergeListData', 'remoteListData, localListData')
|
||||
let listData
|
||||
switch (mode) {
|
||||
case 'merge_local_remote':
|
||||
listData = mergeList(localListData, remoteListData)
|
||||
break
|
||||
case 'merge_remote_local':
|
||||
listData = mergeList(remoteListData, localListData)
|
||||
break
|
||||
case 'overwrite_local_remote':
|
||||
listData = overwriteList(localListData, remoteListData)
|
||||
break
|
||||
case 'overwrite_remote_local':
|
||||
listData = overwriteList(remoteListData, localListData)
|
||||
break
|
||||
case 'overwrite_local_remote_full':
|
||||
listData = localListData
|
||||
break
|
||||
case 'overwrite_remote_local_full':
|
||||
listData = remoteListData
|
||||
break
|
||||
case 'none': return
|
||||
case 'cancel':
|
||||
socket.disconnect(true)
|
||||
throw new Error('cancel')
|
||||
}
|
||||
return listData
|
||||
}
|
||||
|
||||
const handleSyncList = async socket => {
|
||||
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
|
||||
console.log('handleSyncList', 'remoteListData, localListData')
|
||||
const listData = {}
|
||||
if (localListData.defaultList.list.length || localListData.loveList.list.length || localListData.userList.length) {
|
||||
if (remoteListData.defaultList.list.length || remoteListData.loveList.list.length || remoteListData.userList.length) {
|
||||
const mergedList = await handleMergeListData(socket)
|
||||
console.log('handleMergeListData', 'mergedList')
|
||||
console.log(mergedList)
|
||||
if (!mergedList) return
|
||||
listData.defaultList = mergedList.defaultList
|
||||
listData.loveList = mergedList.loveList
|
||||
listData.userList = mergedList.userList
|
||||
setLocalList(mergedList)
|
||||
setRemotelList(socket, mergedList)
|
||||
} else {
|
||||
setRemotelList(socket, localListData)
|
||||
listData.defaultList = localListData.defaultList
|
||||
listData.loveList = localListData.loveList
|
||||
listData.userList = localListData.userList
|
||||
}
|
||||
} else {
|
||||
if (remoteListData.defaultList.list.length || remoteListData.loveList.list.length || remoteListData.userList.length) {
|
||||
setLocalList(remoteListData)
|
||||
listData.defaultList = remoteListData.defaultList
|
||||
listData.loveList = remoteListData.loveList
|
||||
listData.userList = remoteListData.userList
|
||||
} else {
|
||||
listData.defaultList = localListData.defaultList
|
||||
listData.loveList = localListData.loveList
|
||||
listData.userList = localListData.userList
|
||||
}
|
||||
}
|
||||
return updateSnapshot(socket.data.snapshotFilePath, JSON.stringify({
|
||||
defaultList: listData.defaultList,
|
||||
loveList: listData.loveList,
|
||||
userList: listData.userList,
|
||||
})).then(() => {
|
||||
socket.data.isCreatedSnapshot = true
|
||||
return listData
|
||||
})
|
||||
}
|
||||
|
||||
const mergeListDataFromSnapshot = (sourceList, targetList, snapshotList, addMusicLocationType) => {
|
||||
const removedListIds = new Set()
|
||||
const sourceListItemIds = new Set()
|
||||
const targetListItemIds = new Set()
|
||||
for (const m of sourceList.list) sourceListItemIds.add(m.songmid)
|
||||
for (const m of targetList.list) targetListItemIds.add(m.songmid)
|
||||
for (const m of snapshotList.list) {
|
||||
if (!sourceListItemIds.has(m.songmid)) removedListIds.add(m.songmid)
|
||||
}
|
||||
for (const m of snapshotList.list) {
|
||||
if (!targetListItemIds.has(m.songmid)) removedListIds.add(m.songmid)
|
||||
}
|
||||
|
||||
let newList
|
||||
const map = {}
|
||||
const ids = []
|
||||
switch (addMusicLocationType) {
|
||||
case 'top':
|
||||
newList = [...targetList.list, ...sourceList.list]
|
||||
for (let i = newList.length - 1; i > -1; i--) {
|
||||
const item = newList[i]
|
||||
if (map[item.songmid] || removedListIds.has(item.songmid)) continue
|
||||
ids.unshift(item.songmid)
|
||||
map[item.songmid] = item
|
||||
}
|
||||
break
|
||||
case 'bottom':
|
||||
default:
|
||||
newList = [...sourceList.list, ...targetList.list]
|
||||
for (const item of newList) {
|
||||
if (map[item.songmid] || removedListIds.has(item.songmid)) continue
|
||||
ids.push(item.songmid)
|
||||
map[item.songmid] = item
|
||||
}
|
||||
break
|
||||
}
|
||||
return {
|
||||
...sourceList,
|
||||
list: ids.map(id => map[id]),
|
||||
}
|
||||
}
|
||||
const handleMergeListDataFromSnapshot = async(socket, snapshot) => {
|
||||
const addMusicLocationType = global.appSetting.list.addMusicLocationType
|
||||
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
|
||||
console.log('handleMergeListDataFromSnapshot', 'remoteListData, localListData')
|
||||
const newListData = {}
|
||||
newListData.defaultList = mergeListDataFromSnapshot(localListData.defaultList, remoteListData.defaultList, snapshot.defaultList, addMusicLocationType)
|
||||
newListData.loveList = mergeListDataFromSnapshot(localListData.loveList, remoteListData.loveList, snapshot.loveList, addMusicLocationType)
|
||||
const localUserListData = createListDataObj(localListData)
|
||||
const remoteUserListData = createListDataObj(remoteListData)
|
||||
const snapshotUserListData = createListDataObj(snapshot)
|
||||
const removedListIds = new Set()
|
||||
const localUserListIds = new Set()
|
||||
const remoteUserListIds = new Set()
|
||||
for (const l of localListData.userList) localUserListIds.add(l.id)
|
||||
for (const l of remoteListData.userList) remoteUserListIds.add(l.id)
|
||||
|
||||
for (const l of snapshot.userList) {
|
||||
if (!localUserListIds.has(l.id)) removedListIds.add(l.id)
|
||||
}
|
||||
for (const l of snapshot.userList) {
|
||||
if (!remoteUserListIds.has(l.id)) removedListIds.add(l.id)
|
||||
}
|
||||
|
||||
let newUserList = []
|
||||
for (const list of localListData.userList) {
|
||||
if (removedListIds.has(list.id)) continue
|
||||
const remoteList = remoteUserListData[list.id]
|
||||
let newList
|
||||
if (remoteList) {
|
||||
newList = mergeListDataFromSnapshot(list, remoteList, snapshotUserListData[list.id], addMusicLocationType)
|
||||
} else {
|
||||
newList = { ...list }
|
||||
}
|
||||
newUserList.push(newList)
|
||||
}
|
||||
for (const list of remoteListData.userList) {
|
||||
if (removedListIds.has(list.id) || localUserListData[list.id]) continue
|
||||
newUserList.push({ ...list })
|
||||
}
|
||||
newListData.userList = newUserList
|
||||
setLocalList(newListData)
|
||||
setRemotelList(socket, newListData)
|
||||
return updateSnapshot(socket.data.snapshotFilePath, JSON.stringify({
|
||||
defaultList: newListData.defaultList,
|
||||
loveList: newListData.loveList,
|
||||
userList: newListData.userList,
|
||||
})).then(() => {
|
||||
socket.data.isCreatedSnapshot = true
|
||||
return newListData
|
||||
})
|
||||
}
|
||||
|
||||
const registerUpdateSnapshotTask = (socket, snapshot) => {
|
||||
if (!socket.data.isCreatedSnapshot) return
|
||||
const handleUpdateSnapshot = throttle(({ defaultList, loveList, userList }) => {
|
||||
if (defaultList != null) snapshot.defaultList = defaultList
|
||||
if (loveList != null) snapshot.loveList = loveList
|
||||
if (userList != null) snapshot.userList = userList
|
||||
updateSnapshot(socket.data.snapshotFilePath, JSON.stringify(snapshot))
|
||||
}, 10000)
|
||||
global.lx_event.common.on(COMMON_EVENT_NAME.saveMyList, handleUpdateSnapshot)
|
||||
socket.on('disconnect', () => {
|
||||
global.lx_event.common.off(COMMON_EVENT_NAME.saveMyList, handleUpdateSnapshot)
|
||||
})
|
||||
}
|
||||
|
||||
const syncList = async socket => {
|
||||
socket.data.snapshotFilePath = path.join(app.getPath('userData'), `snapshot-${Buffer.from(socket.data.keyInfo.clientId).toString('hex').substring(0, 10)}.json`)
|
||||
let fileData
|
||||
let isSyncRequired = false
|
||||
try {
|
||||
fileData = await fsPromises.readFile(socket.data.snapshotFilePath)
|
||||
fileData = JSON.parse(fileData)
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') throw err
|
||||
isSyncRequired = true
|
||||
}
|
||||
console.log('isSyncRequired', isSyncRequired)
|
||||
if (isSyncRequired) return handleSyncList(socket)
|
||||
return handleMergeListDataFromSnapshot(socket, fileData)
|
||||
}
|
||||
|
||||
module.exports = (_io, socket) => {
|
||||
io = _io
|
||||
return syncList(socket).then(newListData => {
|
||||
registerUpdateSnapshotTask(socket, { ...newListData })
|
||||
return finishedSync(socket)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
const { networkInterfaces } = require('os')
|
||||
const { randomBytes, createCipheriv, createDecipheriv } = require('crypto')
|
||||
const getStore = require('@common/store')
|
||||
const STORE_NAME = 'sync'
|
||||
|
||||
exports.getAddress = () => {
|
||||
const nets = networkInterfaces()
|
||||
const results = []
|
||||
// console.log(nets)
|
||||
|
||||
for (const name of Object.keys(nets)) {
|
||||
for (const net of nets[name]) {
|
||||
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
|
||||
if (net.family === 'IPv4' && !net.internal) {
|
||||
results.push(net.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
let serverId
|
||||
exports.getServerId = () => {
|
||||
if (serverId) return serverId
|
||||
const store = getStore(STORE_NAME)
|
||||
serverId = store.get('serverId')
|
||||
if (!serverId) {
|
||||
serverId = randomBytes(4 * 4).toString('base64')
|
||||
store.set('serverId', serverId)
|
||||
}
|
||||
return serverId
|
||||
}
|
||||
|
||||
let keyInfos
|
||||
exports.createClientKeyInfo = deviceName => {
|
||||
const keyInfo = {
|
||||
clientId: randomBytes(4 * 4).toString('base64'),
|
||||
key: randomBytes(16).toString('base64'),
|
||||
iv: randomBytes(16).toString('base64'),
|
||||
deviceName,
|
||||
}
|
||||
const store = getStore(STORE_NAME)
|
||||
if (!keyInfos) keyInfos = store.get('keys') || {}
|
||||
if (Object.keys(keyInfos).length > 101) throw new Error('max keys')
|
||||
|
||||
keyInfos[keyInfo.clientId] = keyInfo
|
||||
store.set('keys', keyInfos)
|
||||
return keyInfo
|
||||
}
|
||||
exports.setClientKeyInfo = keyInfo => {
|
||||
keyInfos[keyInfo.clientId] = keyInfo
|
||||
const store = getStore(STORE_NAME)
|
||||
store.set('keys', keyInfos)
|
||||
}
|
||||
exports.getClientKeyInfo = clientId => {
|
||||
if (!keyInfos) {
|
||||
const store = getStore(STORE_NAME)
|
||||
keyInfos = store.get('keys') || {}
|
||||
}
|
||||
return keyInfos[clientId] || null
|
||||
}
|
||||
|
||||
exports.generateCode = () => {
|
||||
return Math.random().toString().substring(2, 8)
|
||||
}
|
||||
|
||||
exports.aesEncrypt = (buffer, key, iv) => {
|
||||
const cipher = createCipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'))
|
||||
return Buffer.concat([cipher.update(buffer), cipher.final()]).toString('base64')
|
||||
}
|
||||
|
||||
exports.aesDecrypt = (text, key, iv) => {
|
||||
const decipher = createDecipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'))
|
||||
return Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString()
|
||||
}
|
||||
|
||||
exports.encryptMsg = (keyInfo, msg) => {
|
||||
return msg
|
||||
// if (!keyInfo) return ''
|
||||
// return exports.aesEncrypt(msg, keyInfo.key, keyInfo.iv)
|
||||
}
|
||||
|
||||
exports.decryptMsg = (keyInfo, enMsg) => {
|
||||
return enMsg
|
||||
// if (!keyInfo) return ''
|
||||
// let msg = ''
|
||||
// try {
|
||||
// msg = exports.aesDecrypt(enMsg, keyInfo.key, keyInfo.iv)
|
||||
// } catch (err) {
|
||||
// console.log(err)
|
||||
// }
|
||||
// return msg
|
||||
}
|
||||
@@ -23,3 +23,4 @@ require('./musicUrl')
|
||||
require('./kw_decodeLyric')
|
||||
|
||||
require('./userApi')
|
||||
require('./sync')
|
||||
|
||||
@@ -24,6 +24,7 @@ mainOn(ipcMainWindowNames.save_playlist, (event, { type, data }) => {
|
||||
switch (type) {
|
||||
case 'myList':
|
||||
handleSaveList(data)
|
||||
global.lx_event.common.saveMyList(data)
|
||||
break
|
||||
case 'downloadList':
|
||||
getStore('downloadList').set('list', data)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
const { mainSend, NAMES: { mainWindow: ipcMainWindowNames }, mainOn, mainHandle } = require('@common/ipc')
|
||||
const { eventNames, modules, startServer, stopServer, getStatus, generateCode } = require('../modules/sync')
|
||||
|
||||
|
||||
mainOn(ipcMainWindowNames.sync_action_list, (event, { action, data }) => {
|
||||
modules.list.sendListAction(action, data)
|
||||
})
|
||||
|
||||
mainHandle(ipcMainWindowNames.sync_enable, (event, { enable, port }) => {
|
||||
return enable ? startServer(port) : stopServer()
|
||||
})
|
||||
|
||||
mainHandle(ipcMainWindowNames.sync_get_status, () => {
|
||||
return getStatus()
|
||||
})
|
||||
|
||||
mainHandle(ipcMainWindowNames.sync_generate_code, () => {
|
||||
return generateCode()
|
||||
})
|
||||
|
||||
mainOn(ipcMainWindowNames.sync_list, (event, { action, data }) => {
|
||||
global.lx_event.sync.sync_handle_list({ action, data })
|
||||
})
|
||||
|
||||
global.lx_event.sync.on(eventNames.sync_action_list, ({ action, data }) => {
|
||||
mainSend(global.modules.mainWindow, ipcMainWindowNames.sync_action_list, { action, data })
|
||||
})
|
||||
global.lx_event.sync.on(eventNames.status, status => {
|
||||
mainSend(global.modules.mainWindow, ipcMainWindowNames.sync_status, status)
|
||||
})
|
||||
global.lx_event.sync.on(eventNames.sync_list, ({ action, data }) => {
|
||||
mainSend(global.modules.mainWindow, ipcMainWindowNames.sync_list, { action, data })
|
||||
})
|
||||
+115
-3
@@ -8,6 +8,7 @@
|
||||
core-icons
|
||||
material-version-modal(v-show="version.showModal")
|
||||
material-pact-modal(v-show="!setting.isAgreePact || globalObj.isShowPact")
|
||||
material-sync-mode-modal(v-show="globalObj.sync.isShowSyncMode")
|
||||
#container(v-else :class="theme")
|
||||
core-aside#left
|
||||
#right
|
||||
@@ -17,6 +18,7 @@
|
||||
core-icons
|
||||
material-version-modal(v-show="version.showModal")
|
||||
material-pact-modal(v-show="!setting.isAgreePact || globalObj.isShowPact")
|
||||
material-sync-mode-modal(v-show="globalObj.sync.isShowSyncMode")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -25,7 +27,7 @@ import { rendererOn, rendererSend, rendererInvoke, NAMES } from '../common/ipc'
|
||||
import { isLinux } from '../common/utils'
|
||||
import music from './utils/music'
|
||||
import { throttle, openUrl, compareVer, getPlayList, parseUrlParams, saveSetting } from './utils'
|
||||
import { base as eventBaseName } from './event/names'
|
||||
import { base as eventBaseName, sync as eventSyncName } from './event/names'
|
||||
import apiSourceInfo from './utils/music/api-source-info'
|
||||
|
||||
window.ELECTRON_DISABLE_SECURITY_WARNINGS = process.env.ELECTRON_DISABLE_SECURITY_WARNINGS
|
||||
@@ -61,6 +63,11 @@ export default {
|
||||
message: 'initing',
|
||||
apis: {},
|
||||
},
|
||||
sync: {
|
||||
enable: false,
|
||||
isShowSyncMode: false,
|
||||
deviceName: '',
|
||||
},
|
||||
},
|
||||
updateTimeout: null,
|
||||
envParams: {
|
||||
@@ -103,6 +110,14 @@ export default {
|
||||
document.body.classList.add(this.isDT ? 'disableTransparent' : 'transparent')
|
||||
window.eventHub.$emit(eventBaseName.bindKey)
|
||||
this.init()
|
||||
window.eventHub.$on(eventSyncName.handle_action_list, this.handleSyncAction)
|
||||
window.eventHub.$on(eventSyncName.handle_sync_list, this.handleSyncList)
|
||||
if (this.setting.sync.enable && this.setting.sync.port) {
|
||||
rendererInvoke(NAMES.mainWindow.sync_enable, {
|
||||
enable: this.setting.sync.enable,
|
||||
port: this.setting.sync.port,
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
setting: {
|
||||
@@ -193,7 +208,25 @@ export default {
|
||||
methods: {
|
||||
...mapActions(['getVersionInfo']),
|
||||
...mapMutations(['setNewVersion', 'setVersionModalVisible', 'setDownloadProgress', 'setSetting', 'setDesktopLyricConfig']),
|
||||
...mapMutations('list', ['initList']),
|
||||
...mapMutations('list', {
|
||||
list_initList: 'initList',
|
||||
list_setList: 'setList',
|
||||
list_listAdd: 'listAdd',
|
||||
list_listMove: 'listMove',
|
||||
list_listAddMultiple: 'listAddMultiple',
|
||||
list_listMoveMultiple: 'listMoveMultiple',
|
||||
list_listRemove: 'listRemove',
|
||||
list_listRemoveMultiple: 'listRemoveMultiple',
|
||||
list_listClear: 'listClear',
|
||||
list_updateMusicInfo: 'updateMusicInfo',
|
||||
list_createUserList: 'createUserList',
|
||||
list_removeUserList: 'removeUserList',
|
||||
list_setUserListName: 'setUserListName',
|
||||
list_moveupUserList: 'moveupUserList',
|
||||
list_movedownUserList: 'movedownUserList',
|
||||
list_setMusicPosition: 'setMusicPosition',
|
||||
list_setSyncListData: 'setSyncListData',
|
||||
}),
|
||||
...mapMutations('download', ['updateDownloadList']),
|
||||
...mapMutations('search', {
|
||||
setSearchHistoryList: 'setHistory',
|
||||
@@ -272,6 +305,7 @@ export default {
|
||||
this.listenEvent()
|
||||
asyncTask.push(this.initData())
|
||||
asyncTask.push(this.initUserApi())
|
||||
this.globalObj.sync.enable = this.setting.sync.enable
|
||||
this.globalObj.apiSource = this.setting.apiSource
|
||||
if (/^user_api/.test(this.setting.apiSource)) {
|
||||
rendererInvoke(NAMES.mainWindow.set_user_api, this.setting.apiSource)
|
||||
@@ -328,7 +362,7 @@ export default {
|
||||
|
||||
if (!defaultList.list) defaultList.list = []
|
||||
if (!loveList.list) loveList.list = []
|
||||
this.initList({ defaultList, loveList, userList })
|
||||
this.list_initList({ defaultList, loveList, userList })
|
||||
this.initDownloadList(downloadList) // 初始化下载列表
|
||||
this.initPlayInfo()
|
||||
})
|
||||
@@ -590,6 +624,84 @@ export default {
|
||||
event.target.value = ''
|
||||
event.target.blur()
|
||||
},
|
||||
handleSyncAction({ action, data }) {
|
||||
if (typeof data == 'object') data.isSync = true
|
||||
// console.log(action, data)
|
||||
|
||||
switch (action) {
|
||||
case 'set_list':
|
||||
this.list_setList(data)
|
||||
break
|
||||
case 'list_add':
|
||||
this.list_listAdd(data)
|
||||
break
|
||||
case 'list_move':
|
||||
this.list_listMove(data)
|
||||
break
|
||||
case 'list_add_multiple':
|
||||
this.list_listAddMultiple(data)
|
||||
break
|
||||
case 'list_move_multiple':
|
||||
this.list_listMoveMultiple(data)
|
||||
break
|
||||
case 'list_remove':
|
||||
this.list_listRemove(data)
|
||||
break
|
||||
case 'list_remove_multiple':
|
||||
this.list_listRemoveMultiple(data)
|
||||
break
|
||||
case 'list_clear':
|
||||
this.list_listClear(data)
|
||||
break
|
||||
case 'update_music_info':
|
||||
this.list_updateMusicInfo(data)
|
||||
break
|
||||
case 'create_user_list':
|
||||
this.list_createUserList(data)
|
||||
break
|
||||
case 'remove_user_list':
|
||||
this.list_removeUserList(data)
|
||||
break
|
||||
case 'set_user_list_name':
|
||||
this.list_setUserListName(data)
|
||||
break
|
||||
case 'moveup_user_list':
|
||||
this.list_moveupUserList(data)
|
||||
break
|
||||
case 'movedown_user_list':
|
||||
this.list_movedownUserList(data)
|
||||
break
|
||||
case 'set_music_position':
|
||||
this.list_setMusicPosition(data)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
handleSyncList({ action, data }) {
|
||||
switch (action) {
|
||||
case 'getData':
|
||||
global.eventHub.$emit(eventSyncName.send_sync_list, {
|
||||
action: 'getData',
|
||||
data: {
|
||||
defaultList: this.defaultList,
|
||||
loveList: this.loveList,
|
||||
userList: this.userList,
|
||||
},
|
||||
})
|
||||
break
|
||||
case 'setData':
|
||||
this.list_setSyncListData(data)
|
||||
break
|
||||
case 'selectMode':
|
||||
this.globalObj.sync.deviceName = data.deviceName
|
||||
this.globalObj.sync.isShowSyncMode = true
|
||||
break
|
||||
case 'closeSelectMode':
|
||||
this.globalObj.sync.isShowSyncMode = false
|
||||
break
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clearUpdateTimeout()
|
||||
|
||||
@@ -123,6 +123,9 @@ small {
|
||||
.small {
|
||||
font-size: .9em;
|
||||
}
|
||||
.tip {
|
||||
color: @color-theme_2-font-label;
|
||||
}
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -192,6 +195,9 @@ each(@themes, {
|
||||
button, input, textarea, a {
|
||||
color: ~'@{color-@{value}-theme_2-font}';
|
||||
}
|
||||
.tip {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
}
|
||||
|
||||
.hover, a {
|
||||
&:hover {
|
||||
|
||||
@@ -411,7 +411,7 @@ export default {
|
||||
this.restorePlayTime = 0
|
||||
}
|
||||
if (!this.targetSong.interval && this.listId != 'download') {
|
||||
this.updateMusicInfo({ id: this.listId, index: this.playIndex, data: { interval: formatPlayTime2(this.maxPlayTime) }, musicInfo: this.targetSong })
|
||||
this.updateMusicInfo({ listId: this.listId, id: this.targetSong.songmid, musicInfo: this.targetSong, data: { interval: formatPlayTime2(this.maxPlayTime) } })
|
||||
}
|
||||
})
|
||||
audio.addEventListener('loadstart', () => {
|
||||
|
||||
@@ -140,7 +140,7 @@ export default {
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, .3);
|
||||
overflow: hidden;
|
||||
max-height: 80%;
|
||||
max-width: 70%;
|
||||
max-width: 76%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
<template lang="pug">
|
||||
material-modal(:show="globalObj.sync.isShowSyncMode" @close="handleClose(false)" :bgClose="false" :close-btn="false")
|
||||
main(:class="$style.main")
|
||||
h2 {{$t('material.sync_mode_modal.title', { name: globalObj.sync.deviceName })}}
|
||||
div.scroll(:class="$style.content")
|
||||
dl(:class="$style.btnGroup")
|
||||
dt(:class="$style.label") {{$t('material.sync_mode_modal.merge_label')}}
|
||||
dd(:class="$style.btns")
|
||||
material-btn(:class="$style.btn" @click="handleSelectMode('merge_local_remote')") {{$t('material.sync_mode_modal.merge_btn_local_remote')}}
|
||||
material-btn(:class="$style.btn" @click="handleSelectMode('merge_remote_local')") {{$t('material.sync_mode_modal.merge_btn_remote_local')}}
|
||||
dl(:class="$style.btnGroup")
|
||||
dt(:class="$style.label") {{$t('material.sync_mode_modal.overwrite_label')}}
|
||||
dd(:class="$style.btns")
|
||||
material-btn(:class="$style.btn" @click="handleSelectMode('overwrite_local_remote')") {{$t('material.sync_mode_modal.overwrite_btn_local_remote')}}
|
||||
material-btn(:class="$style.btn" @click="handleSelectMode('overwrite_remote_local')") {{$t('material.sync_mode_modal.overwrite_btn_remote_local')}}
|
||||
dd(style="font-size: 14px; margin-top: 5px;")
|
||||
material-checkbox(id="sync_mode_modal_isOverwrite" v-model="isOverwrite" :label="$t('material.sync_mode_modal.overwrite')")
|
||||
|
||||
dl(:class="$style.btnGroup")
|
||||
dt(:class="$style.label") {{$t('material.sync_mode_modal.other_label')}}
|
||||
dd(:class="$style.btns")
|
||||
material-btn(:class="$style.btn" @click="handleSelectMode('none')") {{$t('material.sync_mode_modal.overwrite_btn_none')}}
|
||||
material-btn(:class="$style.btn" @click="handleSelectMode('cancel')") {{$t('material.sync_mode_modal.overwrite_btn_cancel')}}
|
||||
dl(:class="$style.btnGroup")
|
||||
dd
|
||||
section(:class="$style.tipGroup")
|
||||
h3(:class="$style.title") {{$t('material.sync_mode_modal.merge_tip')}}
|
||||
p(:class="$style.tip") {{$t('material.sync_mode_modal.merge_tip_desc')}}
|
||||
section(:class="$style.tipGroup")
|
||||
h3(:class="$style.title") {{$t('material.sync_mode_modal.overwrite_tip')}}
|
||||
p(:class="$style.tip") {{$t('material.sync_mode_modal.overwrite_tip_desc')}}
|
||||
section(:class="$style.tipGroup")
|
||||
h3(:class="$style.title") {{$t('material.sync_mode_modal.other_tip')}}
|
||||
p(:class="$style.tip") {{$t('material.sync_mode_modal.other_tip_desc')}}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { sync as eventSyncName } from '@renderer/event/names'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isOverwrite: false,
|
||||
globalObj: {
|
||||
sync: {
|
||||
isShowSyncMode: false,
|
||||
deviceName: '',
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.globalObj = window.globalObj
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
handleSelectMode(mode) {
|
||||
if (mode.startsWith('overwrite') && this.isOverwrite) mode += '_full'
|
||||
window.eventHub.$emit(eventSyncName.send_sync_list, {
|
||||
action: 'selectMode',
|
||||
data: mode,
|
||||
})
|
||||
this.handleClose()
|
||||
},
|
||||
handleClose() {
|
||||
this.globalObj.sync.isShowSyncMode = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
@import '../../assets/styles/layout.less';
|
||||
|
||||
.main {
|
||||
padding: 15px;
|
||||
max-width: 700px;
|
||||
min-width: 200px;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
color: @color-theme_2-font;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: auto;
|
||||
padding: 15px 0 5px;
|
||||
padding-right: 5px;
|
||||
.btnGroup + .btnGroup {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.label {
|
||||
color: @color-theme_2-font-label;
|
||||
font-size: 14px;
|
||||
line-height: 2;
|
||||
}
|
||||
.desc {
|
||||
line-height: 1.5;
|
||||
font-size: 14px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.tipGroup {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 12px;
|
||||
|
||||
+ .tipGroup {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.title {
|
||||
white-space: nowrap;
|
||||
font-weight: bold;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.btn {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
+.btn {
|
||||
margin-left: 15px;
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
each(@themes, {
|
||||
:global(#container.@{value}) {
|
||||
.main {
|
||||
h2 {
|
||||
color: ~'@{color-@{value}-theme_2-font}';
|
||||
}
|
||||
}
|
||||
.name {
|
||||
color: ~'@{color-@{value}-theme}';
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
import Vue from 'vue'
|
||||
import keyBind from '../utils/keyBind'
|
||||
import { rendererOn, rendererSend, NAMES, rendererInvoke } from '../../common/ipc'
|
||||
import { base as baseName } from './names'
|
||||
import { base as baseName, sync as syncName } from './names'
|
||||
import { common as hotKeyNamesCommon } from '../../common/hotKey'
|
||||
|
||||
const eventHub = window.eventHub = new Vue()
|
||||
@@ -77,3 +77,18 @@ rendererOn(NAMES.mainWindow.set_hot_key_config, (event, config) => {
|
||||
}
|
||||
window.eventHub.$emit(baseName.set_hot_key_config, config)
|
||||
})
|
||||
|
||||
rendererOn(NAMES.mainWindow.sync_action_list, (event, { action, data }) => {
|
||||
window.eventHub.$emit(syncName.handle_action_list, { action, data })
|
||||
})
|
||||
eventHub.$on(syncName.send_action_list, ({ action, data }) => {
|
||||
if (!window.globalObj.sync.enable) return
|
||||
rendererSend(NAMES.mainWindow.sync_action_list, { action, data })
|
||||
})
|
||||
rendererOn(NAMES.mainWindow.sync_list, (event, { action, data }) => {
|
||||
window.eventHub.$emit(syncName.handle_sync_list, { action, data })
|
||||
})
|
||||
eventHub.$on(syncName.send_sync_list, ({ action, data }) => {
|
||||
if (!window.globalObj.sync.enable) return
|
||||
rendererSend(NAMES.mainWindow.sync_list, { action, data })
|
||||
})
|
||||
|
||||
@@ -10,6 +10,12 @@ const names = {
|
||||
set_config: 'set_config',
|
||||
set_hot_key_config: 'set_hot_key_config',
|
||||
},
|
||||
sync: {
|
||||
send_action_list: 'send_action_list',
|
||||
handle_action_list: 'handle_action_list',
|
||||
send_sync_list: 'send_sync_list',
|
||||
handle_sync_list: 'handle_sync_list',
|
||||
},
|
||||
}
|
||||
|
||||
for (const item of Object.keys(names)) {
|
||||
@@ -20,3 +26,4 @@ for (const item of Object.keys(names)) {
|
||||
}
|
||||
|
||||
export const base = names.base
|
||||
export const sync = names.sync
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"merge_btn_local_remote": "Local list merge remote list",
|
||||
"merge_btn_remote_local": "Remote list merge local list",
|
||||
"merge_label": "Merge",
|
||||
"merge_tip": "Merge:",
|
||||
"merge_tip_desc": "Merge the two lists together, the same song will be removed (the song of the merged person is removed), and different songs will be added.",
|
||||
"other_label": "Other",
|
||||
"other_tip": "Other: ",
|
||||
"other_tip_desc": "\"Only use real-time synchronization function\" will not modify the lists of both parties, only real-time synchronization operations; \"Cancel synchronization\" will directly disconnect the two parties.",
|
||||
"overwrite": "Full coverage",
|
||||
"overwrite_btn_cancel": "Cancel sync",
|
||||
"overwrite_btn_local_remote": "Local list Overwrite remote list",
|
||||
"overwrite_btn_none": "Only use real-time synchronization",
|
||||
"overwrite_btn_remote_local": "Remote list Overwrite local list",
|
||||
"overwrite_label": "Cover",
|
||||
"overwrite_tip": "Cover: ",
|
||||
"overwrite_tip_desc": "The list with the same ID of the covered person and the covered list will be deleted and replaced with the list of the covered person (lists with different list IDs will be merged together). If you check Complete coverage, all lists of the covered person will be moved. \nDivide, and then replace with a list of overriders.",
|
||||
"title": "Choose how to synchronize the list with {name}"
|
||||
}
|
||||
@@ -130,6 +130,14 @@
|
||||
"search_focus_search_box": "Automatically focus the search box on startup",
|
||||
"search_history": "Search history",
|
||||
"search_hot": "Top Searches",
|
||||
"sync": "Data synchronization [This is a test function, it is recommended to back up the playlist before using it for the first time]",
|
||||
"sync_address": "Synchronization service address: {address}",
|
||||
"sync_auth_code": "Connection code: {code}",
|
||||
"sync_device": "Connected devices: {devices}",
|
||||
"sync_enable": "Enable synchronization",
|
||||
"sync_port": "Sync port settings",
|
||||
"sync_port_tip": "Please enter the synchronization service port number",
|
||||
"sync_refresh_code": "Refresh the connection code",
|
||||
"update": "Update",
|
||||
"update_checking": "Checking for updates...",
|
||||
"update_current_label": "Current version: ",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"merge_btn_local_remote": "本机列表 合并 远程列表",
|
||||
"merge_btn_remote_local": "远程列表 合并 本机列表",
|
||||
"merge_label": "合并",
|
||||
"merge_tip": "合并:",
|
||||
"merge_tip_desc": "将两边的列表合并到一起,相同的歌曲将被去掉(去掉的是被合并者的歌曲),不同的歌曲将被添加。",
|
||||
"other_label": "其他",
|
||||
"other_tip": "其他:",
|
||||
"other_tip_desc": "“仅使用实时同步功能”将不修改双方的列表,仅实时同步操作;“取消同步”将直接断开双方的连接。",
|
||||
"overwrite": "完全覆盖",
|
||||
"overwrite_btn_cancel": "取消同步",
|
||||
"overwrite_btn_local_remote": "本机列表 覆盖 远程列表",
|
||||
"overwrite_btn_none": "仅使用实时同步功能",
|
||||
"overwrite_btn_remote_local": "远程列表 覆盖 本机列表",
|
||||
"overwrite_label": "覆盖",
|
||||
"overwrite_tip": "覆盖:",
|
||||
"overwrite_tip_desc": "被覆盖者与覆盖者列表ID相同的列表将被删除后替换成覆盖者的列表(列表ID不同的列表将被合并到一起),若勾选完全覆盖,则被覆盖者的所有列表将被移除,然后替换成覆盖者的列表。",
|
||||
"title": "选择与 {name} 的列表同步方式"
|
||||
}
|
||||
@@ -130,6 +130,14 @@
|
||||
"search_focus_search_box": "启动时自动聚焦搜索框",
|
||||
"search_history": "显示历史搜索记录",
|
||||
"search_hot": "显示热门搜索",
|
||||
"sync": "数据同步 [此为测试功能,首次使用前建议先备份一次歌单]",
|
||||
"sync_address": "同步服务地址:{address}",
|
||||
"sync_auth_code": "连接码:{code}",
|
||||
"sync_device": "已连接的设备:{devices}",
|
||||
"sync_enable": "启用同步功能",
|
||||
"sync_port": "同步端口设置",
|
||||
"sync_port_tip": "请输入同步服务端口号",
|
||||
"sync_refresh_code": "刷新连接码",
|
||||
"update": "软件更新",
|
||||
"update_checking": "检查更新中...",
|
||||
"update_current_label": "当前版本:",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"merge_btn_local_remote": "本機列表 合併 遠程列表",
|
||||
"merge_btn_remote_local": "遠程列表 合併 本機列表",
|
||||
"merge_label": "合併",
|
||||
"merge_tip": "合併:",
|
||||
"merge_tip_desc": "將兩邊的列表合併到一起,相同的歌曲將被去掉(去掉的是被合併者的歌曲),不同的歌曲將被添加。",
|
||||
"other_label": "其他",
|
||||
"other_tip": "其他:",
|
||||
"other_tip_desc": "“僅使用實時同步功能”將不修改雙方的列表,僅實時同步操作;“取消同步”將直接斷開雙方的連接。",
|
||||
"overwrite": "完全覆蓋",
|
||||
"overwrite_btn_cancel": "取消同步",
|
||||
"overwrite_btn_local_remote": "本機列表 覆蓋 遠程列表",
|
||||
"overwrite_btn_none": "僅使用實時同步功能",
|
||||
"overwrite_btn_remote_local": "遠程列表 覆蓋 本機列表",
|
||||
"overwrite_label": "覆蓋",
|
||||
"overwrite_tip": "覆蓋:",
|
||||
"overwrite_tip_desc": "被覆蓋者與覆蓋者列表ID相同的列表將被刪除後替換成覆蓋者的列表(列表ID不同的列表將被合併到一起),若勾選完全覆蓋,則被覆蓋者的所有列表將被移除,然後替換成覆蓋者的列表。",
|
||||
"title": "選擇與 {name} 的列表同步方式"
|
||||
}
|
||||
@@ -130,6 +130,14 @@
|
||||
"search_focus_search_box": "啟動時自動聚焦搜索框",
|
||||
"search_history": "顯示歷史搜索記錄",
|
||||
"search_hot": "顯示熱門搜索",
|
||||
"sync": "數據同步 [此為測試功能,首次使用前建議先備份一次歌單]",
|
||||
"sync_address": "同步服務地址:{address}",
|
||||
"sync_auth_code": "連接碼:{code}",
|
||||
"sync_device": "已連接的設備:{devices}",
|
||||
"sync_enable": "啟用同步功能",
|
||||
"sync_port": "同步端口設置",
|
||||
"sync_port_tip": "請輸入同步服務端口號",
|
||||
"sync_refresh_code": "刷新連接碼",
|
||||
"update": "軟件更新",
|
||||
"update_checking": "檢查更新中...",
|
||||
"update_current_label": "當前版本:",
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import musicSdk from '../../utils/music'
|
||||
import { clearLyric, clearMusicUrl } from '../../utils'
|
||||
import { sync as eventSyncName } from '@renderer/event/names'
|
||||
|
||||
let allList = {}
|
||||
window.allList = allList
|
||||
|
||||
const allListInit = (defaultList, loveList, userList) => {
|
||||
for (const id of Object.keys(allList)) {
|
||||
delete allList[id]
|
||||
}
|
||||
allList[defaultList.id] = defaultList
|
||||
allList[loveList.id] = loveList
|
||||
for (const list of userList) allList[list.id] = list
|
||||
@@ -67,8 +71,28 @@ const mutations = {
|
||||
if (userList != null) state.userList = userList
|
||||
allListInit(state.defaultList, state.loveList, state.userList)
|
||||
state.isInitedList = true
|
||||
|
||||
// if (!isSync) {
|
||||
// window.eventHub.$emit(eventSyncName.send_action_list, {
|
||||
// action: 'init_list',
|
||||
// data: { defaultList, loveList, userList },
|
||||
// })
|
||||
// }
|
||||
},
|
||||
setList(state, { id, list, name, location, source, sourceListId }) {
|
||||
setSyncListData(state, { defaultList, loveList, userList }) {
|
||||
state.defaultList.list.splice(0, state.defaultList.list.length, ...defaultList.list)
|
||||
state.loveList.list.splice(0, state.loveList.list.length, ...loveList.list)
|
||||
state.userList = userList
|
||||
allListInit(state.defaultList, state.loveList, state.userList)
|
||||
},
|
||||
setList(state, { id, list, name, location, source, sourceListId, isSync }) {
|
||||
if (!isSync) {
|
||||
window.eventHub.$emit(eventSyncName.send_action_list, {
|
||||
action: 'set_list',
|
||||
data: { id, list, name, location, source, sourceListId },
|
||||
})
|
||||
}
|
||||
|
||||
const targetList = allList[id]
|
||||
if (targetList) {
|
||||
if (name && targetList.name === name) {
|
||||
@@ -90,11 +114,20 @@ const mutations = {
|
||||
state.userList.push(newList)
|
||||
allListUpdate(newList)
|
||||
},
|
||||
listAdd(state, { id, musicInfo }) {
|
||||
listAdd(state, { id, musicInfo, addMusicLocationType, isSync }) {
|
||||
if (!addMusicLocationType) addMusicLocationType = this.state.setting.list.addMusicLocationType
|
||||
|
||||
if (!isSync) {
|
||||
window.eventHub.$emit(eventSyncName.send_action_list, {
|
||||
action: 'list_add',
|
||||
data: { id, musicInfo, addMusicLocationType },
|
||||
})
|
||||
}
|
||||
|
||||
const targetList = allList[id]
|
||||
if (!targetList) return
|
||||
if (targetList.list.some(s => s.songmid === musicInfo.songmid)) return
|
||||
switch (this.state.setting.list.addMusicLocationType) {
|
||||
switch (addMusicLocationType) {
|
||||
case 'top':
|
||||
targetList.list.unshift(musicInfo)
|
||||
break
|
||||
@@ -104,11 +137,18 @@ const mutations = {
|
||||
break
|
||||
}
|
||||
},
|
||||
listMove(state, { fromId, musicInfo, toId }) {
|
||||
listMove(state, { fromId, musicInfo, toId, isSync }) {
|
||||
if (!isSync) {
|
||||
window.eventHub.$emit(eventSyncName.send_action_list, {
|
||||
action: 'list_move',
|
||||
data: { fromId, musicInfo, toId },
|
||||
})
|
||||
}
|
||||
|
||||
const fromList = allList[fromId]
|
||||
const toList = allList[toId]
|
||||
if (!fromList || !toList) return
|
||||
fromList.list.splice(fromList.list.indexOf(musicInfo), 1)
|
||||
fromList.list.splice(fromList.list.findIndex(s => s.songmid === musicInfo.songmid), 1)
|
||||
let index = toList.list.findIndex(s => s.songmid === musicInfo.songmid)
|
||||
if (index < 0) {
|
||||
switch (this.state.setting.list.addMusicLocationType) {
|
||||
@@ -122,41 +162,79 @@ const mutations = {
|
||||
}
|
||||
}
|
||||
},
|
||||
listAddMultiple(state, { id, list }) {
|
||||
listAddMultiple(state, { id, list, addMusicLocationType, isSync }) {
|
||||
if (!addMusicLocationType) addMusicLocationType = this.state.setting.list.addMusicLocationType
|
||||
|
||||
if (!isSync) {
|
||||
window.eventHub.$emit(eventSyncName.send_action_list, {
|
||||
action: 'list_add_multiple',
|
||||
data: { id, list, addMusicLocationType },
|
||||
})
|
||||
}
|
||||
|
||||
let targetList = allList[id]
|
||||
if (!targetList) return
|
||||
let newList
|
||||
switch (this.state.setting.list.addMusicLocationType) {
|
||||
const map = {}
|
||||
const ids = []
|
||||
switch (addMusicLocationType) {
|
||||
case 'top':
|
||||
newList = [...list, ...targetList.list]
|
||||
for (let i = newList.length - 1; i > -1; i--) {
|
||||
const item = newList[i]
|
||||
if (map[item.songmid]) continue
|
||||
ids.unshift(item.songmid)
|
||||
map[item.songmid] = item
|
||||
}
|
||||
break
|
||||
case 'bottom':
|
||||
default:
|
||||
newList = [...targetList.list, ...list]
|
||||
for (const item of newList) {
|
||||
if (map[item.songmid]) continue
|
||||
ids.push(item.songmid)
|
||||
map[item.songmid] = item
|
||||
}
|
||||
break
|
||||
}
|
||||
let map = {}
|
||||
let ids = []
|
||||
for (const item of newList) {
|
||||
if (map[item.songmid]) continue
|
||||
ids.push(item.songmid)
|
||||
map[item.songmid] = item
|
||||
}
|
||||
targetList.list.splice(0, targetList.list.length, ...ids.map(id => map[id]))
|
||||
},
|
||||
// { fromId, toId, list }
|
||||
listMoveMultiple(state, { fromId, toId, list }) {
|
||||
listMoveMultiple(state, { fromId, toId, list, isSync }) {
|
||||
if (!isSync) {
|
||||
window.eventHub.$emit(eventSyncName.send_action_list, {
|
||||
action: 'list_move_multiple',
|
||||
data: { fromId, toId, list },
|
||||
})
|
||||
}
|
||||
|
||||
// console.log(state.commit)
|
||||
this.commit('list/listRemoveMultiple', { id: fromId, list })
|
||||
this.commit('list/listAddMultiple', { id: toId, list })
|
||||
this.commit('list/listRemoveMultiple', { listId: fromId, ids: list.map(s => s.songmid), isSync: true })
|
||||
this.commit('list/listAddMultiple', { id: toId, list, isSync: true })
|
||||
},
|
||||
listRemove(state, { id, index }) {
|
||||
let targetList = allList[id]
|
||||
listRemove(state, { listId, id, isSync }) {
|
||||
if (!isSync) {
|
||||
window.eventHub.$emit(eventSyncName.send_action_list, {
|
||||
action: 'list_remove',
|
||||
data: { listId, id },
|
||||
})
|
||||
}
|
||||
|
||||
let targetList = allList[listId]
|
||||
if (!targetList) return
|
||||
const index = targetList.list.findIndex(item => item.songmid == id)
|
||||
if (index < 0) return
|
||||
targetList.list.splice(index, 1)
|
||||
},
|
||||
listRemoveMultiple(state, { id, list }) {
|
||||
let targetList = allList[id]
|
||||
listRemoveMultiple(state, { listId, ids: musicIds, isSync }) {
|
||||
if (!isSync) {
|
||||
window.eventHub.$emit(eventSyncName.send_action_list, {
|
||||
action: 'list_remove_multiple',
|
||||
data: { listId, ids: musicIds },
|
||||
})
|
||||
}
|
||||
|
||||
let targetList = allList[listId]
|
||||
if (!targetList) return
|
||||
let map = {}
|
||||
let ids = []
|
||||
@@ -164,25 +242,50 @@ const mutations = {
|
||||
ids.push(item.songmid)
|
||||
map[item.songmid] = item
|
||||
}
|
||||
for (const item of list) {
|
||||
if (map[item.songmid]) delete map[item.songmid]
|
||||
for (const songmid of musicIds) {
|
||||
if (map[songmid]) delete map[songmid]
|
||||
}
|
||||
let newList = []
|
||||
for (const id of ids) if (map[id]) newList.push(map[id])
|
||||
|
||||
targetList.list.splice(0, targetList.list.length, ...newList)
|
||||
},
|
||||
listClear(state, id) {
|
||||
listClear(state, { id, isSync }) {
|
||||
if (!isSync) {
|
||||
window.eventHub.$emit(eventSyncName.send_action_list, {
|
||||
action: 'list_clear',
|
||||
data: { id },
|
||||
})
|
||||
}
|
||||
|
||||
let targetList = allList[id]
|
||||
if (!targetList) return
|
||||
targetList.list.splice(0, targetList.list.length)
|
||||
},
|
||||
updateMusicInfo(state, { id, index, data, musicInfo = {} }) {
|
||||
let targetList = allList[id]
|
||||
if (!targetList) return Object.assign(musicInfo, data)
|
||||
Object.assign(targetList.list[index], data)
|
||||
updateMusicInfo(state, { listId, id, data, musicInfo, isSync }) {
|
||||
if (!isSync) {
|
||||
window.eventHub.$emit(eventSyncName.send_action_list, {
|
||||
action: 'update_music_info',
|
||||
data: { listId, id, data, musicInfo },
|
||||
})
|
||||
}
|
||||
|
||||
let targetList = allList[listId]
|
||||
if (!targetList) {
|
||||
if (musicInfo) Object.assign(musicInfo, data)
|
||||
return
|
||||
}
|
||||
const targetMusicInfo = targetList.list.find(item => item.songmid == id)
|
||||
if (targetMusicInfo) Object.assign(targetMusicInfo, data)
|
||||
},
|
||||
createUserList(state, { name, id = `userlist_${Date.now()}`, list = [], source, sourceListId }) {
|
||||
createUserList(state, { name, id = `userlist_${Date.now()}`, list = [], source, sourceListId, isSync }) {
|
||||
if (!isSync) {
|
||||
window.eventHub.$emit(eventSyncName.send_action_list, {
|
||||
action: 'create_user_list',
|
||||
data: { name, id, list, source, sourceListId },
|
||||
})
|
||||
}
|
||||
|
||||
let newList = state.userList.find(item => item.id === id)
|
||||
if (!newList) {
|
||||
newList = {
|
||||
@@ -196,35 +299,75 @@ const mutations = {
|
||||
state.userList.push(newList)
|
||||
allListUpdate(newList)
|
||||
}
|
||||
this.commit('list/listAddMultiple', { id, list })
|
||||
this.commit('list/listAddMultiple', { id, list, isSync: true })
|
||||
},
|
||||
removeUserList(state, index) {
|
||||
removeUserList(state, { id, isSync }) {
|
||||
if (!isSync) {
|
||||
window.eventHub.$emit(eventSyncName.send_action_list, {
|
||||
action: 'remove_user_list',
|
||||
data: { id },
|
||||
})
|
||||
}
|
||||
|
||||
const index = state.userList.findIndex(l => l.id === id)
|
||||
if (index < 0) return
|
||||
let list = state.userList.splice(index, 1)[0]
|
||||
allListRemove(list)
|
||||
},
|
||||
setUserListName(state, { index, name }) {
|
||||
let list = state.userList[index]
|
||||
setUserListName(state, { id, name, isSync }) {
|
||||
if (!isSync) {
|
||||
window.eventHub.$emit(eventSyncName.send_action_list, {
|
||||
action: 'set_user_list_name',
|
||||
data: { id, name },
|
||||
})
|
||||
}
|
||||
|
||||
let list = allList[id]
|
||||
if (!list) return
|
||||
list.name = name
|
||||
},
|
||||
moveupUserList(state, index) {
|
||||
let targetList = state.userList[index]
|
||||
moveupUserList(state, { id, isSync }) {
|
||||
if (!isSync) {
|
||||
window.eventHub.$emit(eventSyncName.send_action_list, {
|
||||
action: 'moveup_user_list',
|
||||
data: { id },
|
||||
})
|
||||
}
|
||||
|
||||
const index = state.userList.findIndex(l => l.id == id)
|
||||
if (index < 0) return
|
||||
let targetList = allList[id]
|
||||
state.userList.splice(index, 1)
|
||||
state.userList.splice(index - 1, 0, targetList)
|
||||
},
|
||||
movedownUserList(state, index) {
|
||||
let targetList = state.userList[index]
|
||||
movedownUserList(state, { id, isSync }) {
|
||||
if (!isSync) {
|
||||
window.eventHub.$emit(eventSyncName.send_action_list, {
|
||||
action: 'movedown_user_list',
|
||||
data: { id },
|
||||
})
|
||||
}
|
||||
const index = state.userList.findIndex(l => l.id == id)
|
||||
if (index < 0) return
|
||||
let targetList = allList[id]
|
||||
state.userList.splice(index, 1)
|
||||
state.userList.splice(index + 1, 0, targetList)
|
||||
},
|
||||
setListScroll(state, { id, location }) {
|
||||
if (allList[id]) allList[id].location = location
|
||||
},
|
||||
sortList(state, { id, sortNum, musicInfos }) {
|
||||
let targetList = allList[id]
|
||||
this.commit('list/listRemoveMultiple', { id, list: musicInfos })
|
||||
setMusicPosition(state, { id, position, list, isSync }) {
|
||||
if (!isSync) {
|
||||
window.eventHub.$emit(eventSyncName.send_action_list, {
|
||||
action: 'set_music_position',
|
||||
data: { id, position, list },
|
||||
})
|
||||
}
|
||||
|
||||
targetList.list.splice(sortNum - 1, 0, ...musicInfos)
|
||||
let targetList = allList[id]
|
||||
this.commit('list/listRemoveMultiple', { listId: id, ids: list.map(m => m.songmid), isSync: true })
|
||||
|
||||
targetList.list.splice(position - 1, 0, ...list)
|
||||
},
|
||||
clearCache() {
|
||||
const lists = Object.values(allList)
|
||||
|
||||
@@ -156,11 +156,11 @@ const getters = {
|
||||
|
||||
if (listId != '__temp__') {
|
||||
if (isPlayList) {
|
||||
playIndex = state.listInfo.list.indexOf(state.playMusicInfo.musicInfo)
|
||||
playIndex = state.listInfo.list.findIndex(m => m.songmid == state.playMusicInfo.musicInfo.songmid)
|
||||
if (!isTempPlay) listPlayIndex = playIndex
|
||||
} else {
|
||||
let list = window.allList[listId]
|
||||
if (list) playIndex = list.list.indexOf(state.playMusicInfo.musicInfo)
|
||||
if (list) playIndex = list.list.findIndex(m => m.songmid == state.playMusicInfo.musicInfo.songmid)
|
||||
}
|
||||
}
|
||||
// console.log({
|
||||
@@ -273,7 +273,8 @@ const actions = {
|
||||
})
|
||||
if (!filteredList.length) return commit('setPlayMusicInfo', null)
|
||||
const playInfo = getters.playInfo
|
||||
let currentIndex = filteredList.indexOf(currentList[playInfo.listPlayIndex])
|
||||
const currentMusic = currentList[playInfo.listPlayIndex]
|
||||
let currentIndex = filteredList.findIndex(m => m.songmid == currentMusic.songmid)
|
||||
if (currentIndex == -1) currentIndex = 0
|
||||
let nextIndex = currentIndex
|
||||
if (!playInfo.isTempPlay) {
|
||||
@@ -334,7 +335,8 @@ const actions = {
|
||||
|
||||
if (!filteredList.length) return commit('setPlayMusicInfo', null)
|
||||
const playInfo = getters.playInfo
|
||||
const currentIndex = filteredList.indexOf(currentList[playInfo.listPlayIndex])
|
||||
const currentMusic = currentList[playInfo.listPlayIndex]
|
||||
let currentIndex = filteredList.findIndex(m => m.songmid == currentMusic.songmid)
|
||||
let nextIndex = currentIndex
|
||||
switch (rootState.setting.player.togglePlayMethod) {
|
||||
case 'listLoop':
|
||||
@@ -433,7 +435,7 @@ const mutations = {
|
||||
playIndex = -1
|
||||
} else {
|
||||
let listId = playMusicInfo.listId
|
||||
if (listId != '__temp__' && !playMusicInfo.isTempPlay && listId === state.listInfo.id) playIndex = state.listInfo.list.indexOf(playMusicInfo.musicInfo)
|
||||
if (listId != '__temp__' && !playMusicInfo.isTempPlay && listId === state.listInfo.id) playIndex = state.listInfo.list.findIndex(m => m.songmid == playMusicInfo.musicInfo.songmid)
|
||||
}
|
||||
|
||||
state.playMusicInfo = playMusicInfo
|
||||
|
||||
+12
-11
@@ -355,7 +355,7 @@ export default {
|
||||
'removeUserList',
|
||||
'setListScroll',
|
||||
'setList',
|
||||
'sortList',
|
||||
'setMusicPosition',
|
||||
]),
|
||||
...mapActions('songList', ['getListDetailAll']),
|
||||
...mapActions('leaderboard', {
|
||||
@@ -576,7 +576,7 @@ export default {
|
||||
this.setPlayList({ list: this.listData, index })
|
||||
},
|
||||
handleRemove(index) {
|
||||
this.listRemove({ id: this.listId, index })
|
||||
this.listRemove({ listId: this.listId, id: this.list[index].songmid })
|
||||
},
|
||||
handleListBtnClick(info) {
|
||||
switch (info.action) {
|
||||
@@ -677,8 +677,9 @@ export default {
|
||||
let dom_target = this.$refs.dom_lists_list.querySelector('.' + this.$style.editing)
|
||||
if (dom_target) dom_target.classList.remove(this.$style.editing)
|
||||
let name = event.target.value.trim()
|
||||
if (name.length) return this.setUserListName({ index, name })
|
||||
event.target.value = this.userList[index].name
|
||||
const targetList = this.userList[index]
|
||||
if (name.length) return this.setUserListName({ id: targetList.id, name })
|
||||
event.target.value = targetList.name
|
||||
},
|
||||
handleListsCreate(event) {
|
||||
if (event.target.readonly) return
|
||||
@@ -774,13 +775,13 @@ export default {
|
||||
this.handleSyncSourceList(index)
|
||||
break
|
||||
case 'moveup':
|
||||
this.moveupUserList(index)
|
||||
this.moveupUserList({ id: this.userList[index].id })
|
||||
break
|
||||
case 'movedown':
|
||||
this.movedownUserList(index)
|
||||
this.movedownUserList({ id: this.userList[index].id })
|
||||
break
|
||||
case 'remove':
|
||||
this.removeUserList(index)
|
||||
this.removeUserList({ id: this.userList[index].id })
|
||||
break
|
||||
}
|
||||
},
|
||||
@@ -870,7 +871,7 @@ export default {
|
||||
break
|
||||
case 'remove':
|
||||
if (this.selectdListDetailData.length) {
|
||||
this.listRemoveMultiple({ id: this.listId, list: this.selectdListDetailData })
|
||||
this.listRemoveMultiple({ listId: this.listId, ids: this.selectdListDetailData.map(m => m.songmid) })
|
||||
this.removeAllSelectListDetail()
|
||||
} else {
|
||||
this.handleRemove(index)
|
||||
@@ -929,10 +930,10 @@ export default {
|
||||
},
|
||||
handleSortMusicInfo(num) {
|
||||
num = Math.min(num, this.list.length)
|
||||
this.sortList({
|
||||
this.setMusicPosition({
|
||||
id: this.listId,
|
||||
sortNum: num,
|
||||
musicInfos: this.selectdListDetailData.length ? [...this.selectdListDetailData] : [this.musicInfo],
|
||||
position: num,
|
||||
list: this.selectdListDetailData.length ? [...this.selectdListDetailData] : [this.musicInfo],
|
||||
})
|
||||
this.removeAllSelectListDetail()
|
||||
this.isShowListSortModal = false
|
||||
|
||||
@@ -150,6 +150,21 @@ div(:class="$style.main")
|
||||
div
|
||||
material-checkbox(id="setting_download_isDownloadLrc" v-model="current_setting.download.isDownloadLrc" :label="$t('view.setting.is_enable')")
|
||||
|
||||
dt#sync {{$t('view.setting.sync')}}
|
||||
dd
|
||||
material-checkbox(id="setting_sync_enable" v-model="current_setting.sync.enable" @change="handleSyncChange('enable')" :label="syncEnableTitle")
|
||||
div
|
||||
p.small {{$t('view.setting.sync_auth_code', { code: sync.status.code || '' })}}
|
||||
p.small {{$t('view.setting.sync_address', { address: sync.status.address.join(', ') || '' })}}
|
||||
p.small {{$t('view.setting.sync_device', { devices: syncDevices })}}
|
||||
p
|
||||
material-btn(:class="$style.btn" min :disabled="!current_setting.sync.enable" @click="handleRefreshSyncCode") {{$t('view.setting.sync_refresh_code')}}
|
||||
dd
|
||||
h3#sync_port {{$t('view.setting.sync_port')}}
|
||||
div
|
||||
p
|
||||
material-input(:class="$style.gapLeft" v-model.trim="current_setting.sync.port" @change="handleSyncChange('port')" :placeholder="$t('view.setting.sync_port_tip')")
|
||||
|
||||
dt#hot_key {{$t('view.setting.hot_key')}}
|
||||
dd
|
||||
h3#hot_key_local_title {{$t('view.setting.hot_key_local_title')}}
|
||||
@@ -295,7 +310,7 @@ import {
|
||||
getSetting,
|
||||
saveSetting,
|
||||
} from '../utils'
|
||||
import { rendererSend, rendererInvoke, NAMES } from '@common/ipc'
|
||||
import { rendererSend, rendererInvoke, rendererOn, NAMES, rendererOff } from '@common/ipc'
|
||||
import { mergeSetting, isMac } from '../../common/utils'
|
||||
import apiSourceInfo from '../utils/music/api-source-info'
|
||||
import fs from 'fs'
|
||||
@@ -410,6 +425,21 @@ export default {
|
||||
},
|
||||
]
|
||||
},
|
||||
syncEnableTitle() {
|
||||
let title = this.$t('view.setting.sync_enable')
|
||||
if (this.sync.status.message) {
|
||||
title += ` [${this.sync.status.message}]`
|
||||
}
|
||||
// else if (this.sync.status.address.length) {
|
||||
// // title += ` [${this.sync.status.address.join(', ')}]`
|
||||
// }
|
||||
return title
|
||||
},
|
||||
syncDevices() {
|
||||
return this.sync.status.devices.length
|
||||
? this.sync.status.devices.map(d => `${d.deviceName} (${d.clientId.substring(0, 5)})`).join(', ')
|
||||
: ''
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -474,6 +504,10 @@ export default {
|
||||
isToTray: false,
|
||||
themeId: 0,
|
||||
},
|
||||
sync: {
|
||||
enable: false,
|
||||
port: '23332',
|
||||
},
|
||||
windowSizeId: 1,
|
||||
langId: 'cns',
|
||||
themeId: 0,
|
||||
@@ -608,6 +642,15 @@ export default {
|
||||
},
|
||||
isDisabledResourceCacheClear: false,
|
||||
isDisabledListCacheClear: false,
|
||||
sync: {
|
||||
status: {
|
||||
status: false,
|
||||
message: '',
|
||||
address: [],
|
||||
code: '',
|
||||
devices: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -665,6 +708,7 @@ export default {
|
||||
window.eventHub.$off(eventBaseName.set_config, this.handleUpdateSetting)
|
||||
window.eventHub.$off(eventBaseName.key_down, this.handleKeyDown)
|
||||
window.eventHub.$off(eventBaseName.set_hot_key_config, this.handleUpdateHotKeyConfig)
|
||||
this.syncUnInit()
|
||||
|
||||
if (this.current_setting.network.proxy.enable && !this.current_setting.network.proxy.host) window.globalObj.proxy.enable = false
|
||||
},
|
||||
@@ -684,6 +728,7 @@ export default {
|
||||
this.current_hot_key = window.appHotKeyConfig
|
||||
this.initHotKeyConfig()
|
||||
this.getHotKeyStatus()
|
||||
this.syncInit()
|
||||
},
|
||||
// initTOC() {
|
||||
// const list = this.$refs.dom_setting_list.children
|
||||
@@ -1151,6 +1196,42 @@ export default {
|
||||
|
||||
return status
|
||||
},
|
||||
setStatus(e, status) {
|
||||
this.sync.status.status = status.status
|
||||
this.sync.status.message = status.message
|
||||
this.sync.status.address = status.address
|
||||
this.sync.status.code = status.code
|
||||
this.sync.status.devices = status.devices
|
||||
},
|
||||
syncInit() {
|
||||
rendererInvoke(NAMES.mainWindow.sync_get_status).then(status => {
|
||||
this.sync.status.status = status.status
|
||||
this.sync.status.message = status.message
|
||||
this.sync.status.address = status.address
|
||||
this.sync.status.code = status.code
|
||||
this.sync.status.devices = status.devices
|
||||
})
|
||||
rendererOn(NAMES.mainWindow.sync_status, this.setStatus)
|
||||
},
|
||||
syncUnInit() {
|
||||
rendererOff(NAMES.mainWindow.sync_status, this.setStatus)
|
||||
},
|
||||
handleSyncChange(action) {
|
||||
switch (action) {
|
||||
case 'port':
|
||||
if (!this.current_setting.sync.enable) return
|
||||
case 'enable':
|
||||
rendererInvoke(NAMES.mainWindow.sync_enable, {
|
||||
enable: this.current_setting.sync.enable,
|
||||
port: this.current_setting.sync.port,
|
||||
})
|
||||
window.globalObj.sync.enable = this.current_setting.sync.enable
|
||||
break
|
||||
}
|
||||
},
|
||||
handleRefreshSyncCode() {
|
||||
rendererInvoke(NAMES.mainWindow.sync_generate_code)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user