Files
Melo/src/renderer/utils/index.js
T
2021-12-03 22:11:11 +08:00

486 lines
13 KiB
JavaScript

import fs from 'fs'
import path from 'path'
import { shell, clipboard } from 'electron'
import crypto from 'crypto'
import { rendererSend, rendererInvoke, NAMES } from '@common/ipc'
import iconv from 'iconv-lite'
import { gzip, gunzip } from 'zlib'
import { proxy, qualityList } from '@renderer/core/share'
/**
* 获取两个数之间的随机整数,大于等于min,小于max
* @param {*} min
* @param {*} max
*/
export const getRandom = (min, max) => Math.floor(Math.random() * (max - min)) + min
export const sizeFormate = size => {
// https://gist.github.com/thomseddon/3511330
if (!size) return '0 B'
let units = ['B', 'KB', 'MB', 'GB', 'TB']
let number = Math.floor(Math.log(size) / Math.log(1024))
return `${(size / Math.pow(1024, Math.floor(number))).toFixed(2)} ${units[number]}`
}
/**
* 日期格式化
* @param {*} date 时间
* @param {String} format 时间格式,默认YYYY-MM-DD hh:mm:ss
*/
export const dateFormat = (date = new Date(), format = 'YYYY-MM-DD hh:mm:ss') => {
if (typeof date != 'object') date = new Date(date)
const munFix = (n) => n < 10 ? ('0' + n) : n
return format
.replace('YYYY', date.getFullYear())
.replace('MM', munFix(date.getMonth() + 1))
.replace('DD', munFix(date.getDate()))
.replace('hh', munFix(date.getHours()))
.replace('mm', munFix(date.getMinutes()))
.replace('ss', munFix(date.getSeconds()))
}
/**
* 时间格式化
*/
export const dateFormat2 = time => {
let differ = parseInt((Date.now() - time) / 1000)
if (differ < 60) {
return window.i18n.t('date_format_second', { num: differ })
} else if (differ < 3600) {
return window.i18n.t('date_format_minute', { num: parseInt(differ / 60) })
} else if (differ < 86400) {
return window.i18n.t('date_format_hour', { num: parseInt(differ / 3600) })
} else {
return dateFormat(time)
}
}
export const formatPlayTime = time => {
let m = parseInt(time / 60)
let s = parseInt(time % 60)
return m === 0 && s === 0 ? '--/--' : (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s)
}
export const formatPlayTime2 = time => {
let m = parseInt(time / 60)
let s = parseInt(time % 60)
return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s)
}
export const b64DecodeUnicode = str => {
// Going backwards: from bytestream, to percent-encoding, to original string.
return decodeURIComponent(window.atob(str).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
}).join(''))
}
const encodeNames = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&apos;': "'",
'&#039;': "'",
}
export const decodeName = (str = '') => str?.replace(/(?:&amp;|&lt;|&gt;|&quot;|&apos;|&#039;)/gm, s => encodeNames[s]) || ''
const easeInOutQuad = (t, b, c, d) => {
t /= d / 2
if (t < 1) return (c / 2) * t * t + b
t--
return (-c / 2) * (t * (t - 2) - 1) + b
}
const handleScroll = (element, to, duration = 300, fn = () => {}) => {
if (!element) return fn()
const start = element.scrollTop || element.scrollY || 0
let cancel = false
if (to > start) {
let maxScrollTop = element.scrollHeight - element.clientHeight
if (to > maxScrollTop) to = maxScrollTop
} else if (to < start) {
if (to < 0) to = 0
} else return fn()
const change = to - start
const increment = 10
if (!change) return fn()
let currentTime = 0
let val
const animateScroll = () => {
currentTime += increment
val = parseInt(easeInOutQuad(currentTime, start, change, duration))
if (element.scrollTo) {
element.scrollTo(0, val)
} else {
element.scrollTop = val
}
if (currentTime < duration) {
if (cancel) return fn()
setTimeout(animateScroll, increment)
} else {
fn()
}
}
animateScroll()
return () => {
cancel = true
}
}
/**
* 设置滚动条位置
* @param {*} element 要设置滚动的容器 dom
* @param {*} to 滚动的目标位置
* @param {*} duration 滚动完成时间 ms
* @param {*} fn 滚动完成后的回调
* @param {*} delay 延迟执行时间
*/
export const scrollTo = (element, to, duration = 300, fn = () => {}, delay = 0) => {
let cancelFn
let timeout
if (delay) {
let scrollCancelFn
cancelFn = () => {
timeout == null ? scrollCancelFn && scrollCancelFn() : clearTimeout(timeout)
}
timeout = setTimeout(() => {
timeout = null
scrollCancelFn = handleScroll(element, to, duration, fn, delay)
}, delay)
} else {
cancelFn = handleScroll(element, to, duration, fn, delay)
}
return cancelFn
}
/**
* 检查路径是否存在
* @param {*} path 路径
*/
export const checkPath = (path) => new Promise(resolve => {
fs.access(path, fs.constants.F_OK, err => {
if (err) return resolve(false)
resolve(true)
})
})
/**
* 选择路径
* @param {*} 选项
*/
export const selectDir = options => rendererInvoke(NAMES.mainWindow.select_dir, options)
/**
* 打开保存对话框
* @param {*} 选项
*/
export const openSaveDir = options => rendererInvoke(NAMES.mainWindow.show_save_dialog, options)
/**
* 在资源管理器中打开目录
* @param {*} dir
*/
export const openDirInExplorer = dir => {
shell.showItemInFolder(dir)
}
// https://stackoverflow.com/a/53387532
export const compareVer = (currentVer, targetVer) => {
// treat non-numerical characters as lower version
// replacing them with a negative number based on charcode of each character
const fix = s => `.${s.toLowerCase().charCodeAt(0) - 2147483647}.`
currentVer = ('' + currentVer).replace(/[^0-9.]/g, fix).split('.')
targetVer = ('' + targetVer).replace(/[^0-9.]/g, fix).split('.')
let c = Math.max(currentVer.length, targetVer.length)
for (let i = 0; i < c; i++) {
// convert to integer the most efficient way
currentVer[i] = ~~currentVer[i]
targetVer[i] = ~~targetVer[i]
if (currentVer[i] > targetVer[i]) return 1
else if (currentVer[i] < targetVer[i]) return -1
}
return 0
}
export const isObject = item => item && typeof item === 'object' && !Array.isArray(item)
/**
* 对象深度合并
* @param {} target 要合并源对象
* @param {} source 要合并目标对象
*/
export const objectDeepMerge = (target, source, mergedObj) => {
if (!mergedObj) {
mergedObj = new Set()
mergedObj.add(target)
}
let base = {}
Object.keys(source).forEach(item => {
if (isObject(source[item])) {
if (mergedObj.has(source[item])) return
if (!isObject(target[item])) target[item] = {}
mergedObj.add(source[item])
objectDeepMerge(target[item], source[item], mergedObj)
return
}
base[item] = source[item]
})
Object.assign(target, base)
}
/**
* 在浏览器打开URL
* @param {*} url
*/
export const openUrl = url => {
if (!/^https?:\/\//.test(url)) return
shell.openExternal(url)
}
/**
* 设置标题
*/
let dom_title = document.getElementsByTagName('title')[0]
export const setTitle = (title = '洛雪音乐助手') => {
dom_title.innerText = title
}
/**
* 创建 MD5 hash
* @param {*} str
*/
export const toMD5 = str => crypto.createHash('md5').update(str).digest('hex')
/**
* 复制文本到剪贴板
* @param {*} str
*/
export const clipboardWriteText = str => clipboard.writeText(str)
/**
* 从剪贴板读取文本
* @param {*} str
*/
export const clipboardReadText = str => clipboard.readText()
/**
* 设置音频 meta 信息
* @param {*} filePath
* @param {*} meta
*/
export const setMeta = (filePath, meta) => {
rendererSend(NAMES.mainWindow.set_music_meta, { filePath, meta })
}
/**
* 保存歌词文件
* @param {*} filePath
* @param {*} lrc
* @param {*} format
*/
export const saveLrc = (filePath, lrc, format) => {
switch (format) {
case 'gbk':
fs.writeFile(filePath, iconv.encode(lrc, 'gbk', { addBOM: true }), err => {
if (err) console.log(err)
})
break
case 'utf8':
default:
fs.writeFile(filePath, lrc, 'utf8', err => {
if (err) console.log(err)
})
break
}
}
/**
* 生成节流函数
* @param {*} fn
* @param {*} delay
*/
export const throttle = (fn, delay = 100) => {
let timer = null
let _args = null
return function(...args) {
_args = args
if (timer) return
timer = setTimeout(() => {
timer = null
fn.apply(this, _args)
}, delay)
}
}
/**
* 生成防抖函数
* @param {*} fn
* @param {*} delay
*/
export const debounce = (fn, delay = 100) => {
let timer = null
let _args = null
return function(...args) {
_args = args
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
timer = null
fn.apply(this, _args)
}, delay)
}
}
const async_removeItem = (arr, num, callback) => window.requestAnimationFrame(() => {
let len = arr.length
if (len > num) {
arr.splice(0, num)
return async_removeItem(arr, num, callback)
} else {
arr.splice(0, len)
return callback()
}
})
const async_addItem = (arr, newArr, num, callback) => window.requestAnimationFrame(() => {
let len = newArr.length
if (len > num) {
arr.push(...newArr.splice(0, num))
return async_addItem(arr, newArr, num, callback)
} else {
arr.push(...newArr.splice(0, len))
return callback()
}
})
/**
* 异步设置数组
* @param {*} from 原数组
* @param {*} to 设置后的数组内容
* @param {*} num 每次设置的个数
*/
export const asyncSetArray = (from, to, num = 100) => new Promise(resolve => {
async_removeItem(from, num, () => {
async_addItem(from, Array.from(to), num, () => {
resolve()
})
})
})
/**
* 获取缓存大小
*/
export const getCacheSize = () => rendererInvoke(NAMES.mainWindow.get_cache_size)
/**
* 清除缓存
*/
export const clearCache = () => rendererInvoke(NAMES.mainWindow.clear_cache)
/**
* 设置窗口大小
* @param {*} width
* @param {*} height
*/
export const setWindowSize = (width, height) => rendererSend(NAMES.mainWindow.set_window_size, { width, height })
export const getProxyInfo = () => proxy.enable && proxy.host
? `http://${proxy.username}:${proxy.password}@${proxy.host}:${proxy.port};`
: undefined
export const assertApiSupport = source => qualityList.value[source] != undefined
export const getSetting = () => rendererInvoke(NAMES.mainWindow.get_setting)
export const saveSetting = setting => rendererInvoke(NAMES.mainWindow.set_app_setting, setting)
export const getPlayList = () => rendererInvoke(NAMES.mainWindow.get_playlist).catch(error => {
rendererInvoke(NAMES.mainWindow.get_data_path).then(dataPath => {
let filePath = path.join(dataPath, 'playList.json.bak')
rendererInvoke(NAMES.mainWindow.show_dialog, {
type: 'error',
message: window.i18n.t('store.state.load_list_file_error_title'),
detail: window.i18n.t('store.state.load_list_file_error_detail', {
path: filePath,
detail: error.message,
}),
}).then(() => openDirInExplorer(filePath))
})
return rendererInvoke(NAMES.mainWindow.get_playlist, true)
})
// 解析URL参数为对象
export const parseUrlParams = str => {
const params = {}
if (typeof str !== 'string') return params
const paramsArr = str.split('&')
for (const param of paramsArr) {
let [key, value] = param.split('=')
params[key] = value
}
return params
}
export const getLyric = musicInfo => rendererInvoke(NAMES.mainWindow.get_lyric, `${musicInfo.source}_${musicInfo.songmid}`)
export const setLyric = (musicInfo, { lyric, tlyric, lxlyric }) => rendererSend(NAMES.mainWindow.save_lyric, {
id: `${musicInfo.source}_${musicInfo.songmid}`,
lyrics: { lyric, tlyric, lxlyric },
})
export const clearLyric = () => rendererSend(NAMES.mainWindow.clear_lyric)
export const getMusicUrl = (musicInfo, type) => rendererInvoke(NAMES.mainWindow.get_music_url, `${musicInfo.source}_${musicInfo.songmid}_${type}`)
export const setMusicUrl = (musicInfo, type, url) => rendererSend(NAMES.mainWindow.save_music_url, {
id: `${musicInfo.source}_${musicInfo.songmid}_${type}`,
url,
})
export const clearMusicUrl = () => rendererSend(NAMES.mainWindow.clear_music_url)
export const gzipData = str => {
return new Promise((resolve, reject) => {
gzip(str, (err, result) => {
if (err) return reject(err)
resolve(result)
})
})
}
export const gunzipData = buf => {
return new Promise((resolve, reject) => {
gunzip(buf, (err, result) => {
if (err) return reject(err)
resolve(result.toString())
})
})
}
export const saveLxConfigFile = async(path, data) => {
if (!path.endsWith('.lxmc')) path += '.lxmc'
fs.writeFile(path, await gzipData(JSON.stringify(data)), 'binary', err => {
console.log(err)
})
}
export const readLxConfigFile = async path => {
let isJSON = path.endsWith('.json')
let data = await fs.promises.readFile(path, isJSON ? 'utf8' : 'binary')
if (!data || isJSON) return data
data = await gunzipData(Buffer.from(data, 'binary'))
data = JSON.parse(data.toString('utf8'))
// 修复v1.14.0出现的导出数据被序列化两次的问题
if (typeof data != 'object') {
try {
data = JSON.parse(data)
} catch (err) {
return data
}
}
return data
}
const fileNameRxp = /[\\/:*?#"<>|]/g
export const filterFileName = name => name.replace(fileNameRxp, '')