v2.0.0
This commit is contained in:
@@ -1,130 +0,0 @@
|
||||
const { log, isWin } = require('../../common/utils')
|
||||
const { autoUpdater } = require('electron-updater')
|
||||
const { mainOn, mainSend, NAMES: { mainWindow: ipcMainWindowNames } } = require('../../common/ipc')
|
||||
|
||||
autoUpdater.logger = log
|
||||
// autoUpdater.autoDownload = false
|
||||
autoUpdater.logger.transports.file.level = 'info'
|
||||
|
||||
let isFirstCheckedUpdate = true
|
||||
|
||||
log.info('App starting...')
|
||||
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Open a window that displays the version
|
||||
//
|
||||
// THIS SECTION IS NOT REQUIRED
|
||||
//
|
||||
// This isn't required for auto-updates to work, but it's easier
|
||||
// for the app to show a window than to have to click "About" to see
|
||||
// that updates are working.
|
||||
// -------------------------------------------------------------------
|
||||
// let win
|
||||
|
||||
function sendStatusToWindow(text) {
|
||||
log.info(text)
|
||||
// ipcMain.send('message', text)
|
||||
}
|
||||
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Auto updates
|
||||
//
|
||||
// For details about these events, see the Wiki:
|
||||
// https://github.com/electron-userland/electron-builder/wiki/Auto-Update#events
|
||||
//
|
||||
// The app doesn't need to listen to any events except `update-downloaded`
|
||||
//
|
||||
// Uncomment any of the below events to listen for them. Also,
|
||||
// look in the previous section to see them being used.
|
||||
// -------------------------------------------------------------------
|
||||
// autoUpdater.on('checking-for-update', () => {
|
||||
// })
|
||||
// autoUpdater.on('update-available', (ev, info) => {
|
||||
// })
|
||||
// autoUpdater.on('update-not-available', (ev, info) => {
|
||||
// })
|
||||
// autoUpdater.on('error', (ev, err) => {
|
||||
// })
|
||||
// autoUpdater.on('download-progress', (ev, progressObj) => {
|
||||
// })
|
||||
// autoUpdater.on('update-downloaded', (ev, info) => {
|
||||
// // Wait 5 seconds, then quit and install
|
||||
// // In your application, you don't need to wait 5 seconds.
|
||||
// // You could call autoUpdater.quitAndInstall(); immediately
|
||||
// // setTimeout(function() {
|
||||
// // autoUpdater.quitAndInstall()
|
||||
// // }, 5000)
|
||||
|
||||
// })
|
||||
|
||||
let waitEvent = []
|
||||
const handleSendEvent = action => {
|
||||
if (global.modules.mainWindow) {
|
||||
setTimeout(() => { // 延迟发送事件,过早发送可能渲染进程还没启动完成
|
||||
if (!global.modules.mainWindow) return
|
||||
mainSend(global.modules.mainWindow, action.type, action.info)
|
||||
}, 2000)
|
||||
} else {
|
||||
waitEvent.push(action)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = () => {
|
||||
if (!isFirstCheckedUpdate) {
|
||||
if (waitEvent.length) {
|
||||
waitEvent.forEach((event, index) => {
|
||||
setTimeout(() => { // 延迟发送事件,过早发送可能渲染进程还没启动完成
|
||||
if (!global.modules.mainWindow) return
|
||||
mainSend(global.modules.mainWindow, event.type, event.info)
|
||||
}, 2000 * (index + 1))
|
||||
})
|
||||
waitEvent = []
|
||||
}
|
||||
return
|
||||
}
|
||||
isFirstCheckedUpdate = false
|
||||
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
sendStatusToWindow('Checking for update...')
|
||||
})
|
||||
autoUpdater.on('update-available', info => {
|
||||
sendStatusToWindow('Update available.')
|
||||
handleSendEvent({ type: ipcMainWindowNames.update_available, info })
|
||||
})
|
||||
autoUpdater.on('update-not-available', info => {
|
||||
sendStatusToWindow('Update not available.')
|
||||
handleSendEvent({ type: ipcMainWindowNames.update_not_available, info })
|
||||
})
|
||||
autoUpdater.on('error', err => {
|
||||
sendStatusToWindow('Error in auto-updater.')
|
||||
handleSendEvent({ type: ipcMainWindowNames.update_error, info: err.message })
|
||||
})
|
||||
autoUpdater.on('download-progress', progressObj => {
|
||||
let log_message = 'Download speed: ' + progressObj.bytesPerSecond
|
||||
log_message = log_message + ' - Downloaded ' + progressObj.percent + '%'
|
||||
log_message = log_message + ' (' + progressObj.transferred + '/' + progressObj.total + ')'
|
||||
sendStatusToWindow(log_message)
|
||||
handleSendEvent({ type: ipcMainWindowNames.update_progress, info: progressObj })
|
||||
})
|
||||
autoUpdater.on('update-downloaded', info => {
|
||||
sendStatusToWindow('Update downloaded.')
|
||||
handleSendEvent({ type: ipcMainWindowNames.update_downloaded, info })
|
||||
})
|
||||
mainOn(ipcMainWindowNames.quit_update, () => {
|
||||
global.isQuitting = true
|
||||
|
||||
setTimeout(() => {
|
||||
autoUpdater.quitAndInstall(true, true)
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
// 由于集合安装包中不包含win arm版,这将会导致arm版更新失败
|
||||
if (isWin && process.arch.includes('arm')) {
|
||||
handleSendEvent({ type: ipcMainWindowNames.update_error, info: 'failed' })
|
||||
} else {
|
||||
autoUpdater.checkForUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,281 +0,0 @@
|
||||
// Fork from https://github.com/claus/flac-metadata
|
||||
// 在 flac-metadata 的基础上修复与改进标签与封面写入逻辑
|
||||
|
||||
|
||||
const Transform = require('stream').Transform
|
||||
|
||||
const MetaDataBlock = require('./lib/MetaDataBlock')
|
||||
const MetaDataBlockStreamInfo = require('./lib/MetaDataBlockStreamInfo')
|
||||
const MetaDataBlockVorbisComment = require('./lib/MetaDataBlockVorbisComment')
|
||||
const MetaDataBlockPicture = require('./lib/MetaDataBlockPicture')
|
||||
|
||||
const STATE_IDLE = 0
|
||||
const STATE_MARKER = 1
|
||||
const STATE_MDB_HEADER = 2
|
||||
const STATE_MDB = 3
|
||||
const STATE_PASS_THROUGH = 4
|
||||
|
||||
class Processor extends Transform {
|
||||
constructor(options) {
|
||||
super(options)
|
||||
// MDB types
|
||||
this.MDB_TYPE_STREAMINFO = 0
|
||||
this.MDB_TYPE_PADDING = 1
|
||||
this.MDB_TYPE_APPLICATION = 2
|
||||
this.MDB_TYPE_SEEKTABLE = 3
|
||||
this.MDB_TYPE_VORBIS_COMMENT = 4
|
||||
this.MDB_TYPE_CUESHEET = 5
|
||||
this.MDB_TYPE_PICTURE = 6
|
||||
this.MDB_TYPE_INVALID = 127
|
||||
|
||||
this.state = STATE_IDLE
|
||||
|
||||
this.isFlac = false
|
||||
|
||||
this.buf = null
|
||||
this.bufPos = 0
|
||||
|
||||
this.mdb = null
|
||||
this.mdbLen = 0
|
||||
this.mdbLast = false
|
||||
this.mdbPush = false
|
||||
this.mdbLastWritten = false
|
||||
|
||||
this.parseMetaDataBlocks = false
|
||||
|
||||
this.waitWriteVorbis = null
|
||||
this.waitWritePicture = null
|
||||
this.tasks = 0
|
||||
|
||||
if (!(this instanceof Processor)) return new Processor(options)
|
||||
if (options && !!options.parseMetaDataBlocks) { this.parseMetaDataBlocks = true }
|
||||
}
|
||||
|
||||
writeMeta({ vorbis, picture }) {
|
||||
if (vorbis != null) {
|
||||
this.waitWriteVorbis = vorbis
|
||||
this.tasks++
|
||||
}
|
||||
if (picture != null) {
|
||||
this.waitWritePicture = picture
|
||||
this.tasks++
|
||||
}
|
||||
}
|
||||
|
||||
// clearMeta() {
|
||||
// this.mdbLastWritten = true
|
||||
// }
|
||||
|
||||
readMeta(callback) {
|
||||
this.parseMetaDataBlocks = true
|
||||
this.readCallBack = callback
|
||||
}
|
||||
|
||||
_transform(chunk, enc, done) {
|
||||
let chunkPos = 0
|
||||
let chunkLen = chunk.length
|
||||
let isChunkProcessed = false
|
||||
let _this = this
|
||||
|
||||
function _safePush(minCapacity, persist, validate) {
|
||||
let slice
|
||||
let chunkAvailable = chunkLen - chunkPos
|
||||
let isDone = (chunkAvailable + this.bufPos >= minCapacity)
|
||||
validate = (typeof validate === 'function') ? validate : function() { return true }
|
||||
if (isDone) {
|
||||
// Enough data available
|
||||
if (persist) {
|
||||
// Persist the entire block so it can be parsed
|
||||
if (this.bufPos > 0) {
|
||||
// Part of this block's data is in backup buffer, copy rest over
|
||||
chunk.copy(this.buf, this.bufPos, chunkPos, chunkPos + minCapacity - this.bufPos)
|
||||
slice = this.buf.slice(0, minCapacity)
|
||||
} else {
|
||||
// Entire block fits in current chunk
|
||||
slice = chunk.slice(chunkPos, chunkPos + minCapacity)
|
||||
}
|
||||
} else {
|
||||
slice = chunk.slice(chunkPos, chunkPos + minCapacity - this.bufPos)
|
||||
}
|
||||
// Push block after validation
|
||||
validate(slice, isDone) && _this.push(slice)
|
||||
chunkPos += minCapacity - this.bufPos
|
||||
this.bufPos = 0
|
||||
this.buf = null
|
||||
} else {
|
||||
// Not enough data available
|
||||
if (persist) {
|
||||
// Copy/append incomplete block to backup buffer
|
||||
this.buf = this.buf || Buffer.alloc(minCapacity)
|
||||
chunk.copy(this.buf, this.bufPos, chunkPos, chunkLen)
|
||||
} else {
|
||||
// Push incomplete block after validation
|
||||
slice = chunk.slice(chunkPos, chunkLen)
|
||||
validate(slice, isDone) && _this.push(slice)
|
||||
}
|
||||
this.bufPos += chunkLen - chunkPos
|
||||
}
|
||||
return isDone
|
||||
};
|
||||
let safePush = _safePush.bind(this)
|
||||
|
||||
while (!isChunkProcessed) {
|
||||
switch (this.state) {
|
||||
case STATE_IDLE:
|
||||
this.state = STATE_MARKER
|
||||
break
|
||||
case STATE_MARKER:
|
||||
if (safePush(4, true, this._validateMarker.bind(this))) {
|
||||
this.state = this.isFlac ? STATE_MDB_HEADER : STATE_PASS_THROUGH
|
||||
} else {
|
||||
isChunkProcessed = true
|
||||
}
|
||||
break
|
||||
case STATE_MDB_HEADER:
|
||||
if (safePush(4, true, this._validateMDBHeader.bind(this))) {
|
||||
this.state = STATE_MDB
|
||||
} else {
|
||||
isChunkProcessed = true
|
||||
}
|
||||
break
|
||||
case STATE_MDB:
|
||||
if (safePush(this.mdbLen, this.parseMetaDataBlocks, this._validateMDB.bind(this))) {
|
||||
if (this.mdb.isLast) {
|
||||
// This MDB has the isLast flag set to true.
|
||||
// Ignore all following MDBs.
|
||||
this.mdbLastWritten = true
|
||||
}
|
||||
this.readCallBack && this.readCallBack(this.mdb)
|
||||
if (this.mdbLast) {
|
||||
this._writeVorbisComment()
|
||||
this._writePicture()
|
||||
this.state = STATE_PASS_THROUGH
|
||||
} else {
|
||||
this.state = STATE_MDB_HEADER
|
||||
}
|
||||
} else {
|
||||
isChunkProcessed = true
|
||||
}
|
||||
break
|
||||
case STATE_PASS_THROUGH:
|
||||
safePush(chunkLen - chunkPos, false)
|
||||
isChunkProcessed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
done()
|
||||
}
|
||||
|
||||
_validateMarker(slice, isDone) {
|
||||
this.isFlac = (slice.toString('utf8', 0) === 'fLaC')
|
||||
// TODO: completely bail out if file is not a FLAC?
|
||||
return true
|
||||
}
|
||||
|
||||
_validateMDBHeader(slice, isDone) {
|
||||
// Parse MDB header
|
||||
let header = slice.readUInt32BE(0)
|
||||
let type = (header >>> 24) & 0x7f
|
||||
this.mdbLast = (((header >>> 24) & 0x80) !== 0)
|
||||
this.mdbLen = header & 0xffffff
|
||||
// Create appropriate MDB object
|
||||
// (data is injected later in _validateMDB, if parseMetaDataBlocks option is set to true)
|
||||
switch (type) {
|
||||
case this.MDB_TYPE_STREAMINFO:
|
||||
this.mdb = new MetaDataBlockStreamInfo(this.mdbLast)
|
||||
break
|
||||
case this.MDB_TYPE_VORBIS_COMMENT:
|
||||
if (this.waitWriteVorbis) {
|
||||
this._writeVorbisComment(slice, header)
|
||||
this.mdbPush = false
|
||||
return this.mdbPush
|
||||
} else {
|
||||
this.mdb = new MetaDataBlockVorbisComment(this.mdbLast)
|
||||
this.readCallback && this.readCallback(this.mdb)
|
||||
}
|
||||
break
|
||||
case this.MDB_TYPE_PICTURE:
|
||||
if (this.waitWritePicture) {
|
||||
this._writePicture(slice, header)
|
||||
this.mdbPush = false
|
||||
return this.mdbPush
|
||||
} else {
|
||||
this.mdb = new MetaDataBlockPicture(this.mdbLast)
|
||||
this.readCallback && this.readCallback(this.mdb)
|
||||
}
|
||||
break
|
||||
case this.MDB_TYPE_PADDING:
|
||||
case this.MDB_TYPE_APPLICATION:
|
||||
case this.MDB_TYPE_SEEKTABLE:
|
||||
case this.MDB_TYPE_CUESHEET:
|
||||
case this.MDB_TYPE_INVALID:
|
||||
default:
|
||||
this.mdb = new MetaDataBlock(this.mdbLast, type)
|
||||
break
|
||||
}
|
||||
|
||||
// this.emit('preprocess', this.mdb)
|
||||
|
||||
if (this.mdbLastWritten) {
|
||||
// A previous MDB had the isLast flag set to true.
|
||||
// Ignore all following MDBs.
|
||||
this.mdb.remove()
|
||||
} else {
|
||||
if (this.mdbLast && this.tasks > 0) {
|
||||
header &= 0x7fffffff
|
||||
slice.writeUInt32BE(header >>> 0, 0)
|
||||
}
|
||||
// The consumer may change the MDB's isLast flag in the preprocess handler.
|
||||
// Here that flag is updated in the MDB header.
|
||||
}
|
||||
this.mdbPush = !this.mdb.removed
|
||||
return this.mdbPush
|
||||
}
|
||||
|
||||
_validateMDB(slice, isDone) {
|
||||
// Parse the MDB if parseMetaDataBlocks option is set to true
|
||||
if (this.parseMetaDataBlocks && isDone) {
|
||||
this.mdb.parse(slice)
|
||||
}
|
||||
return this.mdbPush
|
||||
}
|
||||
|
||||
_flush(done) {
|
||||
// All chunks have been processed
|
||||
// Clean up
|
||||
this.state = STATE_IDLE
|
||||
this.mdbLastWritten = false
|
||||
this.isFlac = false
|
||||
this.bufPos = 0
|
||||
this.buf = null
|
||||
this.mdb = null
|
||||
done()
|
||||
}
|
||||
|
||||
_writeVorbisComment() {
|
||||
if (this.waitWriteVorbis == null) return
|
||||
let isLast = this.mdbLast && this.tasks === 1
|
||||
this.tasks--
|
||||
this.push(MetaDataBlockVorbisComment.create(isLast, this.waitWriteVorbis.vendor, this.waitWriteVorbis.comments).publish())
|
||||
this.waitWriteVorbis = null
|
||||
}
|
||||
|
||||
_writePicture() {
|
||||
if (this.waitWritePicture == null) return
|
||||
let isLast = this.mdbLast && this.tasks === 1
|
||||
this.tasks--
|
||||
this.push(
|
||||
MetaDataBlockPicture.create(
|
||||
isLast, this.waitWritePicture.pictureType,
|
||||
this.waitWritePicture.mimeType, this.waitWritePicture.description,
|
||||
this.waitWritePicture.width, this.waitWritePicture.height,
|
||||
this.waitWritePicture.bitsPerPixel, this.waitWritePicture.colors,
|
||||
this.waitWritePicture.pictureData,
|
||||
).publish(),
|
||||
)
|
||||
this.waitWritePicture = null
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Processor
|
||||
@@ -1,25 +0,0 @@
|
||||
class MetaDataBlock {
|
||||
constructor(isLast, type) {
|
||||
this.isLast = isLast
|
||||
this.type = type
|
||||
this.error = null
|
||||
this.hasData = false
|
||||
this.removed = false
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.removed = true
|
||||
}
|
||||
|
||||
parse(buffer) {
|
||||
}
|
||||
|
||||
toString() {
|
||||
let str = '[MetaDataBlock]'
|
||||
str += ' type: ' + this.type
|
||||
str += ', isLast: ' + this.isLast
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MetaDataBlock
|
||||
@@ -1,130 +0,0 @@
|
||||
const MetaDataBlock = require('./MetaDataBlock')
|
||||
|
||||
class MetaDataBlockPicture extends MetaDataBlock {
|
||||
constructor(isLast) {
|
||||
super(isLast, 6)
|
||||
|
||||
this.pictureType = 0
|
||||
this.mimeType = ''
|
||||
this.description = ''
|
||||
this.width = 0
|
||||
this.height = 0
|
||||
this.bitsPerPixel = 0
|
||||
this.colors = 0
|
||||
this.pictureData = null
|
||||
}
|
||||
|
||||
static create(isLast, pictureType, mimeType, description, width, height, bitsPerPixel, colors, pictureData) {
|
||||
let mdb = new MetaDataBlockPicture(isLast)
|
||||
mdb.pictureType = pictureType
|
||||
mdb.mimeType = mimeType
|
||||
mdb.description = description
|
||||
mdb.width = width
|
||||
mdb.height = height
|
||||
mdb.bitsPerPixel = bitsPerPixel
|
||||
mdb.colors = colors
|
||||
mdb.pictureData = pictureData
|
||||
mdb.hasData = true
|
||||
return mdb
|
||||
}
|
||||
|
||||
|
||||
parse(buffer) {
|
||||
try {
|
||||
let pos = 0
|
||||
|
||||
this.pictureType = buffer.readUInt32BE(pos)
|
||||
pos += 4
|
||||
|
||||
let mimeTypeLength = buffer.readUInt32BE(pos)
|
||||
this.mimeType = buffer.toString('utf8', pos + 4, pos + 4 + mimeTypeLength)
|
||||
pos += 4 + mimeTypeLength
|
||||
|
||||
let descriptionLength = buffer.readUInt32BE(pos)
|
||||
this.description = buffer.toString('utf8', pos + 4, pos + 4 + descriptionLength)
|
||||
pos += 4 + descriptionLength
|
||||
|
||||
this.width = buffer.readUInt32BE(pos)
|
||||
this.height = buffer.readUInt32BE(pos + 4)
|
||||
this.bitsPerPixel = buffer.readUInt32BE(pos + 8)
|
||||
this.colors = buffer.readUInt32BE(pos + 12)
|
||||
pos += 16
|
||||
|
||||
let pictureDataLength = buffer.readUInt32BE(pos)
|
||||
this.pictureData = Buffer.alloc(pictureDataLength)
|
||||
buffer.copy(this.pictureData, 0, pos + 4, pictureDataLength)
|
||||
|
||||
this.hasData = true
|
||||
} catch (e) {
|
||||
this.error = e
|
||||
this.hasData = false
|
||||
}
|
||||
}
|
||||
|
||||
publish() {
|
||||
let pos = 0
|
||||
let size = this.getSize()
|
||||
let buffer = Buffer.alloc(4 + size)
|
||||
|
||||
let header = size
|
||||
header |= (this.type << 24)
|
||||
header |= (this.isLast ? 0x80000000 : 0)
|
||||
buffer.writeUInt32BE(header >>> 0, pos)
|
||||
pos += 4
|
||||
|
||||
buffer.writeUInt32BE(this.pictureType, pos)
|
||||
pos += 4
|
||||
|
||||
let mimeTypeLen = Buffer.byteLength(this.mimeType)
|
||||
buffer.writeUInt32BE(mimeTypeLen, pos)
|
||||
buffer.write(this.mimeType, pos + 4)
|
||||
pos += 4 + mimeTypeLen
|
||||
|
||||
let descriptionLen = Buffer.byteLength(this.description)
|
||||
buffer.writeUInt32BE(descriptionLen, pos)
|
||||
buffer.write(this.description, pos + 4)
|
||||
pos += 4 + descriptionLen
|
||||
|
||||
buffer.writeUInt32BE(this.width, pos)
|
||||
buffer.writeUInt32BE(this.height, pos + 4)
|
||||
buffer.writeUInt32BE(this.bitsPerPixel, pos + 8)
|
||||
buffer.writeUInt32BE(this.colors, pos + 12)
|
||||
pos += 16
|
||||
|
||||
buffer.writeUInt32BE(this.pictureData.length, pos)
|
||||
this.pictureData.copy(buffer, pos + 4)
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
getSize() {
|
||||
let size = 4
|
||||
size += 4 + Buffer.byteLength(this.mimeType)
|
||||
size += 4 + Buffer.byteLength(this.description)
|
||||
size += 16
|
||||
size += 4 + this.pictureData.length
|
||||
return size
|
||||
}
|
||||
|
||||
toString() {
|
||||
let str = '[MetaDataBlockPicture]'
|
||||
str += ' type: ' + this.type
|
||||
str += ', isLast: ' + this.isLast
|
||||
if (this.error) {
|
||||
str += '\n ERROR: ' + this.error
|
||||
}
|
||||
if (this.hasData) {
|
||||
str += '\n pictureType: ' + this.pictureType
|
||||
str += '\n mimeType: ' + this.mimeType
|
||||
str += '\n description: ' + this.description
|
||||
str += '\n width: ' + this.width
|
||||
str += '\n height: ' + this.height
|
||||
str += '\n bitsPerPixel: ' + this.bitsPerPixel
|
||||
str += '\n colors: ' + this.colors
|
||||
str += '\n pictureData: ' + (this.pictureData ? this.pictureData.length : '<null>')
|
||||
}
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MetaDataBlockPicture
|
||||
@@ -1,84 +0,0 @@
|
||||
const MetaDataBlock = require('./MetaDataBlock')
|
||||
|
||||
function pad(n, width) {
|
||||
n = '' + n
|
||||
return (n.length >= width) ? n : new Array(width - n.length + 1).join('0') + n
|
||||
}
|
||||
|
||||
class MetaDataBlockStreamInfo extends MetaDataBlock {
|
||||
constructor(isLast) {
|
||||
super(isLast, 0)
|
||||
|
||||
this.minBlockSize = 0
|
||||
this.maxBlockSize = 0
|
||||
this.minFrameSize = 0
|
||||
this.maxFrameSize = 0
|
||||
this.sampleRate = 0
|
||||
this.channels = 0
|
||||
this.bitsPerSample = 0
|
||||
this.samples = 0
|
||||
this.checksum = null
|
||||
this.duration = 0
|
||||
this.durationStr = '0:00.000'
|
||||
}
|
||||
|
||||
remove() {
|
||||
console.error("WARNING: Can't remove StreamInfo block!")
|
||||
}
|
||||
|
||||
parse(buffer) {
|
||||
try {
|
||||
let pos = 0
|
||||
|
||||
this.minBlockSize = buffer.readUInt16BE(pos)
|
||||
this.maxBlockSize = buffer.readUInt16BE(pos + 2)
|
||||
this.minFrameSize = (buffer.readUInt8(pos + 4) << 16) | buffer.readUInt16BE(pos + 5)
|
||||
this.maxFrameSize = (buffer.readUInt8(pos + 7) << 16) | buffer.readUInt16BE(pos + 8)
|
||||
|
||||
let tmp = buffer.readUInt32BE(pos + 10)
|
||||
this.sampleRate = tmp >>> 12
|
||||
this.channels = (tmp >>> 9) & 0x07
|
||||
this.bitsPerSample = (tmp >>> 4) & 0x1f
|
||||
this.samples = +((tmp & 0x0f) << 4) + buffer.readUInt32BE(pos + 14)
|
||||
|
||||
this.checksum = Buffer.alloc(16)
|
||||
buffer.copy(this.checksum, 0, 18, 34)
|
||||
|
||||
this.duration = this.samples / this.sampleRate
|
||||
|
||||
let minutes = '' + Math.floor(this.duration / 60)
|
||||
let seconds = pad(Math.floor(this.duration % 60), 2)
|
||||
let milliseconds = pad(Math.round(((this.duration % 60) - Math.floor(this.duration % 60)) * 1000), 3)
|
||||
this.durationStr = minutes + ':' + seconds + '.' + milliseconds
|
||||
|
||||
this.hasData = true
|
||||
} catch (e) {
|
||||
this.error = e
|
||||
this.hasData = false
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
let str = '[MetaDataBlockStreamInfo]'
|
||||
str += ' type: ' + this.type
|
||||
str += ', isLast: ' + this.isLast
|
||||
if (this.error) {
|
||||
str += '\n ERROR: ' + this.error
|
||||
}
|
||||
if (this.hasData) {
|
||||
str += '\n minBlockSize: ' + this.minBlockSize
|
||||
str += '\n maxBlockSize: ' + this.maxBlockSize
|
||||
str += '\n minFrameSize: ' + this.minFrameSize
|
||||
str += '\n maxFrameSize: ' + this.maxFrameSize
|
||||
str += '\n samples: ' + this.samples
|
||||
str += '\n sampleRate: ' + this.sampleRate
|
||||
str += '\n channels: ' + (this.channels + 1)
|
||||
str += '\n bitsPerSample: ' + (this.bitsPerSample + 1)
|
||||
str += '\n duration: ' + this.durationStr
|
||||
str += '\n checksum: ' + (this.checksum ? this.checksum.toString('hex') : '<null>')
|
||||
}
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MetaDataBlockStreamInfo
|
||||
@@ -1,105 +0,0 @@
|
||||
const MetaDataBlock = require('./MetaDataBlock')
|
||||
|
||||
class MetaDataBlockVorbisComment extends MetaDataBlock {
|
||||
constructor(isLast) {
|
||||
super(isLast, 4)
|
||||
|
||||
this.vendor = ''
|
||||
this.comments = []
|
||||
}
|
||||
|
||||
static create(isLast, vendor, comments) {
|
||||
let mdb = new MetaDataBlockVorbisComment(isLast)
|
||||
mdb.vendor = vendor
|
||||
mdb.comments = comments
|
||||
mdb.hasData = true
|
||||
return mdb
|
||||
}
|
||||
|
||||
parse(buffer) {
|
||||
try {
|
||||
let pos = 0
|
||||
|
||||
let vendorLen = buffer.readUInt32LE(pos)
|
||||
let vendor = buffer.toString('utf8', pos + 4, pos + 4 + vendorLen)
|
||||
this.vendor = vendor
|
||||
pos += 4 + vendorLen
|
||||
|
||||
let commentCount = buffer.readUInt32LE(pos)
|
||||
pos += 4
|
||||
|
||||
while (commentCount-- > 0) {
|
||||
let commentLen = buffer.readUInt32LE(pos)
|
||||
let comment = buffer.toString('utf8', pos + 4, pos + 4 + commentLen)
|
||||
this.comments.push(comment)
|
||||
pos += 4 + commentLen
|
||||
}
|
||||
|
||||
this.hasData = true
|
||||
} catch (e) {
|
||||
this.error = e
|
||||
this.hasData = false
|
||||
}
|
||||
}
|
||||
|
||||
publish() {
|
||||
let pos = 0
|
||||
let size = this.getSize()
|
||||
let buffer = Buffer.alloc(4 + size)
|
||||
|
||||
let header = size
|
||||
header |= (this.type << 24)
|
||||
header |= (this.isLast ? 0x80000000 : 0)
|
||||
buffer.writeUInt32BE(header >>> 0, pos)
|
||||
pos += 4
|
||||
let vendorLen = Buffer.byteLength(this.vendor)
|
||||
buffer.writeUInt32LE(vendorLen, pos)
|
||||
buffer.write(this.vendor, pos + 4)
|
||||
pos += 4 + vendorLen
|
||||
|
||||
let commentCount = this.comments.length
|
||||
buffer.writeUInt32LE(commentCount, pos)
|
||||
pos += 4
|
||||
|
||||
for (let i = 0; i < commentCount; i++) {
|
||||
let comment = this.comments[i]
|
||||
let commentLen = Buffer.byteLength(comment)
|
||||
buffer.writeUInt32LE(commentLen, pos)
|
||||
buffer.write(comment, pos + 4)
|
||||
pos += 4 + commentLen
|
||||
}
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
getSize() {
|
||||
let size = 8 + Buffer.byteLength(this.vendor)
|
||||
for (let i = 0; i < this.comments.length; i++) {
|
||||
size += 4 + Buffer.byteLength(this.comments[i])
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
toString() {
|
||||
let str = '[MetaDataBlockVorbisComment]'
|
||||
str += ' type: ' + this.type
|
||||
str += ', isLast: ' + this.isLast
|
||||
if (this.error) {
|
||||
str += '\n ERROR: ' + this.error
|
||||
}
|
||||
if (this.hasData) {
|
||||
str += '\n vendor: ' + this.vendor
|
||||
if (this.comments.length) {
|
||||
str += '\n comments:'
|
||||
for (let i = 0; i < this.comments.length; i++) {
|
||||
str += '\n ' + this.comments[i].split('=').join(': ')
|
||||
}
|
||||
} else {
|
||||
str += '\n comments: none'
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MetaDataBlockVorbisComment
|
||||
@@ -1,95 +0,0 @@
|
||||
const fs = require('fs')
|
||||
const fsPromises = fs.promises
|
||||
const path = require('path')
|
||||
const getImgSize = require('image-size')
|
||||
const request = require('request')
|
||||
|
||||
const FlacProcessor = require('./flac-metadata')
|
||||
|
||||
const extReg = /^(\.(?:jpe?g|png)).*$/
|
||||
const vendor = 'reference libFLAC 1.2.1 20070917'
|
||||
|
||||
const writeMeta = async(filePath, meta, picPath) => {
|
||||
const comments = Object.keys(meta).map(key => `${key.toUpperCase()}=${meta[key] || ''}`)
|
||||
const data = {
|
||||
vorbis: {
|
||||
vendor,
|
||||
comments,
|
||||
},
|
||||
}
|
||||
if (picPath) {
|
||||
const apicData = await fsPromises.readFile(picPath)
|
||||
let imgSize = getImgSize(apicData)
|
||||
let mime_type
|
||||
let bitsPerPixel
|
||||
if (apicData[0] == 0xff && apicData[1] == 0xd8 && apicData[2] == 0xff) {
|
||||
mime_type = 'image/jpeg'
|
||||
bitsPerPixel = 24
|
||||
} else {
|
||||
mime_type = 'image/png'
|
||||
bitsPerPixel = 32
|
||||
}
|
||||
data.picture = {
|
||||
pictureType: 3,
|
||||
mimeType: mime_type,
|
||||
description: '',
|
||||
width: imgSize.width,
|
||||
height: imgSize.height,
|
||||
bitsPerPixel,
|
||||
colors: 0,
|
||||
pictureData: apicData,
|
||||
}
|
||||
}
|
||||
|
||||
const reader = fs.createReadStream(filePath)
|
||||
const tempPath = filePath + '.lxmtemp'
|
||||
const writer = fs.createWriteStream(tempPath)
|
||||
const flacProcessor = new FlacProcessor()
|
||||
flacProcessor.writeMeta(data)
|
||||
|
||||
reader.pipe(flacProcessor).pipe(writer).on('finish', () => {
|
||||
fs.unlink(filePath, err => {
|
||||
if (err) return console.log(err.message)
|
||||
fs.rename(tempPath, filePath, err => {
|
||||
if (err) console.log(err.message)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = (filePath, meta) => {
|
||||
if (!meta.APIC) return writeMeta(filePath, meta)
|
||||
const picUrl = meta.APIC
|
||||
delete meta.APIC
|
||||
if (!/^http/.test(picUrl)) {
|
||||
return writeMeta(filePath, meta)
|
||||
}
|
||||
let ext = path.extname(picUrl)
|
||||
let picPath = filePath.replace(/\.flac$/, '') + (ext ? ext.replace(extReg, '$1') : '.jpg')
|
||||
|
||||
request(picUrl)
|
||||
.on('response', respones => {
|
||||
if (respones.statusCode !== 200 && respones.statusCode != 206) return writeMeta(filePath, meta)
|
||||
respones
|
||||
.pipe(fs.createWriteStream(picPath))
|
||||
.on('finish', async() => {
|
||||
if (respones.complete) {
|
||||
await writeMeta(filePath, meta, picPath)
|
||||
} else {
|
||||
writeMeta(filePath, meta)
|
||||
}
|
||||
fs.unlink(picPath, err => {
|
||||
if (err) console.log(err.message)
|
||||
})
|
||||
})
|
||||
.on('error', err => {
|
||||
if (err) console.log(err.message)
|
||||
writeMeta(filePath, meta)
|
||||
})
|
||||
})
|
||||
.on('error', err => {
|
||||
if (err) console.log(err.message)
|
||||
writeMeta(filePath, meta)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
// exports.getAvailableFontFamilies = getAvailableFontFamilies
|
||||
|
||||
|
||||
const fontList = require('font-list')
|
||||
import { getFonts } from 'font-list'
|
||||
|
||||
exports.getFonts = fontList.getFonts
|
||||
export {
|
||||
getFonts,
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
const { windowSizeList } = require('../../common/config')
|
||||
const { objectDeepMerge, throttle, initSetting } = require('../../common/utils')
|
||||
const getStore = require('@common/store')
|
||||
|
||||
exports.getWindowSizeInfo = ({ windowSizeId = 1 } = {}) => {
|
||||
return windowSizeList.find(i => i.id === windowSizeId) || windowSizeList[0]
|
||||
}
|
||||
|
||||
exports.getAppSetting = () => {
|
||||
return getStore('config').get('setting')
|
||||
}
|
||||
|
||||
exports.getAppHotKeyConfig = () => {
|
||||
const electronStore_hotKey = getStore('hotKey')
|
||||
|
||||
return {
|
||||
global: electronStore_hotKey.get('global'),
|
||||
local: electronStore_hotKey.get('local'),
|
||||
}
|
||||
}
|
||||
const saveHotKeyConfig = throttle(config => {
|
||||
for (const key of Object.keys(config)) {
|
||||
global.appHotKey.config[key] = config[key]
|
||||
getStore('hotKey').set(key, config[key])
|
||||
}
|
||||
})
|
||||
exports.saveAppHotKeyConfig = config => {
|
||||
saveHotKeyConfig(config)
|
||||
}
|
||||
|
||||
// const saveSetting = throttle(n => {
|
||||
// electronStore_config.set('setting', n)
|
||||
// })
|
||||
exports.updateSetting = (settings) => {
|
||||
objectDeepMerge(global.appSetting, settings)
|
||||
getStore('config').set('setting', global.appSetting)
|
||||
exports.initSetting(false)
|
||||
}
|
||||
exports.initSetting = (isShowErrorAlert = true) => {
|
||||
const info = initSetting(isShowErrorAlert)
|
||||
global.appSetting = info.setting
|
||||
global.appSettingVersion = info.version
|
||||
}
|
||||
|
||||
exports.openDevTools = webContents => {
|
||||
webContents.openDevTools({
|
||||
mode: 'undocked',
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
import { throttle } from '@common/utils'
|
||||
import migrateSetting from '@common/utils/migrateSetting'
|
||||
import getStore from '@main/utils/store'
|
||||
import { STORE_NAMES, URL_SCHEME_RXP } from '@common/constants'
|
||||
import defaultSetting from '@common/defaultSetting'
|
||||
import defaultHotKey from '@common/defaultHotKey'
|
||||
import { migrateDataJson, migrateHotKey, migrateUserApi, parseDataFile } from './migrate'
|
||||
import { commonColorNames, commonLightColorValues, commonDarkColorValues } from '@common/config'
|
||||
import { nativeTheme } from 'electron'
|
||||
import { joinPath } from '@common/utils/nodejs'
|
||||
import themes from '@common/theme/index.json'
|
||||
|
||||
export const parseEnvParams = (): { cmdParams: LX.CmdParams, deeplink: string | null } => {
|
||||
const cmdParams: LX.CmdParams = {}
|
||||
let deeplink = null
|
||||
const rx = /^-\w+/
|
||||
for (let param of process.argv) {
|
||||
if (URL_SCHEME_RXP.test(param)) {
|
||||
deeplink = param
|
||||
}
|
||||
|
||||
if (!rx.test(param)) continue
|
||||
param = param.substring(1)
|
||||
let index = param.indexOf('=')
|
||||
if (index < 0) {
|
||||
cmdParams[param] = true
|
||||
} else {
|
||||
cmdParams[param.substring(0, index)] = param.substring(index + 1)
|
||||
}
|
||||
}
|
||||
return {
|
||||
cmdParams,
|
||||
deeplink,
|
||||
}
|
||||
}
|
||||
|
||||
const primitiveType = ['string', 'boolean', 'number']
|
||||
const checkPrimitiveType = (val: any): boolean => val === null || primitiveType.includes(typeof val)
|
||||
// const handleMergeSetting = (defaultSetting: LX.AppSetting, currentSetting: Partial<LX.AppSetting>) => {
|
||||
// const updatedSettingKeys: Array<keyof LX.AppSetting> = []
|
||||
// for (const key of Object.keys(defaultSetting) as Array<keyof LX.AppSetting>) {
|
||||
// const currentValue: any = currentSetting[key]
|
||||
// const isPrimitive = checkPrimitiveType(currentValue)
|
||||
// // if (checkPrimitiveType(value)) {
|
||||
// if (!isPrimitive) continue
|
||||
// updatedSettingKeys.push(key)
|
||||
// // @ts-expect-error
|
||||
// defaultSetting[key] = currentValue
|
||||
// // } else {
|
||||
// // if (!isPrimitive && currentValue != undefined) handleMergeSetting(value, currentValue)
|
||||
// // }
|
||||
// }
|
||||
// return {
|
||||
// setting: defaultSetting,
|
||||
// updatedSettingKeys,
|
||||
// }
|
||||
// }
|
||||
|
||||
export const mergeSetting = (originSetting: LX.AppSetting, targetSetting?: Partial<LX.AppSetting> | null): {
|
||||
setting: LX.AppSetting
|
||||
updatedSettingKeys: Array<keyof LX.AppSetting>
|
||||
updatedSetting: Partial<LX.AppSetting>
|
||||
} => {
|
||||
let originSettingCopy: LX.AppSetting = JSON.parse(JSON.stringify(originSetting))
|
||||
// const defaultVersion = targetSettingCopy.version
|
||||
const updatedSettingKeys: Array<keyof LX.AppSetting> = []
|
||||
const updatedSetting: Partial<LX.AppSetting> = {}
|
||||
|
||||
if (targetSetting) {
|
||||
const originSettingKeys = Object.keys(originSettingCopy)
|
||||
const targetSettingKeys = Object.keys(targetSetting)
|
||||
|
||||
if (originSettingKeys.length > targetSettingKeys.length) {
|
||||
for (const key of targetSettingKeys as Array<keyof LX.AppSetting>) {
|
||||
const targetValue: any = targetSetting[key]
|
||||
const isPrimitive = checkPrimitiveType(targetValue)
|
||||
// if (checkPrimitiveType(value)) {
|
||||
if (!isPrimitive || targetValue == originSettingCopy[key] || originSettingCopy[key] === undefined) continue
|
||||
updatedSettingKeys.push(key)
|
||||
updatedSetting[key] = targetValue
|
||||
// @ts-expect-error
|
||||
originSettingCopy[key] = targetValue
|
||||
// } else {
|
||||
// if (!isPrimitive && currentValue != undefined) handleMergeSetting(value, currentValue)
|
||||
// }
|
||||
}
|
||||
} else {
|
||||
for (const key of originSettingKeys as Array<keyof LX.AppSetting>) {
|
||||
const targetValue: any = targetSetting[key]
|
||||
const isPrimitive = checkPrimitiveType(targetValue)
|
||||
// if (checkPrimitiveType(value)) {
|
||||
if (!isPrimitive || targetValue == originSettingCopy[key]) continue
|
||||
updatedSettingKeys.push(key)
|
||||
updatedSetting[key] = targetValue
|
||||
// @ts-expect-error
|
||||
originSettingCopy[key] = targetValue
|
||||
// } else {
|
||||
// if (!isPrimitive && currentValue != undefined) handleMergeSetting(value, currentValue)
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
setting: originSettingCopy,
|
||||
updatedSettingKeys,
|
||||
updatedSetting,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const updateSetting = (setting?: Partial<LX.AppSetting>, isInit: boolean = false) => {
|
||||
const electronStore_config = getStore(STORE_NAMES.APP_SETTINGS)
|
||||
|
||||
let originSetting: LX.AppSetting
|
||||
if (isInit) {
|
||||
if (setting) setting = migrateSetting(setting)
|
||||
originSetting = defaultSetting
|
||||
} else originSetting = global.lx.appSetting
|
||||
|
||||
const result = mergeSetting(originSetting, setting)
|
||||
|
||||
result.setting.version = defaultSetting.version
|
||||
|
||||
electronStore_config.set({ version: result.setting.version, setting: result.setting })
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化设置
|
||||
*/
|
||||
export const initSetting = async() => {
|
||||
const electronStore_config = getStore(STORE_NAMES.APP_SETTINGS)
|
||||
|
||||
let setting = electronStore_config.get('setting') as LX.AppSetting | undefined
|
||||
|
||||
// migrate setting
|
||||
if (!setting) {
|
||||
const config = await parseDataFile<{ setting?: any }>('config.json')
|
||||
if (config?.setting) setting = config.setting as LX.AppSetting
|
||||
await migrateUserApi()
|
||||
await migrateDataJson()
|
||||
}
|
||||
|
||||
// console.log(setting)
|
||||
return updateSetting(setting, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化快捷键设置
|
||||
*/
|
||||
export const initHotKey = async() => {
|
||||
const electronStore_hotKey = getStore(STORE_NAMES.HOTKEY)
|
||||
|
||||
let localConfig = electronStore_hotKey.get('local') as LX.HotKeyConfig | null
|
||||
let globalConfig = electronStore_hotKey.get('global') as LX.HotKeyConfig | null
|
||||
|
||||
if (!localConfig) {
|
||||
// migrate hotKey
|
||||
const config = await migrateHotKey()
|
||||
if (config) {
|
||||
localConfig = config.local
|
||||
globalConfig = config.global
|
||||
} else {
|
||||
localConfig = JSON.parse(JSON.stringify(defaultHotKey.global))
|
||||
globalConfig = JSON.parse(JSON.stringify(defaultHotKey.local))
|
||||
}
|
||||
|
||||
electronStore_hotKey.set('local', localConfig)
|
||||
electronStore_hotKey.set('global', globalConfig)
|
||||
}
|
||||
|
||||
return {
|
||||
local: localConfig as LX.HotKeyConfig,
|
||||
global: globalConfig as LX.HotKeyConfig,
|
||||
}
|
||||
}
|
||||
|
||||
type HotKeyType = 'local' | 'global'
|
||||
|
||||
const saveHotKeyConfig = throttle<[LX.HotKeyConfigAll]>((config: LX.HotKeyConfigAll) => {
|
||||
for (const key of Object.keys(config) as HotKeyType[]) {
|
||||
global.lx.hotKey.config[key] = config[key]
|
||||
getStore(STORE_NAMES.HOTKEY).set(key, config[key])
|
||||
}
|
||||
})
|
||||
export const saveAppHotKeyConfig = (config: LX.HotKeyConfigAll) => {
|
||||
saveHotKeyConfig(config)
|
||||
}
|
||||
|
||||
export const openDevTools = (webContents: Electron.WebContents) => {
|
||||
webContents.openDevTools({
|
||||
mode: 'undocked',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
let userThemes: LX.Theme[]
|
||||
export const getAllThemes = () => {
|
||||
if (!userThemes) userThemes = getStore(STORE_NAMES.THEME).get('themes') as LX.Theme[] | null ?? []
|
||||
return {
|
||||
themes,
|
||||
userThemes,
|
||||
dataPath: joinPath(global.lxDataPath, 'theme_images'),
|
||||
}
|
||||
}
|
||||
|
||||
export const saveTheme = (theme: LX.Theme) => {
|
||||
const targetTheme = userThemes.find(t => t.id === theme.id)
|
||||
if (targetTheme) Object.assign(targetTheme, theme)
|
||||
else userThemes.push(theme)
|
||||
getStore(STORE_NAMES.THEME).set('themes', userThemes)
|
||||
}
|
||||
|
||||
export const removeTheme = (id: string) => {
|
||||
const index = userThemes.findIndex(t => t.id === id)
|
||||
if (index < 0) return
|
||||
userThemes.splice(index, 1)
|
||||
getStore(STORE_NAMES.THEME).set('themes', userThemes)
|
||||
}
|
||||
|
||||
const copyTheme = (theme: LX.Theme): LX.Theme => {
|
||||
return {
|
||||
...theme,
|
||||
config: {
|
||||
...theme.config,
|
||||
extInfo: { ...theme.config.extInfo },
|
||||
themeColors: { ...theme.config.themeColors },
|
||||
},
|
||||
}
|
||||
}
|
||||
export const getTheme = () => {
|
||||
// fs.promises.readdir()
|
||||
const shouldUseDarkColors = nativeTheme.shouldUseDarkColors
|
||||
let themeId = global.lx.appSetting['theme.id'] == 'auto'
|
||||
? shouldUseDarkColors
|
||||
? global.lx.appSetting['theme.darkId']
|
||||
: global.lx.appSetting['theme.lightId']
|
||||
: global.lx.appSetting['theme.id']
|
||||
// themeId = 'naruto'
|
||||
// themeId = 'pink'
|
||||
// themeId = 'black'
|
||||
let theme = themes.find(theme => theme.id == themeId)
|
||||
if (!theme) {
|
||||
userThemes = getStore(STORE_NAMES.THEME).get('themes') as LX.Theme[] | null ?? []
|
||||
theme = userThemes.find(theme => theme.id == themeId)
|
||||
if (theme) {
|
||||
if (theme.config.extInfo['--background-image'] != 'none') {
|
||||
theme = copyTheme(theme)
|
||||
theme.config.extInfo['--background-image'] = `url(file:///${joinPath(global.lxDataPath, 'theme_images', theme.config.extInfo['--background-image']).replaceAll('\\', '/')})`
|
||||
}
|
||||
} else {
|
||||
themeId = global.lx.appSetting['theme.id'] == 'auto' && shouldUseDarkColors ? 'black' : 'green'
|
||||
theme = themes.find(theme => theme.id == themeId) as LX.Theme
|
||||
}
|
||||
}
|
||||
|
||||
const colors: Record<string, string> = {
|
||||
...theme.config.themeColors,
|
||||
...theme.config.extInfo,
|
||||
}
|
||||
|
||||
const vals = theme.isDark ? commonDarkColorValues : commonLightColorValues
|
||||
for (let i = commonColorNames.length; i--;) {
|
||||
colors[commonColorNames[i]] = vals[i]
|
||||
}
|
||||
|
||||
return {
|
||||
shouldUseDarkColors,
|
||||
theme: {
|
||||
id: global.lx.appSetting['theme.id'],
|
||||
name: theme.name,
|
||||
isDark: theme.isDark,
|
||||
colors,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import fs from 'node:fs'
|
||||
import { checkPath, joinPath } from '@common/utils/nodejs'
|
||||
import { log } from '@common/utils'
|
||||
import { toNewMusicInfo } from '@common/utils/tools'
|
||||
import { APP_EVENT_NAMES, STORE_NAMES } from '@common/constants'
|
||||
|
||||
/**
|
||||
* 读取配置文件
|
||||
* @returns
|
||||
*/
|
||||
export const parseDataFile = async<T>(name: string): Promise<T | null> => {
|
||||
const path = joinPath(global.lxOldDataPath, name)
|
||||
if (await checkPath(path)) {
|
||||
try {
|
||||
return JSON.parse((await fs.promises.readFile(path)).toString())
|
||||
} catch (err) {
|
||||
log.error(err)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
interface OldUserListInfo {
|
||||
name: string
|
||||
id: string
|
||||
source?: LX.OnlineSource
|
||||
sourceListId?: string
|
||||
locationUpdateTime?: number
|
||||
list: any[]
|
||||
}
|
||||
/**
|
||||
* 迁移 v2.0.0 之前的 list data
|
||||
* @returns
|
||||
*/
|
||||
export const migrateDBData = async() => {
|
||||
let playList = await parseDataFile<{ defaultList?: { list: any[] }, loveList?: { list: any[] }, tempList?: { list: any[] }, userList?: OldUserListInfo[] }>('playList.json')
|
||||
let listDataAll: LX.List.ListDataFull = {
|
||||
defaultList: [],
|
||||
loveList: [],
|
||||
userList: [],
|
||||
tempList: [],
|
||||
}
|
||||
let isRequiredSave = false
|
||||
if (playList) {
|
||||
if (playList.defaultList) listDataAll.defaultList = playList.defaultList.list.map(m => toNewMusicInfo(m))
|
||||
if (playList.loveList) listDataAll.loveList = playList.loveList.list.map(m => toNewMusicInfo(m))
|
||||
if (playList.tempList) listDataAll.tempList = playList.tempList.list.map(m => toNewMusicInfo(m))
|
||||
if (playList.userList) {
|
||||
listDataAll.userList = playList.userList.map(l => {
|
||||
return {
|
||||
...l,
|
||||
locationUpdateTime: l.locationUpdateTime ?? null,
|
||||
list: l.list.map(m => toNewMusicInfo(m)),
|
||||
}
|
||||
})
|
||||
}
|
||||
isRequiredSave = true
|
||||
} else {
|
||||
const config = await parseDataFile<{ list?: { defaultList?: any[], loveList?: any[] } }>('config.json')
|
||||
if (config?.list) {
|
||||
const list = config.list
|
||||
if (list.defaultList) listDataAll.defaultList = list.defaultList.map(m => toNewMusicInfo(m))
|
||||
if (list.loveList) listDataAll.loveList = list.loveList.map(m => toNewMusicInfo(m))
|
||||
isRequiredSave = true
|
||||
}
|
||||
}
|
||||
if (isRequiredSave) await global.lx.worker.dbService.listDataOverwrite(listDataAll)
|
||||
|
||||
const lyricData = await parseDataFile<Record<string, LX.Music.LyricInfo>>('lyrics_edited.json')
|
||||
if (lyricData) {
|
||||
for await (const [id, info] of Object.entries(lyricData)) {
|
||||
await global.lx.worker.dbService.editedLyricAdd(id, info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 迁移文件
|
||||
const migrateFile = async(name: string, targetName: string) => {
|
||||
let path = joinPath(global.lxDataPath, targetName)
|
||||
let oldPath = joinPath(global.lxOldDataPath, name)
|
||||
if (!await checkPath(path) && await checkPath(oldPath)) {
|
||||
await fs.promises.copyFile(oldPath, path).catch(err => {
|
||||
log.error(err)
|
||||
}).catch(err => log.error(err))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移 v2.0.0 之前的 data.json
|
||||
* @returns
|
||||
*/
|
||||
export const migrateDataJson = async() => {
|
||||
const path = joinPath(global.lxDataPath, 'data.json')
|
||||
if (await checkPath(path)) return
|
||||
const oldDataFile = await parseDataFile<{
|
||||
searchHistoryList?: string[]
|
||||
playInfo?: any
|
||||
listPrevSelectId?: any
|
||||
listPosition?: any
|
||||
listUpdateInfo?: any
|
||||
}>('data.json')
|
||||
if (!oldDataFile) return
|
||||
const newData: any = {}
|
||||
if (oldDataFile.searchHistoryList) newData.searchHistoryList = oldDataFile.searchHistoryList
|
||||
if (oldDataFile.playInfo) newData.playInfo = oldDataFile.playInfo
|
||||
if (oldDataFile.listPrevSelectId) newData.listPrevSelectId = oldDataFile.listPrevSelectId
|
||||
if (oldDataFile.listPosition) newData.listScrollPosition = oldDataFile.listPosition
|
||||
if (oldDataFile.listUpdateInfo) newData.listUpdateInfo = oldDataFile.listUpdateInfo
|
||||
|
||||
await fs.promises.writeFile(path, JSON.stringify(newData)).catch(err => log.error(err))
|
||||
}
|
||||
|
||||
|
||||
const hotKeyNameMap = {
|
||||
mainWindow: APP_EVENT_NAMES.winMainName,
|
||||
winLyric: APP_EVENT_NAMES.winLyricName,
|
||||
} as const
|
||||
const updateHotKeyTypeName = (config: LX.HotKeyConfig) => {
|
||||
for (const keyConfig of Object.values(config.keys)) {
|
||||
if (hotKeyNameMap[keyConfig.type as keyof typeof hotKeyNameMap]) keyConfig.type = hotKeyNameMap[keyConfig.type as keyof typeof hotKeyNameMap]
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 迁移 v2.0.0 之前的 hotkey
|
||||
* @returns
|
||||
*/
|
||||
export const migrateHotKey = async() => {
|
||||
const oldConfig = await parseDataFile<LX.HotKeyConfigAll>('hotKey.json')
|
||||
if (oldConfig) {
|
||||
let localConfig: LX.HotKeyConfig
|
||||
let globalConfig: LX.HotKeyConfig
|
||||
updateHotKeyTypeName(oldConfig.local)
|
||||
updateHotKeyTypeName(oldConfig.global)
|
||||
|
||||
localConfig = oldConfig.local
|
||||
globalConfig = oldConfig.global
|
||||
|
||||
// 移除v1.0.1及之前设置的全局声音媒体快捷键接管
|
||||
if (globalConfig.keys.VolumeUp) {
|
||||
delete globalConfig.keys.VolumeUp
|
||||
delete globalConfig.keys.VolumeDown
|
||||
delete globalConfig.keys.VolumeMute
|
||||
}
|
||||
return {
|
||||
local: localConfig,
|
||||
global: globalConfig,
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移 v2.0.0 之前的user api
|
||||
* @returns
|
||||
*/
|
||||
export const migrateUserApi = async() => migrateFile('userApi.json', STORE_NAMES.USER_API + '.json')
|
||||
@@ -1,60 +0,0 @@
|
||||
const NodeID3 = require('node-id3')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const request = require('request')
|
||||
const extReg = /^(\.(?:jpe?g|png)).*$/
|
||||
|
||||
const handleWriteMeta = (meta, filePath) => {
|
||||
if (meta.lyrics) {
|
||||
meta.unsynchronisedLyrics = {
|
||||
language: 'zho',
|
||||
text: meta.lyrics,
|
||||
}
|
||||
delete meta.lyrics
|
||||
}
|
||||
NodeID3.write(meta, filePath)
|
||||
}
|
||||
|
||||
module.exports = (filePath, meta) => {
|
||||
if (!meta.APIC) return handleWriteMeta(meta, filePath)
|
||||
if (!/^http/.test(meta.APIC)) {
|
||||
delete meta.APIC
|
||||
return handleWriteMeta(meta, filePath)
|
||||
}
|
||||
let ext = path.extname(meta.APIC)
|
||||
let picPath = filePath.replace(/\.mp3$/, '') + (ext ? ext.replace(extReg, '$1') : '.jpg')
|
||||
request(meta.APIC)
|
||||
.on('response', respones => {
|
||||
if (respones.statusCode !== 200 && respones.statusCode != 206) {
|
||||
delete meta.APIC
|
||||
handleWriteMeta(meta, filePath)
|
||||
return
|
||||
}
|
||||
respones
|
||||
.pipe(fs.createWriteStream(picPath))
|
||||
.on('finish', () => {
|
||||
if (respones.complete) {
|
||||
meta.APIC = picPath
|
||||
handleWriteMeta(meta, filePath)
|
||||
} else {
|
||||
delete meta.APIC
|
||||
handleWriteMeta(meta, filePath)
|
||||
}
|
||||
fs.unlink(picPath, err => {
|
||||
if (err) console.log(err.message)
|
||||
})
|
||||
}).on('error', err => {
|
||||
if (err) console.log(err.message)
|
||||
delete meta.APIC
|
||||
handleWriteMeta(meta, filePath)
|
||||
fs.unlink(picPath, err => {
|
||||
if (err) console.log(err.message)
|
||||
})
|
||||
})
|
||||
})
|
||||
.on('error', err => {
|
||||
if (err) console.log(err.message)
|
||||
delete meta.APIC
|
||||
handleWriteMeta(meta, filePath)
|
||||
})
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
const path = require('path')
|
||||
const mp3Meta = require('./mp3Meta')
|
||||
const flacMeta = require('./flacMeta')
|
||||
|
||||
exports.setMeta = (filePath, meta) => {
|
||||
switch (path.extname(filePath)) {
|
||||
case '.mp3':
|
||||
mp3Meta(filePath, meta)
|
||||
break
|
||||
case '.flac':
|
||||
flacMeta(filePath, meta)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
todo.md
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-debug.log
|
||||
yarn-error.log
|
||||
package-lock.json
|
||||
tsconfig.tsbuildinfo
|
||||
report.*.json
|
||||
|
||||
.eslintcache
|
||||
.DS_Store
|
||||
.idea
|
||||
.vscode
|
||||
.yarn
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
@@ -1,22 +0,0 @@
|
||||
|
||||
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.
|
||||
@@ -1,10 +0,0 @@
|
||||
# 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
@@ -1,2 +0,0 @@
|
||||
export function simplify(source: string): string
|
||||
export function tranditionalize(source: string): string
|
||||
@@ -1,30 +0,0 @@
|
||||
const { simplified, traditional } = require('./chinese')
|
||||
|
||||
const stMap = new Map()
|
||||
const tsMap = new Map()
|
||||
|
||||
simplified.split('').forEach((char, index) => {
|
||||
stMap.set(char, traditional[index])
|
||||
tsMap.set(traditional[index], char)
|
||||
})
|
||||
|
||||
function simplify(source) {
|
||||
let result = []
|
||||
for (const char of source) {
|
||||
result.push(tsMap.get(char) || char)
|
||||
}
|
||||
return result.join('')
|
||||
}
|
||||
|
||||
function tranditionalize(source) {
|
||||
let result = []
|
||||
for (const char of source) {
|
||||
result.push(stMap.get(char) || char)
|
||||
}
|
||||
return result.join('')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
simplify,
|
||||
tranditionalize,
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "simplify-chinese",
|
||||
"description": "Convert chinese characters between simplified form and tranditional form 汉字简繁体转换工具",
|
||||
"version": "1.1.0",
|
||||
"main": "index.js",
|
||||
"typings": "index.d.ts",
|
||||
"repository": "https://github.com/koishijs/simplify-chinese.git",
|
||||
"author": "Shigma <1700011071@pku.edu.cn>",
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import Store from 'electron-store'
|
||||
import { dialog, shell } from 'electron'
|
||||
import { join } from 'path'
|
||||
import fs from 'fs'
|
||||
import log from 'electron-log'
|
||||
|
||||
interface Stores {
|
||||
[key: string]: Store
|
||||
}
|
||||
|
||||
const stores: Stores = {}
|
||||
|
||||
/**
|
||||
* 获取 Store 对象
|
||||
* @param {string} name store 名
|
||||
* @param {boolean|undefined} isIgnoredError 是否忽略错误
|
||||
* @param {boolean|undefined} isShowErrorAlert=true 是否显示错误弹窗
|
||||
* @returns Store
|
||||
*/
|
||||
export default (name: string, isIgnoredError = true, isShowErrorAlert = true): Store => {
|
||||
if (stores[name]) return stores[name]
|
||||
let store: Store
|
||||
try {
|
||||
store = stores[name] = new Store({ name, clearInvalidConfig: false, cwd: global.lxDataPath })
|
||||
} catch (err: any) {
|
||||
const error = err as Error
|
||||
log.error(error)
|
||||
|
||||
if (!isIgnoredError) throw error
|
||||
|
||||
|
||||
const backPath = join(global.lxDataPath, name + '.json.bak')
|
||||
fs.copyFileSync(join(global.lxDataPath, name + '.json'), backPath)
|
||||
if (isShowErrorAlert) {
|
||||
dialog.showMessageBoxSync({
|
||||
type: 'error',
|
||||
message: name + ' data load error',
|
||||
detail: `We have helped you back up the old ${name} file to: ${backPath}\nYou can try to repair and restore it manually\n\nError detail: ${error.message}`,
|
||||
})
|
||||
shell.showItemInFolder(backPath)
|
||||
}
|
||||
|
||||
|
||||
store = new Store({ name, clearInvalidConfig: true })
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
export {
|
||||
Store,
|
||||
}
|
||||
Reference in New Issue
Block a user