This commit is contained in:
lyswhut
2022-10-29 11:36:35 +08:00
parent 66b965d9f7
commit e45cee6596
715 changed files with 57096 additions and 31771 deletions
-130
View File
@@ -1,130 +0,0 @@
module.exports = {
windowSizeList: [
{
id: 0,
name: 'smaller',
width: 828,
height: 530,
fontSize: '14px',
},
{
id: 1,
name: 'small',
width: 920,
height: 590,
fontSize: '16px',
},
{
id: 2,
name: 'medium',
width: 1018,
height: 650,
fontSize: '16px',
},
{
id: 3,
name: 'big',
width: 1114,
height: 708,
fontSize: '17px',
},
{
id: 4,
name: 'larger',
width: 1202,
height: 766,
fontSize: '17px',
},
{
id: 5,
name: 'oversized',
width: 1382,
height: 886,
fontSize: '18px',
},
{
id: 6,
name: 'huge',
width: 1686,
height: 1062,
fontSize: '19px',
},
],
navigationUrlWhiteList: [
],
themes: [
{
id: 0,
name: '绿意盎然',
className: 'green',
},
{
id: 1,
name: '蓝田生玉',
className: 'blue',
},
{
id: 2,
name: '信口雌黄',
className: 'yellow',
},
{
id: 3,
name: '橙黄橘绿',
className: 'orange',
},
{
id: 4,
name: '热情似火',
className: 'red',
},
{
id: 10,
name: '粉装玉琢',
className: 'pink',
},
{
id: 5,
name: '重斤球紫',
className: 'purple',
},
{
id: 6,
name: '灰常美丽',
className: 'grey',
},
{
id: 11,
name: '青出于黑',
className: 'ming',
},
{
id: 12,
name: '青出于黑',
className: 'blue2',
},
{
id: 13,
name: '黑灯瞎火',
className: 'black',
},
{
id: 7,
name: '月里嫦娥',
className: 'mid_autumn',
},
{
id: 8,
name: '木叶之村',
className: 'naruto',
},
{
id: 9,
name: '新年快乐',
className: 'happy_new_year',
},
],
themeLights: [0, 1, 2, 3, 4, 10, 5, 6, 11, 12, 7, 8, 9],
themeDarks: [13, 7],
}
+86
View File
@@ -0,0 +1,86 @@
export interface WindowSize {
id: number
name: string
width: number
height: number
}
export const windowSizeList: WindowSize[] = [
{
id: 0,
name: 'smaller',
width: 828,
height: 540,
},
{
id: 1,
name: 'small',
width: 920,
height: 600,
},
{
id: 2,
name: 'medium',
width: 1020,
height: 660,
},
{
id: 3,
name: 'big',
width: 1114,
height: 718,
},
{
id: 4,
name: 'larger',
width: 1202,
height: 776,
},
{
id: 5,
name: 'oversized',
width: 1385,
height: 896,
},
{
id: 6,
name: 'huge',
width: 1700,
height: 1070,
},
]
export const navigationUrlWhiteList: RegExp[] = []
// 基础黑白色
export const commonColorNames = [
'--color-000', '--color-050', '--color-100', '--color-200', '--color-300', '--color-400',
'--color-500', '--color-600', '--color-700', '--color-800', '--color-900',
] as const
export const commonLightColorValues = [
'rgb(255, 255, 255)',
'rgb(217,217,217)',
'rgb(184,184,184)',
'rgb(156,156,156)',
'rgb(133,133,133)',
'rgb(113,113,113)',
'rgb(96,96,96)',
'rgb(82,82,82)',
'rgb(70,70,70)',
'rgb(60,60,60)',
'rgb(51,51,51)',
] as const
export const commonDarkColorValues = [
'rgb(11, 11, 11)',
'rgb(60,60,60)',
'rgb(99,99,99)',
'rgb(130,130,130)',
'rgb(155,155,155)',
'rgb(175,175,175)',
'rgb(191,191,191)',
'rgb(204,204,204)',
'rgb(214,214,214)',
'rgb(222,222,222)',
'rgb(229,229,229)',
] as const
+74
View File
@@ -0,0 +1,74 @@
export const URL_SCHEME_RXP = /^lxmusic:\/\//
export const STORE_NAMES = {
APP_SETTINGS: 'config_v2',
DATA: 'data',
SYNC: 'sync',
HOTKEY: 'hot_key',
USER_API: 'user_api',
LRC_RAW: 'lyrics',
LRC_EDITED: 'lyrics_edited',
THEME: 'theme',
} as const
export const APP_EVENT_NAMES = {
winMainName: 'win_main',
winLyricName: 'win_lyric',
trayName: 'tray',
} as const
export const LIST_IDS = {
DEFAULT: 'default',
LOVE: 'love',
TEMP: 'temp',
DOWNLOAD: 'download',
} as const
export const DATA_KEYS = {
viewPrevState: 'viewPrevState',
playInfo: 'playInfo',
searchHistoryList: 'searchHistoryList',
listScrollPosition: 'listScrollPosition',
listPrevSelectId: 'listPrevSelectId',
listUpdateInfo: 'listUpdateInfo',
ignoreVersion: 'ignoreVersion',
leaderboardSetting: 'leaderboardSetting',
songListSetting: 'songListSetting',
searchSetting: 'searchSetting',
} as const
export const DEFAULT_SETTING = {
leaderboard: {
source: 'kw',
boardId: 'kw__16',
},
songList: {
source: 'kg',
sortId: '5',
tagId: '',
},
search: {
temp_source: 'kw',
source: 'all',
type: 'music',
},
viewPrevState: {
url: '/search',
query: {},
},
}
export const DOWNLOAD_STATUS = {
RUN: 'run',
WAITING: 'waiting',
PAUSE: 'pause',
ERROR: 'error',
COMPLETED: 'completed',
} as const
export const QUALITYS = ['flac24bit', 'flac', 'wav', 'ape', '320k', '192k', '128k'] as const
-89
View File
@@ -1,89 +0,0 @@
const { player: hotKeyPlayer, common: hotKeyCommon, desktop_lyric: hotKeyDesktopLyric } = require('./hotKey')
module.exports = {
local: {
enable: true,
keys: {
'mod+f5': {
type: hotKeyPlayer.toggle_play.type,
name: hotKeyPlayer.toggle_play.name,
action: hotKeyPlayer.toggle_play.action,
},
'mod+arrowleft': {
type: hotKeyPlayer.prev.type,
name: hotKeyPlayer.prev.name,
action: hotKeyPlayer.prev.action,
},
'mod+arrowright': {
type: hotKeyPlayer.next.type,
name: hotKeyPlayer.next.name,
action: hotKeyPlayer.next.action,
},
f1: {
type: hotKeyCommon.focusSearchInput.type,
name: hotKeyCommon.focusSearchInput.name,
action: hotKeyCommon.focusSearchInput.action,
},
},
},
global: {
enable: false,
keys: {
MediaPlayPause: {
type: hotKeyPlayer.toggle_play.type,
name: null,
action: hotKeyPlayer.toggle_play.action,
},
MediaPreviousTrack: {
type: hotKeyPlayer.prev.type,
name: null,
action: hotKeyPlayer.prev.action,
},
MediaNextTrack: {
type: hotKeyPlayer.next.type,
name: null,
action: hotKeyPlayer.next.action,
},
'mod+alt+f5': {
type: hotKeyPlayer.toggle_play.type,
name: hotKeyPlayer.toggle_play.name,
action: hotKeyPlayer.toggle_play.action,
},
'mod+alt+arrowleft': {
type: hotKeyPlayer.prev.type,
name: hotKeyPlayer.prev.name,
action: hotKeyPlayer.prev.action,
},
'mod+alt+arrowright': {
type: hotKeyPlayer.next.type,
name: hotKeyPlayer.next.name,
action: hotKeyPlayer.next.action,
},
'mod+alt+arrowup': {
type: hotKeyPlayer.volume_up.type,
name: hotKeyPlayer.volume_up.name,
action: hotKeyPlayer.volume_up.action,
},
'mod+alt+arrowdown': {
type: hotKeyPlayer.volume_down.type,
name: hotKeyPlayer.volume_down.name,
action: hotKeyPlayer.volume_down.action,
},
'mod+alt+0': {
type: hotKeyDesktopLyric.toggle_visible.type,
name: hotKeyDesktopLyric.toggle_visible.name,
action: hotKeyDesktopLyric.toggle_visible.action,
},
'mod+alt+-': {
type: hotKeyDesktopLyric.toggle_lock.type,
name: hotKeyDesktopLyric.toggle_lock.name,
action: hotKeyDesktopLyric.toggle_lock.action,
},
'mod+alt+=': {
type: hotKeyDesktopLyric.toggle_always_top.type,
name: hotKeyDesktopLyric.toggle_always_top.name,
action: hotKeyDesktopLyric.toggle_always_top.action,
},
},
},
}
+93
View File
@@ -0,0 +1,93 @@
import { HOTKEY_PLAYER, HOTKEY_COMMON, HOTKEY_DESKTOP_LYRIC } from './hotKey'
const local: LX.HotKeyConfig = {
enable: true,
keys: {
'mod+f5': {
type: HOTKEY_PLAYER.toggle_play.type,
name: HOTKEY_PLAYER.toggle_play.name,
action: HOTKEY_PLAYER.toggle_play.action,
},
'mod+arrowleft': {
type: HOTKEY_PLAYER.prev.type,
name: HOTKEY_PLAYER.prev.name,
action: HOTKEY_PLAYER.prev.action,
},
'mod+arrowright': {
type: HOTKEY_PLAYER.next.type,
name: HOTKEY_PLAYER.next.name,
action: HOTKEY_PLAYER.next.action,
},
f1: {
type: HOTKEY_COMMON.focusSearchInput.type,
name: HOTKEY_COMMON.focusSearchInput.name,
action: HOTKEY_COMMON.focusSearchInput.action,
},
},
}
const global: LX.HotKeyConfig = {
enable: false,
keys: {
MediaPlayPause: {
type: HOTKEY_PLAYER.toggle_play.type,
name: '',
action: HOTKEY_PLAYER.toggle_play.action,
},
MediaPreviousTrack: {
type: HOTKEY_PLAYER.prev.type,
name: '',
action: HOTKEY_PLAYER.prev.action,
},
MediaNextTrack: {
type: HOTKEY_PLAYER.next.type,
name: '',
action: HOTKEY_PLAYER.next.action,
},
'mod+alt+f5': {
type: HOTKEY_PLAYER.toggle_play.type,
name: HOTKEY_PLAYER.toggle_play.name,
action: HOTKEY_PLAYER.toggle_play.action,
},
'mod+alt+arrowleft': {
type: HOTKEY_PLAYER.prev.type,
name: HOTKEY_PLAYER.prev.name,
action: HOTKEY_PLAYER.prev.action,
},
'mod+alt+arrowright': {
type: HOTKEY_PLAYER.next.type,
name: HOTKEY_PLAYER.next.name,
action: HOTKEY_PLAYER.next.action,
},
'mod+alt+arrowup': {
type: HOTKEY_PLAYER.volume_up.type,
name: HOTKEY_PLAYER.volume_up.name,
action: HOTKEY_PLAYER.volume_up.action,
},
'mod+alt+arrowdown': {
type: HOTKEY_PLAYER.volume_down.type,
name: HOTKEY_PLAYER.volume_down.name,
action: HOTKEY_PLAYER.volume_down.action,
},
'mod+alt+0': {
type: HOTKEY_DESKTOP_LYRIC.toggle_visible.type,
name: HOTKEY_DESKTOP_LYRIC.toggle_visible.name,
action: HOTKEY_DESKTOP_LYRIC.toggle_visible.action,
},
'mod+alt+-': {
type: HOTKEY_DESKTOP_LYRIC.toggle_lock.type,
name: HOTKEY_DESKTOP_LYRIC.toggle_lock.name,
action: HOTKEY_DESKTOP_LYRIC.toggle_lock.action,
},
'mod+alt+=': {
type: HOTKEY_DESKTOP_LYRIC.toggle_always_top.type,
name: HOTKEY_DESKTOP_LYRIC.toggle_always_top.name,
action: HOTKEY_DESKTOP_LYRIC.toggle_always_top.action,
},
},
}
export default {
local,
global,
}
-142
View File
@@ -1,142 +0,0 @@
const path = require('path')
const os = require('os')
const defaultSetting = {
version: '1.0.59',
player: {
togglePlayMethod: 'listLoop',
highQuality: false,
isShowTaskProgess: true,
volume: 1,
isMute: false,
mediaDeviceId: 'default',
isMediaDeviceRemovedStopPlay: false,
isShowLyricTranslation: false,
isShowLyricRoma: false,
isS2t: false, // 是否将歌词从简体转换为繁体
isPlayLxlrc: true,
isSavePlayTime: false,
audioVisualization: false,
waitPlayEndStop: true,
waitPlayEndStopTime: '',
autoSkipOnError: true,
},
playDetail: {
isZoomActiveLrc: true,
isShowLyricProgressSetting: false,
style: {
fontSize: 100,
align: 'center',
},
},
desktopLyric: {
enable: false,
isLock: false,
isAlwaysOnTop: false,
isAlwaysOnTopLoop: false,
width: 450,
height: 300,
x: null,
y: null,
theme: 0,
isLockScreen: true,
isDelayScroll: true,
isHoverHide: false,
style: {
font: '',
fontSize: 120,
opacity: 95,
isZoomActiveLrc: true,
},
},
list: {
isClickPlayList: false,
isShowAlbumName: true,
isShowSource: true,
isSaveScrollLocation: true,
addMusicLocationType: 'top',
},
download: {
enable: false,
savePath: path.join(os.homedir(), 'Desktop'),
fileName: '歌名 - 歌手',
maxDownloadNum: 3,
isDownloadLrc: false,
lrcFormat: 'utf8',
isEmbedPic: true,
isEmbedLyric: false,
isUseOtherSource: false,
},
leaderboard: {
source: 'kw',
tabId: 'kw__16',
},
songList: {
source: 'kg',
sortId: '5',
tagInfo: {
name: '默认',
id: null,
},
},
odc: {
isAutoClearSearchInput: false,
isAutoClearSearchList: false,
},
search: {
searchSource: 'all',
tempSearchSource: 'kw',
isShowHotSearch: false,
isShowHistorySearch: false,
isFocusSearchBox: false,
},
network: {
proxy: {
enable: false,
host: '',
port: '',
username: '',
password: '',
},
},
tray: {
isShow: false,
isToTray: false,
themeId: 0,
},
sync: {
enable: false,
port: '23332',
},
windowSizeId: 2,
startInFullscreen: false,
theme: {
id: 0,
lightId: 0,
darkId: 13,
},
langId: null,
sourceId: 'kw',
apiSource: 'temp',
sourceNameType: 'alias',
font: '',
isShowAnimation: true,
randomAnimate: true,
ignoreVersion: null,
isAgreePact: false,
controlBtnPosition: process.platform === 'darwin' ? 'left' : 'right',
}
const overwriteSetting = {
}
// 使用新年皮肤
if (new Date().getMonth() < 2) {
defaultSetting.theme.id = 9
defaultSetting.desktopLyric.theme = 3
}
exports.defaultSetting = defaultSetting
exports.overwriteSetting = overwriteSetting
+120
View File
@@ -0,0 +1,120 @@
import { join } from 'path'
import { homedir } from 'os'
const defaultSetting: LX.AppSetting = {
version: '2.0.0',
'common.windowSizeId': 2,
'common.fontSize': 16,
'common.startInFullscreen': false,
'common.langId': null,
'common.apiSource': 'temp',
'common.sourceNameType': 'alias',
'common.font': '',
'common.isShowAnimation': true,
'common.randomAnimate': true,
'common.isAgreePact': false,
'common.controlBtnPosition': process.platform === 'darwin' ? 'left' : 'right',
'player.startupAutoPlay': false,
'player.togglePlayMethod': 'listLoop',
'player.highQuality': false,
'player.isShowTaskProgess': true,
'player.volume': 1,
'player.isMute': false,
'player.mediaDeviceId': 'default',
'player.isMediaDeviceRemovedStopPlay': false,
'player.isShowLyricTranslation': false,
'player.isShowLyricRoma': false,
'player.isS2t': false,
'player.isPlayLxlrc': false,
'player.isSavePlayTime': false,
'player.audioVisualization': false,
'player.waitPlayEndStop': true,
'player.waitPlayEndStopTime': '',
'player.autoSkipOnError': true,
'playDetail.isZoomActiveLrc': false,
'playDetail.isShowLyricProgressSetting': false,
'playDetail.style.fontSize': 100,
'playDetail.style.align': 'center',
'desktopLyric.enable': false,
'desktopLyric.isLock': false,
'desktopLyric.isAlwaysOnTop': false,
'desktopLyric.isAlwaysOnTopLoop': false,
'desktopLyric.audioVisualization': false,
'desktopLyric.width': 450,
'desktopLyric.height': 300,
'desktopLyric.x': null,
'desktopLyric.y': null,
'desktopLyric.isLockScreen': true,
'desktopLyric.isDelayScroll': true,
'desktopLyric.isHoverHide': false,
'desktopLyric.direction': 'horizontal',
'desktopLyric.style.align': 'center',
'desktopLyric.style.font': '',
'desktopLyric.style.fontSize': 20,
'desktopLyric.style.lyricUnplayColor': 'rgba(255, 255, 255, 1)',
'desktopLyric.style.lyricPlayedColor': 'rgba(7, 197, 86, 1)',
'desktopLyric.style.lyricShadowColor': 'rgba(0, 0, 0, 0.14)',
// 'desktopLyric.style.fontWeight': false,
'desktopLyric.style.opacity': 95,
'desktopLyric.style.ellipsis': false,
'desktopLyric.style.isZoomActiveLrc': true,
'list.isClickPlayList': false,
'list.isShowSource': true,
'list.isSaveScrollLocation': true,
'list.addMusicLocationType': 'top',
'list.actionButtonsVisible': false,
'download.enable': false,
'download.savePath': join(homedir(), 'Desktop'),
'download.fileName': '歌名 - 歌手',
'download.maxDownloadNum': 3,
'download.isDownloadLrc': false,
'download.isDownloadTLrc': false,
'download.isDownloadRLrc': false,
'download.lrcFormat': 'utf8',
'download.isEmbedPic': true,
'download.isEmbedLyric': false,
'download.isUseOtherSource': false,
'search.isShowHotSearch': false,
'search.isShowHistorySearch': false,
'search.isFocusSearchBox': false,
'network.proxy.enable': false,
'network.proxy.host': '',
'network.proxy.port': '',
'network.proxy.username': '',
'network.proxy.password': '',
'tray.enable': false,
// 'tray.isToTray': false,
'tray.themeId': 0,
'sync.enable': false,
'sync.port': '23332',
'theme.id': 'blue_plus',
// 'theme.id': 'green',
'theme.lightId': 'green',
'theme.darkId': 'black',
'odc.isAutoClearSearchInput': false,
'odc.isAutoClearSearchList': false,
}
// 使用新年皮肤
if (new Date().getMonth() < 2) {
defaultSetting['theme.id'] = 'happy_new_year'
defaultSetting['desktopLyric.style.lyricPlayedColor'] = 'rgba(255, 18, 34, 1)'
}
export default defaultSetting
+8 -2
View File
@@ -1,6 +1,12 @@
const { log } = require('./utils')
import { log } from './utils'
process.on('uncaughtException', function(err) {
const ignoreErrorMessage = [
'Possible side-effect in debug-evaluate',
'Unexpected end of input',
]
process.on('uncaughtException', err => {
if (ignoreErrorMessage.includes(err.message)) return
console.error('An uncaught error occurred!')
console.error(err)
log.error(err)
+33 -16
View File
@@ -1,84 +1,101 @@
const names = require('@main/events/_name')
import { APP_EVENT_NAMES } from './constants'
const keyName = {
common: APP_EVENT_NAMES.winMainName,
player: APP_EVENT_NAMES.winMainName,
desktop_lyric: APP_EVENT_NAMES.winLyricName,
}
const hotKey = {
common: {
min: {
name: 'min',
action: 'min',
type: '',
},
min_toggle: {
name: 'toggle_min',
action: 'toggle_min',
type: '',
},
hide_toggle: {
name: 'toggle_hide',
action: 'toggle_hide',
type: '',
},
close: {
name: 'toggle_close',
action: 'toggle_close',
type: '',
},
focusSearchInput: {
name: 'focus_search_input',
action: 'focus_search_input',
type: '',
},
},
player: {
toggle_play: {
name: 'toggle_play',
action: 'toggle_play',
type: '',
},
next: {
name: 'next',
action: 'next',
type: '',
},
prev: {
name: 'prev',
action: 'prev',
type: '',
},
volume_up: {
name: 'volume_up',
action: 'volume_up',
type: '',
},
volume_down: {
name: 'volume_down',
action: 'volume_down',
type: '',
},
volume_mute: {
name: 'volume_mute',
action: 'volume_mute',
type: '',
},
},
desktop_lyric: {
toggle_visible: {
name: 'toggle_visible',
action: 'toggle_visible',
type: '',
},
toggle_lock: {
name: 'toggle_lock',
action: 'toggle_lock',
type: '',
},
toggle_always_top: {
name: 'toggle_always_top',
action: 'toggle_always_top',
type: '',
},
},
}
const keyName = {
common: names.mainWindow.name,
player: names.mainWindow.name,
desktop_lyric: names.winLyric.name,
}
for (const type of Object.keys(hotKey)) {
let item = hotKey[type]
for (const key of Object.keys(item)) {
item[key].action = `${type}_${item[key].action}`
item[key].name = `${type}_${item[key].name}`
item[key].type = keyName[type]
for (const type of Object.keys(hotKey) as Array<keyof typeof hotKey>) {
let keys = hotKey[type]
for (const key of Object.keys(keys) as Array<keyof typeof keys>) {
const keyInfo: LX.HotKey = keys[key]
keyInfo.action = `${type}_${keyInfo.action}`
keyInfo.name = `${type}_${keyInfo.name}`
keyInfo.type = keyName[type] as keyof typeof hotKey
}
}
exports.common = hotKey.common
exports.player = hotKey.player
exports.desktop_lyric = hotKey.desktop_lyric
export const HOTKEY_COMMON = hotKey.common
export const HOTKEY_PLAYER = hotKey.player
export const HOTKEY_DESKTOP_LYRIC = hotKey.desktop_lyric
-52
View File
@@ -1,52 +0,0 @@
const { ipcMain, ipcRenderer } = require('electron')
const names = require('./ipcNames')
exports.mainOn = (name, callback) => {
ipcMain.on(name, callback)
}
exports.mainOnce = (name, callback) => {
ipcMain.once(name, callback)
}
exports.mainOff = (name, callback) => {
ipcMain.removeListener(name, callback)
}
exports.mainOffAll = name => {
ipcMain.removeAllListeners(name)
}
exports.mainHandle = (name, callback) => {
ipcMain.handle(name, callback)
}
exports.mainHandleOnce = (name, callback) => {
ipcMain.handleOnce(name, callback)
}
exports.mainHandleRemove = name => {
ipcMain.removeListener(name)
}
exports.mainSend = (window, name, params) => {
window.webContents.send(name, params)
}
exports.rendererSend = (name, params) => {
ipcRenderer.send(name, params)
}
exports.rendererSendSync = (name, params) => ipcRenderer.sendSync(name, params)
exports.rendererInvoke = (name, params) => ipcRenderer.invoke(name, params)
exports.rendererOn = (name, callback) => {
ipcRenderer.on(name, callback)
}
exports.rendererOnce = (name, callback) => {
ipcRenderer.once(name, callback)
}
exports.rendererOff = (name, callback) => {
ipcRenderer.removeListener(name, callback)
}
exports.rendererOffAll = name => {
ipcRenderer.removeAllListeners(name)
}
exports.NAMES = names
-126
View File
@@ -1,126 +0,0 @@
const names = {
mainWindow: {
focus: 'focus',
close: 'close',
min: 'min',
max: 'max',
fullscreen: 'fullscreen',
set_app_name: 'set_app_name',
clear_cache: 'clear_cache',
get_cache_size: 'get_cache_size',
get_env_params: 'get_env_params',
clear_env_params_deeplink: 'clear_env_params_deeplink',
wait: 'wait',
wait_cancel: 'wait_cancel',
interval: 'interval',
interval_callback: 'interval_callback',
interval_cancel: 'interval_cancel',
open_dev_tools: 'open_dev_tools',
system_theme_change: 'system_theme_change',
set_music_meta: 'set_music_meta',
progress: 'progress',
change_tray: 'change_tray',
quit_update: 'quit_update',
update_available: 'update_available',
update_error: 'update_error',
update_progress: 'update_progress',
update_downloaded: 'update_downloaded',
update_not_available: 'update_not_available',
set_ignore_mouse_events: 'set_ignore_mouse_events',
set_app_setting: 'set_app_setting',
set_window_size: 'set_window_size',
show_save_dialog: 'show_save_dialog',
get_system_fonts: 'get_system_fonts',
handle_request: 'handle_request',
cancel_request: 'cancel_request',
handle_xm_verify_open: 'handle_xm_verify_open',
handle_xm_verify_close: 'handle_xm_verify_close',
select_dir: 'select_dir',
restart_window: 'restart_window',
lang_s2t: 'lang_s2t',
handle_kw_decode_lyric: 'handle_kw_decode_lyric',
get_lyric_info: 'get_lyric_info',
set_lyric_info: 'set_lyric_info',
set_config: 'set_config',
set_hot_key_config: 'set_hot_key_config',
key_down: 'key_down',
quit: 'quit',
min_toggle: 'min_toggle',
hide_toggle: 'hide_toggle',
get_data_path: 'get_data_path',
show_dialog: 'show_dialog',
get_setting: 'get_setting',
get_playlist: 'get_playlist',
save_playlist: 'save_playlist',
get_data: 'get_data',
save_data: 'save_data',
get_hot_key: 'get_hot_key',
import_user_api: 'import_user_api',
remove_user_api: 'remove_user_api',
set_user_api: 'set_user_api',
get_user_api_list: 'get_user_api_list',
request_user_api: 'request_user_api',
request_user_api_cancel: 'request_user_api_cancel',
get_user_api_status: 'get_user_api_status',
user_api_status: 'user_api_status',
user_api_show_update_alert: 'user_api_show_update_alert',
user_api_set_allow_update_alert: 'user_api_set_allow_update_alert',
get_lyric: 'get_lyric',
save_lyric: 'save_lyric',
clear_lyric: 'clear_lyric',
get_lyric_raw: 'get_lyric_raw',
save_lyric_raw: 'save_lyric_raw',
clear_lyric_raw: 'clear_lyric_raw',
get_lyric_edited: 'get_lyric_edited',
save_lyric_edited: 'save_lyric_edited',
remove_lyric_edited: 'remove_lyric_edited',
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',
taskbar_set_thumbar_buttons: 'taskbar_set_thumbar_buttons',
taskbar_set_thumbnail_clip: 'taskbar_set_thumbnail_clip',
taskbar_on_thumbar_button_click: 'taskbar_on_thumbar_button_click',
},
winLyric: {
close: 'close',
set_lyric_info: 'set_lyric_info',
get_lyric_info: 'get_lyric_info',
set_lyric_config: 'set_lyric_config',
get_lyric_config: 'get_lyric_config',
set_win_bounds: 'set_win_bounds',
key_down: 'key_down',
},
hotKey: {
enable: 'enable',
status: 'status',
set_config: 'set_config',
},
}
for (const item of Object.keys(names)) {
let name = names[item]
for (const key of Object.keys(name)) {
name[key] = `${item}_${name[key]}`
}
}
exports.mainWindow = names.mainWindow
exports.winLyric = names.winLyric
exports.hotKey = names.hotKey
+177
View File
@@ -0,0 +1,177 @@
const modules = {
common: {
get_env_params: 'get_env_params',
deeplink: 'deeplink',
clear_env_params_deeplink: 'clear_env_params_deeplink',
system_theme_change: 'system_theme_change',
theme_change: 'theme_change',
get_system_fonts: 'get_system_fonts',
get_app_setting: 'get_app_setting',
set_app_setting: 'set_app_setting',
},
player: {
invoke_play_music: 'play_music',
invoke_play_next: 'play_next',
invoke_play_prev: 'play_prev',
invoke_toggle_play: 'toggle_play',
player_play: 'player_play',
player_pause: 'player_pause',
player_stop: 'player_stop',
player_error: 'player_error',
list_data_overwire: 'list_data_overwire',
list_get: 'list_get',
list_add: 'list_add',
list_remove: 'list_remove',
list_update: 'list_update',
list_update_position: 'list_update_position',
list_music_get: 'list_music_get',
list_music_add: 'list_music_add',
list_music_move: 'list_music_move',
list_music_remove: 'list_music_remove',
list_music_update: 'list_music_update',
list_music_update_position: 'list_music_update_position',
list_music_overwrite: 'list_music_overwrite',
list_music_clear: 'list_music_clear',
list_music_check_exist: 'list_music_check_exist',
list_music_get_list_ids: 'list_music_get_list_ids',
},
winMain: {
focus: 'focus',
close: 'close',
min: 'min',
max: 'max',
fullscreen: 'fullscreen',
set_app_name: 'set_app_name',
clear_cache: 'clear_cache',
get_cache_size: 'get_cache_size',
inited: 'inited',
show_save_dialog: 'show_save_dialog',
show_select_dialog: 'show_select_dialog',
show_dialog: 'show_dialog',
open_dev_tools: 'open_dev_tools',
progress: 'progress',
change_tray: 'change_tray',
quit_update: 'quit_update',
update_check: 'update_check',
update_available: 'update_available',
update_error: 'update_error',
update_progress: 'update_progress',
update_downloaded: 'update_downloaded',
update_not_available: 'update_not_available',
set_ignore_mouse_events: 'set_ignore_mouse_events',
set_window_size: 'set_window_size',
handle_request: 'handle_request',
cancel_request: 'cancel_request',
restart_window: 'restart_window',
// lang_s2t: 'lang_s2t',
handle_kw_decode_lyric: 'handle_kw_decode_lyric',
get_lyric_info: 'get_lyric_info',
set_lyric_info: 'set_lyric_info',
set_config: 'set_config',
set_hot_key_config: 'set_hot_key_config',
on_config_change: 'on_config_change',
key_down: 'key_down',
quit: 'quit',
min_toggle: 'min_toggle',
hide_toggle: 'hide_toggle',
get_other_source: 'get_other_source',
save_other_source: 'save_other_source',
clear_other_source: 'clear_other_source',
get_other_source_count: 'get_other_source_count',
get_data: 'get_data',
save_data: 'save_data',
get_hot_key: 'get_hot_key',
import_user_api: 'import_user_api',
remove_user_api: 'remove_user_api',
set_user_api: 'set_user_api',
get_user_api_list: 'get_user_api_list',
request_user_api: 'request_user_api',
request_user_api_cancel: 'request_user_api_cancel',
get_user_api_status: 'get_user_api_status',
user_api_status: 'user_api_status',
user_api_show_update_alert: 'user_api_show_update_alert',
user_api_set_allow_update_alert: 'user_api_set_allow_update_alert',
get_palyer_lyric: 'get_lyric',
// save_lyric: 'save_lyric',
// clear_lyric: 'clear_lyric',
get_lyric_raw: 'get_lyric_raw',
save_lyric_raw: 'save_lyric_raw',
clear_lyric_raw: 'clear_lyric_raw',
get_lyric_raw_count: 'get_lyric_raw_count',
get_lyric_edited: 'get_lyric_edited',
save_lyric_edited: 'save_lyric_edited',
remove_lyric_edited: 'remove_lyric_edited',
clear_lyric_edited: 'clear_lyric_edited',
get_lyric_edited_count: 'get_lyric_edited_count',
get_music_url: 'get_music_url',
save_music_url: 'save_music_url',
clear_music_url: 'clear_music_url',
get_music_url_count: 'get_music_url_count',
sync_action: 'sync_action',
process_new_desktop_lyric_client: 'process_new_desktop_lyric_client',
player_action_set_buttons: 'player_action_set_buttons',
// player_action_set_thumbnail_clip: 'player_action_set_thumbnail_clip',
player_action_on_button_click: 'player_action_on_button_click',
get_themes: 'get_themes',
save_theme: 'save_theme',
remove_theme: 'remove_theme',
download_list_get: 'download_list_get',
download_list_add: 'download_list_add',
download_list_update: 'download_list_update',
download_list_remove: 'download_list_remove',
download_list_clear: 'download_list_clear',
},
winLyric: {
close: 'close',
set_config: 'set_config',
get_config: 'get_config',
on_config_change: 'on_config_change',
main_window_inited: 'main_window_inited',
set_win_bounds: 'set_win_bounds',
key_down: 'key_down',
request_main_window_channel: 'request_main_window_channel',
provide_main_window_channel: 'provide_main_window_channel',
},
hotKey: {
enable: 'enable',
status: 'status',
set_config: 'set_config',
},
}
for (const moduleName of Object.keys(modules) as Array<keyof typeof modules>) {
let eventNames = modules[moduleName]
for (const eventName of Object.keys(eventNames) as Array<keyof typeof eventNames>) {
eventNames[eventName] = `${moduleName}_${eventName as string}` as never
}
}
// for (const moduleName of Object.keys(modules) as Array<keyof typeof modules>) {
// let eventNames = modules[moduleName]
// for (const eventName of Object.keys(eventNames)) {
// eventNames[eventName] = `${moduleName}_${eventName}`
// }
// }
export const CMMON_EVENT_NAME = modules.common
export const PLAYER_EVENT_NAME = modules.player
export const WIN_MAIN_RENDERER_EVENT_NAME = modules.winMain
export const WIN_LYRIC_RENDERER_EVENT_NAME = modules.winLyric
export const HOTKEY_RENDERER_EVENT_NAME = modules.hotKey
+54
View File
@@ -0,0 +1,54 @@
import { ipcMain } from 'electron'
export function mainOn(name: string, listener: LX.IpcMainEventListener): void
export function mainOn<T>(name: string, listener: LX.IpcMainEventListenerParams<T>): void
export function mainOn<T>(name: string, listener: LX.IpcMainEventListenerParams<T>): void {
ipcMain.on(name, (event, params) => {
listener({ event, params })
})
}
export function mainOnce(name: string, listener: LX.IpcMainEventListener): void
export function mainOnce<T>(name: string, listener: LX.IpcMainEventListenerParams<T>): void
export function mainOnce<T>(name: string, listener: LX.IpcMainEventListenerParams<T>): void {
ipcMain.once(name, (event, params) => {
listener({ event, params })
})
}
export const mainOff = (name: string, listener: (...args: any[]) => void) => {
ipcMain.removeListener(name, listener)
}
export const mainOffAll = (name: string) => {
ipcMain.removeAllListeners(name)
}
export function mainHandle(name: string, listener: LX.IpcMainInvokeEventListener): void
export function mainHandle<T>(name: string, listener: LX.IpcMainInvokeEventListenerParams<T>): void
export function mainHandle<V>(name: string, listener: LX.IpcMainInvokeEventListenerValue<V>): void
export function mainHandle<T, V>(name: string, listener: LX.IpcMainInvokeEventListenerParamsValue<T, V>): void
export function mainHandle<T, V>(name: string, listener: LX.IpcMainInvokeEventListenerParamsValue<T, V>): void {
ipcMain.handle(name, async(event, params) => {
return await listener({ event, params })
})
}
export function mainHandleOnce(name: string, listener: LX.IpcMainInvokeEventListener): void
export function mainHandleOnce<T>(name: string, listener: LX.IpcMainInvokeEventListenerParams<T>): void
export function mainHandleOnce<V>(name: string, listener: LX.IpcMainInvokeEventListenerValue<V>): void
export function mainHandleOnce<T, V>(name: string, listener: LX.IpcMainInvokeEventListenerParamsValue<T, V>): void
export function mainHandleOnce<T, V>(name: string, listener: LX.IpcMainInvokeEventListenerParamsValue<T, V>): void {
ipcMain.handleOnce(name, async(event, params) => {
return await listener({ event, params })
})
}
export const mainHandleRemove = (name: string) => {
ipcMain.removeHandler(name)
}
export function mainSend(window: Electron.BrowserWindow, name: string): void
export function mainSend<T>(window: Electron.BrowserWindow, name: string, params: T): void
export function mainSend<T>(window: Electron.BrowserWindow, name: string, params?: T): void {
window.webContents.send(name, params)
}
+45
View File
@@ -0,0 +1,45 @@
import { ipcRenderer } from 'electron'
export function rendererSend(name: string): void
export function rendererSend<T>(name: string, params: T): void
export function rendererSend<T>(name: string, params?: T): void {
ipcRenderer.send(name, params)
}
export function rendererSendSync(name: string): void
export function rendererSendSync<T>(name: string, params: T): void
export function rendererSendSync<T>(name: string, params?: T): void {
ipcRenderer.sendSync(name, params)
}
export async function rendererInvoke(name: string): Promise<void>
export async function rendererInvoke<V>(name: string): Promise<V>
export async function rendererInvoke<T>(name: string, params: T): Promise<void>
export async function rendererInvoke<T, V>(name: string, params: T): Promise<V>
export async function rendererInvoke <T, V>(name: string, params?: T): Promise<V> {
return await ipcRenderer.invoke(name, params)
}
export function rendererOn(name: string, listener: LX.IpcRendererEventListener): void
export function rendererOn<T>(name: string, listener: LX.IpcRendererEventListenerParams<T>): void
export function rendererOn<T>(name: string, listener: LX.IpcRendererEventListenerParams<T>): void {
ipcRenderer.on(name, (event, params) => {
listener({ event, params })
})
}
export function rendererOnce(name: string, listener: LX.IpcRendererEventListener): void
export function rendererOnce<T>(name: string, listener: LX.IpcRendererEventListenerParams<T>): void
export function rendererOnce<T>(name: string, listener: LX.IpcRendererEventListenerParams<T>): void {
ipcRenderer.once(name, (event, params) => {
listener({ event, params })
})
}
export const rendererOff = (name: string, listener: (...args: any[]) => any) => {
ipcRenderer.removeListener(name, listener)
}
export const rendererOffAll = (name: string) => {
ipcRenderer.removeAllListeners(name)
}
-42
View File
@@ -1,42 +0,0 @@
const Store = require('electron-store')
const { dialog, app, shell } = require('electron')
const path = require('path')
const fs = require('fs')
const log = require('electron-log')
const stores = {}
/**
* 获取 Store 对象
* @param {*} name store 名
* @param {*} isIgnoredError 是否忽略错误
* @param {*} isShowErrorAlert 是否显示错误弹窗
* @returns Store
*/
module.exports = (name, isIgnoredError = true, isShowErrorAlert = true) => {
if (stores[name]) return stores[name]
let store
try {
store = stores[name] = new Store({ name, clearInvalidConfig: false })
} catch (error) {
log.error(error)
if (!isIgnoredError) throw error
const backPath = path.join(app.getPath('userData'), name + '.json.bak')
fs.copyFileSync(path.join(app.getPath('userData'), 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
}
+73
View File
@@ -0,0 +1,73 @@
/* eslint-disable */
// https://github.com/PimpTrizkit/PJs/wiki/12.-Shade,-Blend-and-Convert-a-Web-Color-(pSBC.js)#micro-functions-version-4
/**
* Blend color (Lighten or Darken)
* @param {number} p 混合百分比 范围 0.0 - 1.0
* @param {string} c0 rgb(a) color1
* @param {string} c1 rgb(a) color2
* @returns color
*/
exports.RGB_Linear_Blend=(p,c0,c1)=>{
var i=parseInt,r=Math.round,P=1-p,[a,b,c,d]=c0.split(","),[e,f,g,h]=c1.split(","),x=d||h,j=x?","+(!d?h:!h?d:r((parseFloat(d)*P+parseFloat(h)*p)*1000)/1000+")"):")";
return"rgb"+(x?"a(":"(")+r(i(a[3]=="a"?a.slice(5):a.slice(4))*P+i(e[3]=="a"?e.slice(5):e.slice(4))*p)+","+r(i(b)*P+i(f)*p)+","+r(i(c)*P+i(g)*p)+j;
}
/**
* Blend color (Lighten or Darken)
* @param {number} p 混合百分比 范围 0.0 - 1.0
* @param {string} c0 rgb(a) color1
* @param {string} c1 rgb(a) color2
* @returns color
*/
exports.RGB_Log_Blend=(p,c0,c1)=>{
var i=parseInt,r=Math.round,P=1-p,[a,b,c,d]=c0.split(","),[e,f,g,h]=c1.split(","),x=d||h,j=x?","+(!d?h:!h?d:r((parseFloat(d)*P+parseFloat(h)*p)*1000)/1000+")"):")";
return"rgb"+(x?"a(":"(")+r((P*i(a[3]=="a"?a.slice(5):a.slice(4))**2+p*i(e[3]=="a"?e.slice(5):e.slice(4))**2)**0.5)+","+r((P*i(b)**2+p*i(f)**2)**0.5)+","+r((P*i(c)**2+p*i(g)**2)**0.5)+j;
}
/**
* Shade color (Lighten or Darken)
* @param {number} p Shade 百分比范围为 -1.0 - 1.0 负为黑色,正为白色
* @param {string} c0 rgb(a) color
* @returns color
*/
exports.RGB_Linear_Shade=(p,c0)=>{
var i=parseInt,r=Math.round,[a,b,c,d]=c0.split(","),n=p<0,t=n?0:255*p,P=n?1+p:1-p;
return"rgb"+(d?"a(":"(")+r(i(a[3]=="a"?a.slice(5):a.slice(4))*P+t)+","+r(i(b)*P+t)+","+r(i(c)*P+t)+(d?","+d:")");
}
/**
* Shade color (Lighten or Darken)
* @param {number} p Shade 百分比范围为 -1.0 - 1.0 负为黑色,正为白色
* @param {string} c0 rgb(a) color
* @returns color
*/
exports.RGB_Log_Shade=(p,c0)=>{
var i=parseInt,r=Math.round,[a,b,c,d]=c0.split(","),n=p<0,t=n?0:p*255**2,P=n?1+p:1-p;
return"rgb"+(d?"a(":"(")+r((P*i(a[3]=="a"?a.slice(5):a.slice(4))**2+t)**0.5)+","+r((P*i(b)**2+t)**0.5)+","+r((P*i(c)**2+t)**0.5)+(d?","+d:")");
}
/**
* 修改透明度
* @param {number} p 透明度 -1.0 - 1.0
* @param {string} color
* @returns color
*/
exports.RGB_Alpha_Shade = (p, color) => {
var i = parseInt
var n = p < 0
var [r, g, b, a] = color.split(",")
r = r[3] == 'a' ? r.slice(5) : r.slice(4)
if (a) {
a = parseFloat(a)
a = a - (n ? (1 - a) * p : a * p)
a = n ? Math.max(0, a) : Math.min(1, a)
} else {
a = 1 - p
a = Math.min(1, a)
}
return `rgba(${i(r)}, ${i(g)}, ${i(b)}, ${a.toFixed(2)})`
}
+331
View File
@@ -0,0 +1,331 @@
//! 更新默认主题配置后,需要执行 npm run build:theme 重新构建index.json
const fs = require('fs')
const path = require('path')
const { createThemeColors } = require('./utils')
const defaultThemes = [
{
id: 'green',
name: '绿意盎然',
isDark: false,
config: {
primary: 'rgb(77, 175, 124)',
'--color-app-background': 'var(--color-primary-light-600-alpha-700)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#4baed5',
'--color-badge-tertiary': '#e7aa36',
},
},
{
id: 'blue',
name: '蓝田生玉',
isDark: false,
config: {
primary: 'rgb(52, 152, 219)',
'--color-app-background': 'var(--color-primary-light-600-alpha-700)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#5cbf9b',
'--color-badge-tertiary': '#5cbf9b',
},
},
{
id: 'blue_plus',
name: '蛋雅深蓝',
isDark: false,
config: {
primary: 'rgb(77, 131, 175)',
'--color-app-background': 'var(--color-primary-light-600-alpha-600)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': 'rgba(66.6, 150.7, 171, 1)',
'--color-badge-tertiary': 'rgba(54, 196, 231, 1)',
},
},
{
id: 'orange',
name: '橙黄橘绿',
isDark: false,
config: {
primary: 'rgb(245, 171, 53)',
'--color-app-background': 'var(--color-primary-light-600-alpha-700)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#9ed458',
'--color-badge-tertiary': '#9ed458',
},
},
{
id: 'red',
name: '热情似火',
isDark: false,
config: {
primary: 'rgb(214, 69, 65)',
'--color-app-background': 'var(--color-primary-light-600-alpha-700)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#dfbb6b',
'--color-badge-tertiary': '#dfbb6b',
},
},
{
id: 'pink',
name: '粉装玉琢',
isDark: false,
config: {
primary: 'rgb(241, 130, 141)',
'--color-app-background': 'var(--color-primary-light-600-alpha-700)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#f5b684',
'--color-badge-tertiary': '#f5b684',
},
},
{
id: 'purple',
name: '重斤球紫',
isDark: false,
config: {
primary: 'rgb(155, 89, 182)',
'--color-app-background': 'var(--color-primary-light-600-alpha-700)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#e5a39f',
'--color-badge-tertiary': '#e5a39f',
},
},
{
id: 'grey',
name: '灰常美丽',
isDark: false,
config: {
primary: 'rgb(108, 122, 137)',
'--color-app-background': 'var(--color-primary-light-600-alpha-700)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#b19b9f',
'--color-badge-tertiary': '#b19b9f',
},
},
{
id: 'ming',
name: '青出于黑',
isDark: false,
config: {
primary: 'rgb(51, 110, 123)',
'--color-app-background': 'var(--color-primary-light-600-alpha-700)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#6376a2',
'--color-badge-tertiary': '#6376a2',
},
},
{
id: 'blue2',
name: '清热板蓝',
isDark: false,
config: {
primary: 'rgb(79, 98, 208)',
'--color-app-background': 'var(--color-primary-light-600-alpha-700)',
'--color-main-background': 'rgba(255, 255, 255, 1)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'none',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#b080db',
'--color-badge-tertiary': '#b080db',
},
},
{
id: 'black',
name: '黑灯瞎火',
isDark: true,
config: {
primary: 'rgb(150, 150, 150)',
'--color-app-background': 'rgba(0, 0, 0, 0)',
'--color-main-background': 'rgba(19, 19, 19, 0.9)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'url(./theme_images/landingMoon.png)',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary-dark-200)',
'--color-badge-secondary': 'var(--color-primary)',
'--color-badge-tertiary': 'var(--color-primary-dark-300)',
},
},
{
id: 'mid_autumn',
name: '月里嫦娥',
isDark: false,
config: {
primary: 'rgb(74, 55, 82)',
'--color-app-background': 'rgba(255, 255, 255, 0)',
'--color-main-background': 'rgba(255, 255, 255, 0.9)',
'--color-nav-font': 'var(--color-primary-light-600)',
'--background-image': 'url(./theme_images/jqbg.jpg)',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': '#af9479',
'--color-badge-tertiary': '#af9479',
},
},
{
id: 'naruto',
name: '木叶之村',
isDark: false,
config: {
primary: 'rgb(87, 144, 167)',
'--color-app-background': 'rgba(255, 255, 255, 0.15)',
'--color-main-background': 'rgba(255, 255, 255, 0.8)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'url(./theme_images/myzcbg.jpg)',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': 'var(--color-primary)',
'--color-badge-secondary': 'var(--color-primary-light-100)',
'--color-badge-tertiary': 'var(--color-primary-light-100)',
},
},
{
id: 'happy_new_year',
name: '新年快乐',
isDark: false,
config: {
primary: 'rgb(192, 57, 43)',
'--color-app-background': 'rgba(255, 255, 255, 0.15)',
'--color-main-background': 'rgba(255, 255, 255, 0.8)',
'--color-nav-font': 'var(--color-primary)',
'--background-image': 'url(./theme_images/xnkl.png)',
'--background-image-position': 'center',
'--background-image-size': 'cover',
'--color-btn-hide': '#3bc2b2',
'--color-btn-min': '#85c43b',
'--color-btn-close': '#fab4a0',
'--color-badge-primary': '#7fb575',
'--color-badge-secondary': '#dfbb6b',
'--color-badge-tertiary': 'var(--color-primary-light-100)',
},
},
]
const themes = defaultThemes.map(({ config: { primary, ...extInfo }, ...themeInfo }) => {
return {
...themeInfo,
isCustom: false,
config: {
themeColors: createThemeColors(primary, themeInfo.isDark),
extInfo,
},
}
})
fs.writeFileSync(path.join(__dirname, 'index.json'), JSON.stringify(themes, null, 2))

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 353 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before

Width:  |  Height:  |  Size: 997 KiB

After

Width:  |  Height:  |  Size: 997 KiB

File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
const { RGB_Linear_Shade, RGB_Alpha_Shade } = require('./colorUtils')
exports.createThemeColors = (rgbaColor, isDark) => {
const colors = {
'--color-primary': rgbaColor,
}
let preColor = rgbaColor
for (let i = 1; i < 11; i += 1) {
preColor = RGB_Linear_Shade(isDark ? 0.2 : -0.1, preColor)
colors[`--color-primary-dark-${i * 100}`] = preColor
for (let j = 1; j < 10; j += 1) {
colors[`--color-primary-dark-${i * 100}-alpha-${j * 100}`] = RGB_Alpha_Shade(0.1 * j, preColor)
colors[`--color-primary-alpha-${j * 100}`] = RGB_Alpha_Shade(0.1 * j, rgbaColor)
}
}
preColor = rgbaColor
for (let i = 1; i < 10; i += 1) {
preColor = RGB_Linear_Shade(isDark ? -0.1 : 0.2, preColor)
colors[`--color-primary-light-${i * 100}`] = preColor
for (let j = 1; j < 10; j += 1) {
colors[`--color-primary-light-${i * 100}-alpha-${j * 100}`] = RGB_Alpha_Shade(0.1 * j, preColor)
}
}
preColor = RGB_Linear_Shade(isDark ? -0.2 : 1, preColor)
colors[`--color-primary-light-${1000}`] = preColor
for (let j = 1; j < 10; j += 1) {
colors[`--color-primary-light-${1000}-alpha-${j * 100}`] = RGB_Alpha_Shade(0.1 * j, preColor)
}
colors['--color-theme'] = isDark ? colors['--color-primary-light-900'] : rgbaColor
return colors
}
// rgb(238, 238, 238)
// let prec = 'rgb(255, 255, 255)'
// let colors = [prec]
// for (let j = 1; j < 11; j += 1) {
// colors.push(prec = RGB_Linear_Shade(-0.15, prec))
// }
// console.log(colors)
+16
View File
@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "esnext",
"moduleResolution": "nodenext",
"typeRoots": [ /* Specify multiple folders that act like './node_modules/@types'. */
"./types"
],
},
// "include": [
// "**/*.ts",
// "**/*.js",
// "**/*.vue",
// "**/*.json",
// ],
}
+456
View File
@@ -0,0 +1,456 @@
declare namespace LX {
type AddMusicLocationType = 'top' | 'bottom'
interface AppSetting {
version: string
/**
* 窗口大小id
*/
'common.windowSizeId': number
/**
* 窗口大小id
*/
'common.fontSize': number
/**
* 是否以全屏启动
*/
'common.startInFullscreen': boolean
/**
* 语言id
*/
'common.langId': string | null
/**
* api id
*/
'common.apiSource': string
/**
* 音源名称类型,原名、别名
*/
'common.sourceNameType': 'alias' | 'real'
/**
* 显示的字体
*/
'common.font': string
/**
* 是否启用动画
*/
'common.isShowAnimation': boolean
/**
* 是否启用随机弹窗动画
*/
'common.randomAnimate': boolean
/**
* 是否同意软件协议
*/
'common.isAgreePact': boolean
/**
* 控制按钮位置,左边、右边
*/
'common.controlBtnPosition': 'left' | 'right'
/**
* 启动时自动播放歌曲
*/
'player.startupAutoPlay': boolean
/**
* 切歌模式
*/
'player.togglePlayMethod': 'listLoop' | 'random' | 'list' | 'singleLoop' | 'none'
/**
* 是否优先播放320k音质
*/
'player.highQuality': boolean
/**
* 是否显示任务栏进度条
*/
'player.isShowTaskProgess': boolean
/**
* 音量大小
*/
'player.volume': number
/**
* 是否静音
*/
'player.isMute': boolean
/**
* 音频输出设备id
*/
'player.mediaDeviceId': string
/**
* 是否在音频输出设备更改时暂停播放
*/
'player.isMediaDeviceRemovedStopPlay': boolean
/**
* 是否显示歌词翻译
*/
'player.isShowLyricTranslation': boolean
/**
* 是否显示歌词罗马音
*/
'player.isShowLyricRoma': boolean
/**
* 是否将歌词从简体转换为繁体
*/
'player.isS2t': boolean
/**
* 是否播放卡拉OK歌词
*/
'player.isPlayLxlrc': boolean
/**
* 启动软件时是否恢复上次播放进度
*/
'player.isSavePlayTime': boolean
/**
* 是否启用音频可视化
*/
'player.audioVisualization': boolean
/**
* 定时暂停播放-是否等待歌曲播放完毕再暂停
*/
'player.waitPlayEndStop': boolean
/**
* 定时暂停播放-倒计时时间
*/
'player.waitPlayEndStopTime': string
/**
* 是否启用音频加载失败时自动切歌
*/
'player.autoSkipOnError': boolean
/**
* 播放详情页-是否缩放当前播放的歌词行
*/
'playDetail.isZoomActiveLrc': boolean
/**
* 播放详情页-是否允许通过歌词调整播放进度
*/
'playDetail.isShowLyricProgressSetting': boolean
/**
* 播放详情页-歌词字体大小
*/
'playDetail.style.fontSize': number
/**
* 播放详情页-歌词对齐方式
*/
'playDetail.style.align': 'center' | 'left' | 'right'
/**
* 是否启用桌面歌词
*/
'desktopLyric.enable': boolean
/**
* 是否锁定桌面歌词
*/
'desktopLyric.isLock': boolean
/**
* 是在置顶桌面
*/
'desktopLyric.isAlwaysOnTop': boolean
/**
* 是否自动刷新歌词置顶
*/
'desktopLyric.isAlwaysOnTopLoop': boolean
/**
* 是否启用音频可视化
*/
'desktopLyric.audioVisualization': boolean
/**
* 桌面歌词窗口宽度
*/
'desktopLyric.width': number
/**
* 桌面歌词窗口高度
*/
'desktopLyric.height': number
/**
* 桌面歌词窗口x坐标
*/
'desktopLyric.x': number | null
/**
* 桌面歌词窗口y坐标
*/
'desktopLyric.y': number | null
/**
* 是否允许桌面歌词窗口拖出主屏幕之外
*/
'desktopLyric.isLockScreen': boolean
/**
* 是否延迟桌面歌词滚动
*/
'desktopLyric.isDelayScroll': boolean
/**
* 是否在鼠标划过桌面歌词窗口时降低歌词透明度
*/
'desktopLyric.isHoverHide': boolean
/**
* 歌词方向
*/
'desktopLyric.direction': 'horizontal' | 'vertical'
/**
* 是否在鼠标划过桌面歌词窗口时降低歌词透明度
*/
'desktopLyric.style.align': 'center' | 'left' | 'right'
/**
* 桌面歌词字体
*/
'desktopLyric.style.font': string
/**
* 桌面歌词字体大小
*/
'desktopLyric.style.fontSize': number
/**
* 桌面歌词未播放字体颜色
*/
'desktopLyric.style.lyricUnplayColor': string
/**
* 桌面歌词已播放字体颜色
*/
'desktopLyric.style.lyricPlayedColor': string
/**
* 桌面歌词字体阴影颜色
*/
'desktopLyric.style.lyricShadowColor': string
/**
* 桌面歌词加粗字体
*/
// 'desktopLyric.style.fontWeight': boolean
/**
* 桌面歌词字体透明度
*/
'desktopLyric.style.opacity': number
/**
* 桌面歌词是否允许换行
*/
'desktopLyric.style.ellipsis': boolean
/**
* 是否缩放当前正在播放的桌面歌词
*/
'desktopLyric.style.isZoomActiveLrc': boolean
/**
* 是否启用双击列表里的歌曲时自动切换到当前列表播放(仅对歌单、排行榜有效)
*/
'list.isClickPlayList': boolean
/**
* 是否显示歌曲来源(仅对我的列表有效)
*/
'list.isShowSource': boolean
/**
* 是否自动恢复列表滚动位置(仅对我的列表有效)
*/
'list.isSaveScrollLocation': boolean
/**
* 添加歌曲到我的列表时的方式
*/
'list.addMusicLocationType': LX.AddMusicLocationType
/**
* 是否显示列表操作按钮列
*/
'list.actionButtonsVisible': boolean
/**
* 是否启用下载功能
*/
'download.enable': boolean
/**
* 下载路径
*/
'download.savePath': string
/**
* 文件命名方式
*/
'download.fileName': '歌名 - 歌手' | '歌手 - 歌名' | '歌名'
/**
* 最大并发下载数
*/
'download.maxDownloadNum': number
/**
* 是否下载lrc文件
*/
'download.isDownloadLrc': boolean
/**
* 是否下载翻译歌词文件
*/
'download.isDownloadTLrc': boolean
/**
* 是否下载罗马音歌词文件
*/
'download.isDownloadRLrc': boolean
/**
* 保存lrc时的文本编码格式
*/
'download.lrcFormat': 'utf8' | 'gbk'
/**
* 是否在音频文件中嵌入歌曲封面
*/
'download.isEmbedPic': boolean
/**
* 是否在音频文件中嵌入歌词
*/
'download.isEmbedLyric': boolean
/**
* 歌曲源不可用时,是否启用换源下载
*/
'download.isUseOtherSource': boolean
/**
* 主题id
*/
'theme.id': string
/**
* 亮色主题id
*/
'theme.lightId': string
/**
* 暗色主题id
*/
'theme.darkId': string
/**
* 是否显示热门搜索
*/
'search.isShowHotSearch': boolean
/**
* 是否显示搜索历史
*/
'search.isShowHistorySearch': boolean
/**
* 软件启动时是否自动聚焦搜索框
*/
'search.isFocusSearchBox': boolean
/**
* 是否启用代理
*/
'network.proxy.enable': boolean
/**
* 代理服务器地址
*/
'network.proxy.host': string
/**
* 代理服务器端口号
*/
'network.proxy.port': string
/**
* 代理服务器用户名
*/
'network.proxy.username': string
/**
* 代理服务器密码
*/
'network.proxy.password': string
/**
* 是否启用托盘
*/
'tray.enable': boolean
/**
* 是否关闭时是否最小化到托盘
*/
// 'tray.isToTray': boolean
/**
* 托盘主题id
*/
'tray.themeId': number
/**
* 是否启用同步服务
*/
'sync.enable': boolean
/**
* 同步服务端口号
*/
'sync.port': '23332' | string
/**
* 是否在离开搜索界面时自动清空搜索框
*/
'odc.isAutoClearSearchInput': boolean
/**
* 是否在离开搜索界面时自动清空搜索结果列表
*/
'odc.isAutoClearSearchList': boolean
}
}
+122
View File
@@ -0,0 +1,122 @@
// import './app_setting'
declare namespace LX {
interface CmdParams {
/**
* 搜索,启动软件时自动在搜索框搜索指定的内容,例如:-search="突然的自我 - 伍佰"
*/
search?: string
/**
* 禁用硬件加速启动
*/
dha?: boolean
/**
* 以非透明模式启动
*/
dt?: boolean
/**
* 禁用硬件媒体密钥处理
*/
dhmkh?: boolean
/**
* 设置代理服务器,代理应用的所有流量,例:-proxy-server="127.0.0.1:1081"(不支持设置账号密码,v1.17.0起新增)。注:应用内“设置-网络-代理设置”仅代理接口请求的流量,优先级更高
*/
'proxy-server'?: string
/**
* 以分号分隔的主机列表绕过代理服务器,例:-proxy-bypass-list="<local>;*.google.com;*foo.com;1.2.3.4:5678"(与-proxy-server一起使用才有效,v1.17.0起新增)。注:此设置对应用内接口请求无效
*/
'proxy-bypass-list'?: string
/**
* 启动时播放指定列表的音乐
*/
play?: string
[key: string]: boolean | number | string
}
type OnlineSource = 'kw' | 'kg' | 'tx' | 'wy' | 'mg'
type Source = OnlineSource | 'local'
type Quality = '128k' | '320k' | 'flac' | 'flac24bit' | '192k' | 'ape' | 'wav'
type QualityList = Partial<Record<LX.Source, LX.Quality[]>>
interface EnvParams {
deeplink?: string | null
cmdParams: CmdParams
workAreaSize?: Electron.Size
}
interface HotKey {
name: string
action: string
type: keyof typeof keyName
}
interface HotKeyDownInfo {
type: 'local' | 'global'
key: string
}
interface HotKeyConfig {
enable: boolean
keys: {
[key: string]: HotKey
}
}
interface HotKeyConfigAll {
local: HotKeyConfig
global: HotKeyConfig
}
interface RegisterKeyInfo {
key: string
info: HotKey
}
type HotKeyState = Map<string, {
status: boolean
info: HotKey
}>
interface HotKeyActionWrap<T, D> {
action: T
data: D
source?: string
}
type HotKeyActions = HotKeyActionWrap<'config', HotKeyConfigAll>
| HotKeyActionWrap<'enable', boolean>
| HotKeyActionWrap<'register', RegisterKeyInfo>
| HotKeyActionWrap<'unregister', string>
interface HotKeyEvent {
type: string
key: string
}
interface TaskBarButtonFlags {
empty: boolean
collect: boolean
play: boolean
next: boolean
prev: boolean
}
interface Wait {
time: number
id: string
}
type WaitCancel = string
interface Interval {
time: number
id: string
}
type IntervalCancel = string
interface VersionInfo {
version: string
desc: string
}
}
+9
View File
@@ -0,0 +1,9 @@
declare namespace LX {
namespace ConfigFile {
interface MyListInfoPart {
type: 'playListPart_v2'
data: LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull | LX.List.UserListInfoFull
}
}
}
+82
View File
@@ -0,0 +1,82 @@
declare namespace LX {
namespace DesktopLyric {
interface Config {
'desktopLyric.enable': LX.AppSetting['desktopLyric.enable']
'desktopLyric.isLock': LX.AppSetting['desktopLyric.isLock']
'desktopLyric.isAlwaysOnTop': LX.AppSetting['desktopLyric.isAlwaysOnTop']
'desktopLyric.isAlwaysOnTopLoop': LX.AppSetting['desktopLyric.isAlwaysOnTopLoop']
'desktopLyric.audioVisualization': LX.AppSetting['desktopLyric.audioVisualization']
'desktopLyric.width': LX.AppSetting['desktopLyric.width']
'desktopLyric.height': LX.AppSetting['desktopLyric.height']
'desktopLyric.x': LX.AppSetting['desktopLyric.x']
'desktopLyric.y': LX.AppSetting['desktopLyric.y']
'desktopLyric.isLockScreen': LX.AppSetting['desktopLyric.isLockScreen']
'desktopLyric.isDelayScroll': LX.AppSetting['desktopLyric.isDelayScroll']
'desktopLyric.isHoverHide': LX.AppSetting['desktopLyric.isHoverHide']
'desktopLyric.direction': LX.AppSetting['desktopLyric.direction']
'desktopLyric.style.align': LX.AppSetting['desktopLyric.style.align']
'desktopLyric.style.font': LX.AppSetting['desktopLyric.style.font']
'desktopLyric.style.fontSize': LX.AppSetting['desktopLyric.style.fontSize']
'desktopLyric.style.lyricUnplayColor': LX.AppSetting['desktopLyric.style.lyricUnplayColor']
'desktopLyric.style.lyricPlayedColor': LX.AppSetting['desktopLyric.style.lyricPlayedColor']
'desktopLyric.style.lyricShadowColor': LX.AppSetting['desktopLyric.style.lyricShadowColor']
// 'desktopLyric.style.fontWeight': LX.AppSetting['desktopLyric.style.fontWeight']
'desktopLyric.style.opacity': LX.AppSetting['desktopLyric.style.opacity']
'desktopLyric.style.ellipsis': LX.AppSetting['desktopLyric.style.ellipsis']
'desktopLyric.style.isZoomActiveLrc': LX.AppSetting['desktopLyric.style.isZoomActiveLrc']
'common.langId': LX.AppSetting['common.langId']
'player.isShowLyricTranslation': LX.AppSetting['player.isShowLyricTranslation']
'player.isShowLyricRoma': LX.AppSetting['player.isShowLyricRoma']
'player.isPlayLxlrc': LX.AppSetting['player.isPlayLxlrc']
}
type WinMainActions = 'get_info' | 'get_status' | 'get_analyser_data_array'
interface LyricActionBase <A> {
action: A
}
interface LyricActionData<A, D> extends LyricActionBase<A> {
data: D
}
type LyricAction<A, D = undefined> = D extends undefined ? LyricActionBase<A> : LyricActionData<A, D>
type LyricActions = LyricAction<'set_info', {
id: string | null
singer: string
name: string
album: string
lrc: string | null
tlrc: string | null
rlrc: string | null
lxlrc: string | null
// pic: string | null
isPlay: boolean
line: number
played_time: number
}>
| LyricAction<'set_status', {
isPlay: boolean
line: number
played_time: number
}>
| LyricAction<'set_lyric', {
lrc: string | null
tlrc: string | null
rlrc: string | null
lxlrc: string | null
}>
| LyricAction<'set_offset', number>
| LyricAction<'set_play', number>
| LyricAction<'set_pause'>
| LyricAction<'set_stop'>
| LyricAction<'send_analyser_data_array', Uint8Array>
interface NewBounds {
x?: number | null
y?: number
w: number
h: number
}
}
}
+66
View File
@@ -0,0 +1,66 @@
// interface DownloadList {
// }
declare namespace LX {
namespace Download {
type DownloadTaskStatus = 'run'
| 'waiting'
| 'pause'
| 'error'
| 'completed'
type FileExt = 'mp3' | 'flac' | 'wav' | 'ape'
interface ProgressInfo {
progress: number
speed: string
downloaded: number
total: number
}
interface DownloadTaskActionBase <A> {
action: A
}
interface DownloadTaskActionData<A, D> extends DownloadTaskActionBase<A> {
data: D
}
type DownloadTaskAction<A, D = undefined> = D extends undefined ? DownloadTaskActionBase<A> : DownloadTaskActionData<A, D>
type DownloadTaskActions = DownloadTaskAction<'start'>
| DownloadTaskAction<'complete'>
| DownloadTaskAction<'refreshUrl'>
| DownloadTaskAction<'statusText', string>
| DownloadTaskAction<'progress', ProgressInfo>
| DownloadTaskAction<'error', {
error?: string
message?: string
}>
interface ListItem {
id: string
isComplate: boolean
status: DownloadTaskStatus
statusText: string
downloaded: number
total: number
progress: number
speed: string
metadata: {
musicInfo: LX.Music.MusicInfoOnline
url: string | null
quality: LX.Quality
ext: FileExt
fileName: string
filePath: string
}
}
interface saveDownloadMusicInfo {
list: ListItem[]
addMusicLocationType: LX.AddMusicLocationType
}
}
}
+24
View File
@@ -0,0 +1,24 @@
declare namespace LX {
interface IpcMainEvent {
event: Electron.IpcMainEvent
}
interface IpcMainEventParams<T> {
event: Electron.IpcMainEvent
params: T
}
type IpcMainEventListener = (params: LX.IpcMainEvent) => void
type IpcMainEventListenerParams<T> = (params: LX.IpcMainEventParams<T>) => void
interface IpcMainInvokeEvent {
event: Electron.IpcMainInvokeEvent
}
interface IpcMainInvokeEventParams<T> {
event: Electron.IpcMainInvokeEvent
params: T
}
type IpcMainInvokeEventListener = (params: LX.IpcMainInvokeEvent) => Promise<void>
type IpcMainInvokeEventListenerParams<T> = (params: LX.IpcMainInvokeEventParams<T>) => Promise<void>
type IpcMainInvokeEventListenerValue<V> = (params: LX.IpcMainInvokeEvent) => Promise<V>
type IpcMainInvokeEventListenerParamsValue<T, V> = (params: LX.IpcMainInvokeEventParams<T>) => Promise<V>
}
+11
View File
@@ -0,0 +1,11 @@
declare namespace LX {
interface IpcRendererEvent {
event: Electron.IpcRendererEvent
}
interface IpcRendererEventParams<T> {
event: Electron.IpcRendererEvent
params: T
}
type IpcRendererEventListener = (params: LX.IpcRendererEvent) => any
type IpcRendererEventListenerParams<T> = (params: LX.IpcRendererEventParams<T>) => any
}
+142
View File
@@ -0,0 +1,142 @@
declare namespace LX {
namespace List {
interface UserListInfo {
id: string
name: string
// list: LX.Music.MusicInfo[]
source?: LX.Source
sourceListId?: string
// position?: number
locationUpdateTime: number | null
}
interface MyDefaultListInfo {
id: 'default'
name: '试听列表'
// list: LX.Music.MusicInfo[]
}
interface MyLoveListInfo {
id: 'love'
name: '我的收藏'
// list: LX.Music.MusicInfo[]
}
interface MyTempListInfo {
id: 'temp'
name: '临时列表'
// list: LX.Music.MusicInfo[]
// TODO: save default lists info
meta: {
id?: string
}
}
type MyListInfo = MyDefaultListInfo | MyLoveListInfo | UserListInfo
interface MyAllList {
defaultList: MyDefaultListInfo
loveList: MyLoveListInfo
userList: UserListInfo[]
tempList: MyTempListInfo
}
type SearchHistoryList = string[]
type ListPositionInfo = Record<string, number>
type ListUpdateInfo = Record<string, {
updateTime: number
isAutoUpdate: boolean
}>
type ListSaveType = 'myList' | 'downloadList'
type ListSaveInfo = {
type: 'myList'
data: Partial<MyAllList>
} | {
type: 'downloadList'
data: LX.Download.ListItem[]
}
type ListActionDataOverwrite = MakeOptional<LX.List.ListDataFull, 'tempList'>
interface ListActionAdd {
position: number
listInfos: UserListInfo[]
}
type ListActionRemove = string[]
type ListActionUpdate = UserListInfo[]
interface ListActionUpdatePosition {
/**
* 列表id
*/
ids: string[]
/**
* 位置
*/
position: number
}
interface ListActionMusicAdd {
id: string
musicInfos: LX.Music.MusicInfo[]
addMusicLocationType: LX.AddMusicLocationType
}
interface ListActionMusicMove {
fromId: string
toId: string
musicInfos: LX.Music.MusicInfo[]
addMusicLocationType: LX.AddMusicLocationType
}
interface ListActionCheckMusicExistList {
listId: string
musicInfoId: string
}
interface ListActionMusicRemove {
listId: string
ids: string[]
}
type ListActionMusicUpdate = Array<{
id: string
musicInfo: LX.Music.MusicInfo
}>
interface ListActionMusicUpdatePosition {
listId: string
position: number
ids: string[]
}
interface ListActionMusicOverwrite {
listId: string
musicInfos: LX.Music.MusicInfo[]
}
type ListActionMusicClear = string
interface MyDefaultListInfoFull extends MyDefaultListInfo {
list: LX.Music.MusicInfo[]
}
interface MyLoveListInfoFull extends MyLoveListInfo {
list: LX.Music.MusicInfo[]
}
interface UserListInfoFull extends UserListInfo {
list: LX.Music.MusicInfo[]
}
interface MyTempListInfoFull extends MyTempListInfo {
list: LX.Music.MusicInfo[]
}
interface ListDataFull {
defaultList: LX.Music.MusicInfo[]
loveList: LX.Music.MusicInfo[]
userList: UserListInfoFull[]
tempList: LX.Music.MusicInfo[]
}
}
}
+121
View File
@@ -0,0 +1,121 @@
declare namespace LX {
namespace Music {
interface MusicQualityType { // {"type": "128k", size: "3.56M"}
type: LX.Quality
size: string | null
}
interface MusicQualityTypeKg { // {"type": "128k", size: "3.56M"}
type: LX.Quality
size: string | null
hash: string
}
type _MusicQualityType = Record<Quality, {
size: string | null
}>
type _MusicQualityTypeKg = Record<Quality, {
size: string | null
hash: string
}>
interface MusicInfoMetaBase {
songId: string | number // 歌曲ID,mg源为copyrightId,local为文件路径
albumName: string // 歌曲专辑名称
picUrl?: string | null // 歌曲图片链接
}
interface MusicInfoMeta_online extends MusicInfoMetaBase {
qualitys: MusicQualityType[]
_qualitys: _MusicQualityType
albumId?: string | number // 歌曲专辑ID
}
interface MusicInfoMeta_local extends MusicInfoMetaBase {
filePath: string
ext: string
}
interface MusicInfoBase<S = LX.Source> {
id: string
name: string // 歌曲名
singer: string // 艺术家名
source: S // 源
interval: string | null // 格式化后的歌曲时长,例:03:55
meta: MusicInfoMetaBase
}
interface MusicInfoLocal extends MusicInfoBase<'local'> {
meta: MusicInfoMeta_local
}
interface MusicInfo_online_common extends MusicInfoBase<'kw' | 'wy'> {
meta: MusicInfoMeta_online
}
interface MusicInfoMeta_kg extends MusicInfoMeta_online {
qualitys: MusicQualityTypeKg[]
_qualitys: _MusicQualityTypeKg
hash: string // 歌曲hash
}
interface MusicInfo_kg extends MusicInfoBase<'kg'> {
meta: MusicInfoMeta_kg
}
interface MusicInfoMeta_tx extends MusicInfoMeta_online {
strMediaMid: string // 歌曲strMediaMid
albumMid?: string // 歌曲albumMid
}
interface MusicInfo_tx extends MusicInfoBase<'tx'> {
meta: MusicInfoMeta_tx
}
interface MusicInfoMeta_mg extends MusicInfoMeta_online {
copyrightId: string // 歌曲copyrightId
lrcUrl?: string // 歌曲lrcUrl
mrcUrl?: string // 歌曲mrcUrl
trcUrl?: string // 歌曲trcUrl
}
interface MusicInfo_mg extends MusicInfoBase<'mg'> {
meta: MusicInfoMeta_mg
}
type MusicInfoOnline = MusicInfo_online_common | MusicInfo_kg | MusicInfo_tx | MusicInfo_mg
type MusicInfo = MusicInfoOnline | MusicInfoLocal
interface LyricInfo {
// 歌曲歌词
lyric: string
// 翻译歌词
tlyric?: string | null
// 罗马音歌词
rlyric?: string | null
// 逐字歌词
lxlyric?: string | null
}
interface LyricInfoSave {
id: string
lyrics: LyricInfo
}
interface MusicFileMeta {
title: string
artist: string | null
album: string | null
APIC: string | null
lyrics: string | null
}
interface MusicUrlInfo {
id: string
url: string
}
interface MusicInfoOtherSourceSave {
id: string
list: MusicInfoOnline[]
}
}
}
+11
View File
@@ -0,0 +1,11 @@
import {
IAudioMetadata as iAudioMetadata,
} from 'music-metadata'
declare global {
namespace LX {
namespace MusicMetadataModule {
type IAudioMetadata = iAudioMetadata
}
}
}
+19
View File
@@ -0,0 +1,19 @@
declare namespace LX {
namespace Player {
interface ProgressBarOptions {
progress: number
mode?: Electron.ProgressBarOptions['mode']
}
type StatusButtonActions = 'unCollect'
| 'collect'
| 'prev'
| 'pause'
| 'play'
| 'next'
interface LyricInfo extends LX.Music.LyricInfo {
rawlrcInfo: LX.Music.LyricInfo
}
}
}
+10
View File
@@ -0,0 +1,10 @@
// declare module '*.vue' {
// import { App } from 'vue'
// export default App.Component
// }
declare module '*.vue' {
import { Component } from 'vue'
const component: Component
export default component
}
+73
View File
@@ -0,0 +1,73 @@
declare namespace LX {
namespace Sync {
interface Enable {
enable: boolean
port: string
}
interface SyncActionBase <A> {
action: A
}
interface SyncActionData<A, D> extends SyncActionBase<A> {
data: D
}
type SyncAction<A, D = undefined> = D extends undefined ? SyncActionBase<A> : SyncActionData<A, D>
type SyncMainWindowActions = SyncAction<'select_mode', KeyInfo>
| SyncAction<'close_select_mode'>
| SyncAction<'status', Status>
type SyncServiceActions = SyncAction<'select_mode', Mode>
| SyncAction<'get_status'>
| SyncAction<'generate_code'>
| SyncAction<'enable', Enable>
type ActionList = SyncAction<'list_data_overwrite', LX.List.ListActionDataOverwrite>
| SyncAction<'list_create', LX.List.ListActionAdd>
| SyncAction<'list_remove', LX.List.ListActionRemove>
| SyncAction<'list_update', LX.List.ListActionUpdate>
| SyncAction<'list_update_position', LX.List.ListActionUpdatePosition>
| SyncAction<'list_music_add', LX.List.ListActionMusicAdd>
| SyncAction<'list_music_move', LX.List.ListActionMusicMove>
| SyncAction<'list_music_remove', LX.List.ListActionMusicRemove>
| SyncAction<'list_music_update', LX.List.ListActionMusicUpdate>
| SyncAction<'list_music_update_position', LX.List.ListActionMusicUpdatePosition>
| SyncAction<'list_music_overwrite', LX.List.ListActionMusicOverwrite>
| SyncAction<'list_music_clear', LX.List.ListActionMusicClear>
interface List {
action: string
data: any
}
interface Status {
status: boolean
message: string
address: string[]
code: string
devices: KeyInfo[]
}
interface KeyInfo {
clientId: string
key: string
iv: string
deviceName: string
connectionTime?: number
}
type ListData = Omit<LX.List.ListDataFull, 'tempList'>
type Mode = 'merge_local_remote'
| 'merge_remote_local'
| 'overwrite_local_remote'
| 'overwrite_remote_local'
| 'overwrite_local_remote_full'
| 'overwrite_remote_local_full'
| 'none'
| 'cancel'
}
}
+293
View File
@@ -0,0 +1,293 @@
declare namespace LX {
interface ThemeColors {
// '--color-000': string
// '--color-050': string
// '--color-100': string
// '--color-200': string
// '--color-300': string
// '--color-400': string
// '--color-500': string
// '--color-600': string
// '--color-700': string
// '--color-800': string
// '--color-900': string
'--color-theme': string
'--color-primary': string
'--color-primary-alpha-100': string
'--color-primary-alpha-200': string
'--color-primary-alpha-300': string
'--color-primary-alpha-400': string
'--color-primary-alpha-500': string
'--color-primary-alpha-600': string
'--color-primary-alpha-700': string
'--color-primary-alpha-800': string
'--color-primary-alpha-900': string
'--color-primary-dark-100': string
'--color-primary-dark-100-alpha-100': string
'--color-primary-dark-100-alpha-200': string
'--color-primary-dark-100-alpha-300': string
'--color-primary-dark-100-alpha-400': string
'--color-primary-dark-100-alpha-500': string
'--color-primary-dark-100-alpha-600': string
'--color-primary-dark-100-alpha-700': string
'--color-primary-dark-100-alpha-800': string
'--color-primary-dark-100-alpha-900': string
'--color-primary-dark-200': string
'--color-primary-dark-200-alpha-100': string
'--color-primary-dark-200-alpha-200': string
'--color-primary-dark-200-alpha-300': string
'--color-primary-dark-200-alpha-400': string
'--color-primary-dark-200-alpha-500': string
'--color-primary-dark-200-alpha-600': string
'--color-primary-dark-200-alpha-700': string
'--color-primary-dark-200-alpha-800': string
'--color-primary-dark-200-alpha-900': string
'--color-primary-dark-300': string
'--color-primary-dark-300-alpha-100': string
'--color-primary-dark-300-alpha-200': string
'--color-primary-dark-300-alpha-300': string
'--color-primary-dark-300-alpha-400': string
'--color-primary-dark-300-alpha-500': string
'--color-primary-dark-300-alpha-600': string
'--color-primary-dark-300-alpha-700': string
'--color-primary-dark-300-alpha-800': string
'--color-primary-dark-300-alpha-900': string
'--color-primary-dark-400': string
'--color-primary-dark-400-alpha-100': string
'--color-primary-dark-400-alpha-200': string
'--color-primary-dark-400-alpha-300': string
'--color-primary-dark-400-alpha-400': string
'--color-primary-dark-400-alpha-500': string
'--color-primary-dark-400-alpha-600': string
'--color-primary-dark-400-alpha-700': string
'--color-primary-dark-400-alpha-800': string
'--color-primary-dark-400-alpha-900': string
'--color-primary-dark-500': string
'--color-primary-dark-500-alpha-100': string
'--color-primary-dark-500-alpha-200': string
'--color-primary-dark-500-alpha-300': string
'--color-primary-dark-500-alpha-400': string
'--color-primary-dark-500-alpha-500': string
'--color-primary-dark-500-alpha-600': string
'--color-primary-dark-500-alpha-700': string
'--color-primary-dark-500-alpha-800': string
'--color-primary-dark-500-alpha-900': string
'--color-primary-dark-600': string
'--color-primary-dark-600-alpha-100': string
'--color-primary-dark-600-alpha-200': string
'--color-primary-dark-600-alpha-300': string
'--color-primary-dark-600-alpha-400': string
'--color-primary-dark-600-alpha-500': string
'--color-primary-dark-600-alpha-600': string
'--color-primary-dark-600-alpha-700': string
'--color-primary-dark-600-alpha-800': string
'--color-primary-dark-600-alpha-900': string
'--color-primary-dark-700': string
'--color-primary-dark-700-alpha-100': string
'--color-primary-dark-700-alpha-200': string
'--color-primary-dark-700-alpha-300': string
'--color-primary-dark-700-alpha-400': string
'--color-primary-dark-700-alpha-500': string
'--color-primary-dark-700-alpha-600': string
'--color-primary-dark-700-alpha-700': string
'--color-primary-dark-700-alpha-800': string
'--color-primary-dark-700-alpha-900': string
'--color-primary-dark-800': string
'--color-primary-dark-800-alpha-100': string
'--color-primary-dark-800-alpha-200': string
'--color-primary-dark-800-alpha-300': string
'--color-primary-dark-800-alpha-400': string
'--color-primary-dark-800-alpha-500': string
'--color-primary-dark-800-alpha-600': string
'--color-primary-dark-800-alpha-700': string
'--color-primary-dark-800-alpha-800': string
'--color-primary-dark-800-alpha-900': string
'--color-primary-dark-900': string
'--color-primary-dark-900-alpha-100': string
'--color-primary-dark-900-alpha-200': string
'--color-primary-dark-900-alpha-300': string
'--color-primary-dark-900-alpha-400': string
'--color-primary-dark-900-alpha-500': string
'--color-primary-dark-900-alpha-600': string
'--color-primary-dark-900-alpha-700': string
'--color-primary-dark-900-alpha-800': string
'--color-primary-dark-900-alpha-900': string
'--color-primary-dark-1000': string
'--color-primary-dark-1000-alpha-100': string
'--color-primary-dark-1000-alpha-200': string
'--color-primary-dark-1000-alpha-300': string
'--color-primary-dark-1000-alpha-400': string
'--color-primary-dark-1000-alpha-500': string
'--color-primary-dark-1000-alpha-600': string
'--color-primary-dark-1000-alpha-700': string
'--color-primary-dark-1000-alpha-800': string
'--color-primary-dark-1000-alpha-900': string
'--color-primary-light-100': string
'--color-primary-light-100-alpha-100': string
'--color-primary-light-100-alpha-200': string
'--color-primary-light-100-alpha-300': string
'--color-primary-light-100-alpha-400': string
'--color-primary-light-100-alpha-500': string
'--color-primary-light-100-alpha-600': string
'--color-primary-light-100-alpha-700': string
'--color-primary-light-100-alpha-800': string
'--color-primary-light-100-alpha-900': string
'--color-primary-light-200': string
'--color-primary-light-200-alpha-100': string
'--color-primary-light-200-alpha-200': string
'--color-primary-light-200-alpha-300': string
'--color-primary-light-200-alpha-400': string
'--color-primary-light-200-alpha-500': string
'--color-primary-light-200-alpha-600': string
'--color-primary-light-200-alpha-700': string
'--color-primary-light-200-alpha-800': string
'--color-primary-light-200-alpha-900': string
'--color-primary-light-300': string
'--color-primary-light-300-alpha-100': string
'--color-primary-light-300-alpha-200': string
'--color-primary-light-300-alpha-300': string
'--color-primary-light-300-alpha-400': string
'--color-primary-light-300-alpha-500': string
'--color-primary-light-300-alpha-600': string
'--color-primary-light-300-alpha-700': string
'--color-primary-light-300-alpha-800': string
'--color-primary-light-300-alpha-900': string
'--color-primary-light-400': string
'--color-primary-light-400-alpha-100': string
'--color-primary-light-400-alpha-200': string
'--color-primary-light-400-alpha-300': string
'--color-primary-light-400-alpha-400': string
'--color-primary-light-400-alpha-500': string
'--color-primary-light-400-alpha-600': string
'--color-primary-light-400-alpha-700': string
'--color-primary-light-400-alpha-800': string
'--color-primary-light-400-alpha-900': string
'--color-primary-light-500': string
'--color-primary-light-500-alpha-100': string
'--color-primary-light-500-alpha-200': string
'--color-primary-light-500-alpha-300': string
'--color-primary-light-500-alpha-400': string
'--color-primary-light-500-alpha-500': string
'--color-primary-light-500-alpha-600': string
'--color-primary-light-500-alpha-700': string
'--color-primary-light-500-alpha-800': string
'--color-primary-light-500-alpha-900': string
'--color-primary-light-600': string
'--color-primary-light-600-alpha-100': string
'--color-primary-light-600-alpha-200': string
'--color-primary-light-600-alpha-300': string
'--color-primary-light-600-alpha-400': string
'--color-primary-light-600-alpha-500': string
'--color-primary-light-600-alpha-600': string
'--color-primary-light-600-alpha-700': string
'--color-primary-light-600-alpha-800': string
'--color-primary-light-600-alpha-900': string
'--color-primary-light-700': string
'--color-primary-light-700-alpha-100': string
'--color-primary-light-700-alpha-200': string
'--color-primary-light-700-alpha-300': string
'--color-primary-light-700-alpha-400': string
'--color-primary-light-700-alpha-500': string
'--color-primary-light-700-alpha-600': string
'--color-primary-light-700-alpha-700': string
'--color-primary-light-700-alpha-800': string
'--color-primary-light-700-alpha-900': string
'--color-primary-light-800': string
'--color-primary-light-800-alpha-100': string
'--color-primary-light-800-alpha-200': string
'--color-primary-light-800-alpha-300': string
'--color-primary-light-800-alpha-400': string
'--color-primary-light-800-alpha-500': string
'--color-primary-light-800-alpha-600': string
'--color-primary-light-800-alpha-700': string
'--color-primary-light-800-alpha-800': string
'--color-primary-light-800-alpha-900': string
'--color-primary-light-900': string
'--color-primary-light-900-alpha-100': string
'--color-primary-light-900-alpha-200': string
'--color-primary-light-900-alpha-300': string
'--color-primary-light-900-alpha-400': string
'--color-primary-light-900-alpha-500': string
'--color-primary-light-900-alpha-600': string
'--color-primary-light-900-alpha-700': string
'--color-primary-light-900-alpha-800': string
'--color-primary-light-900-alpha-900': string
'--color-primary-light-1000': string
'--color-primary-light-1000-alpha-100': string
'--color-primary-light-1000-alpha-200': string
'--color-primary-light-1000-alpha-300': string
'--color-primary-light-1000-alpha-400': string
'--color-primary-light-1000-alpha-500': string
'--color-primary-light-1000-alpha-600': string
'--color-primary-light-1000-alpha-700': string
'--color-primary-light-1000-alpha-800': string
'--color-primary-light-1000-alpha-900': string
}
interface Theme {
id: string
name: string
isDark: boolean
isCustom: boolean
config: {
themeColors: ThemeColors
extInfo: {
'--color-app-background': string
'--color-main-background': string
'--color-nav-font': string
'--background-image': string
'--background-image-position': string
'--background-image-size': string
// 关闭按钮颜色
'--color-btn-hide': string
'--color-btn-min': string
'--color-btn-close': string
// 徽章颜色
'--color-badge-primary': string
'--color-badge-secondary': string
'--color-badge-tertiary': string
}
}
}
interface ThemeInfo {
themes: LX.Theme[]
userThemes: LX.Theme[]
dataPath: string
}
interface ThemeSetting {
shouldUseDarkColors: boolean
theme: {
id: string
name: string
isDark: boolean
colors: Record<string, string>
}
}
}
+56
View File
@@ -0,0 +1,56 @@
declare namespace LX {
namespace UserApi {
type UserApiSourceInfoType = 'music'
type UserApiSourceInfoActions = 'musicUrl'
interface UserApiSourceInfo {
name: string
type: UserApiSourceInfoType
actions: UserApiSourceInfoActions[]
qualitys: LX.Quality[]
}
type UserApiSources = Record<LX.Source, UserApiSourceInfo>
interface UserApiInfo {
id: string
name: string
description: string
script: string
allowShowUpdateAlert: boolean
sources?: UserApiSources
}
interface UserApiStatus {
status: boolean
message?: string
apiInfo?: UserApiInfo
}
interface UserApiUpdateInfo {
name: string
description: string
log: string
updateUrl?: string
}
interface UserApiRequestParams {
requestKey: string
data: any
}
type UserApiRequestCancelParams = string
type UserApiSetApiParams = string
interface UserApiSetAllowUpdateAlertParams {
id: string
enable: boolean
}
interface ImportUserApi {
apiInfo: UserApiInfo
apiList: UserApiInfo[]
}
}
}
+7
View File
@@ -0,0 +1,7 @@
type MakeOptional<Type, Key extends keyof Type> = Omit<Type, Key> & Partial<Pick<Type, Key>>
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
}
type Modify<T, R> = Omit<T, keyof R> & R
-222
View File
@@ -1,222 +0,0 @@
const log = require('electron-log')
const { defaultSetting, overwriteSetting } = require('./defaultSetting')
// const apiSource = require('../renderer/utils/music/api-source-info')
const getStore = require('./store')
const defaultHotKey = require('./defaultHotKey')
exports.isLinux = process.platform == 'linux'
exports.isWin = process.platform == 'win32'
exports.isMac = process.platform == 'darwin'
/**
* 生成节流函数
* @param {*} fn
* @param {*} delay
*/
exports.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
*/
exports.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)
}
}
exports.log = log
// https://stackoverflow.com/a/53387532
exports.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
}
exports.isObject = item => item && typeof item === 'object' && !Array.isArray(item)
/**
* 对象深度合并
* @param {} target 要合并源对象
* @param {} source 要合并目标对象
*/
exports.objectDeepMerge = (target, source, mergedObj) => {
if (!mergedObj) {
mergedObj = new Set()
mergedObj.add(target)
}
let base = {}
Object.keys(source).forEach(item => {
if (exports.isObject(source[item])) {
if (mergedObj.has(source[item])) return
if (!exports.isObject(target[item])) target[item] = {}
mergedObj.add(source[item])
exports.objectDeepMerge(target[item], source[item], mergedObj)
return
}
base[item] = source[item]
})
Object.assign(target, base)
}
exports.mergeSetting = (setting, version) => {
let defaultSettingCopy = JSON.parse(JSON.stringify(defaultSetting))
let overwriteSettingCopy = JSON.parse(JSON.stringify(overwriteSetting))
const defaultVersion = defaultSettingCopy.version
if (!version) {
if (setting) {
version = setting.version
delete setting.version
}
}
if (!setting) {
setting = defaultSettingCopy
} else if (exports.compareVer(version, defaultVersion) < 0) {
exports.objectDeepMerge(defaultSettingCopy, setting)
exports.objectDeepMerge(defaultSettingCopy, overwriteSettingCopy)
setting = defaultSettingCopy
}
// if (!apiSource.some(api => api.id === setting.apiSource && !api.disabled)) {
// let api = apiSource.find(api => !api.disabled)
// if (api) setting.apiSource = api.id
// }
return { setting, version: defaultVersion }
}
/**
* 初始化设置
* @param {*} setting
* @param {*} isShowErrorAlert
*/
exports.initSetting = isShowErrorAlert => {
const electronStore_list = getStore('playList', true, isShowErrorAlert)
const electronStore_config = getStore('config')
const electronStore_downloadList = getStore('downloadList')
let setting = electronStore_config.get('setting')
if (setting) {
let version = electronStore_config.get('version')
if (!version) { // 迁移配置
version = electronStore_config.get('setting.version')
electronStore_config.set('version', version)
electronStore_config.delete('setting.version')
const list = electronStore_config.get('list')
if (list) {
if (list.defaultList) electronStore_list.set('defaultList', list.defaultList)
if (list.loveList) electronStore_list.set('loveList', list.loveList)
electronStore_config.delete('list')
}
const downloadList = electronStore_config.get('download')
if (downloadList) {
if (downloadList.list) electronStore_downloadList.set('list', downloadList.list)
electronStore_config.delete('download')
}
}
// 迁移列表滚动位置设置 ~0.18.3
if (setting.list.scroll) {
let scroll = setting.list.scroll
electronStore_config.delete('setting.list.scroll')
electronStore_config.set('setting.list.isSaveScrollLocation', scroll.enable)
delete setting.list.scroll
}
}
// 从我的列表分离下载列表 v1.7.0 后
let downloadList = electronStore_list.get('downloadList')
if (downloadList) {
electronStore_downloadList.set('list', downloadList)
electronStore_list.delete('downloadList')
}
const { version: settingVersion, setting: newSetting } = exports.mergeSetting(setting, electronStore_config.get('version'))
// 修正拼写问题 v1.8.2 及以前
if (newSetting.player.isShowLyricTransition != null) {
newSetting.player.isShowLyricTranslation = newSetting.player.isShowLyricTransition
delete newSetting.player.isShowLyricTransition
}
// 迁移v1.19.0之前的主题设置
if (newSetting.themeId != null) {
newSetting.theme.id = newSetting.themeId
delete newSetting.themeId
}
// 重置 ^0.18.2 排行榜ID
if (!newSetting.leaderboard.tabId.includes('__')) newSetting.leaderboard.tabId = 'kw__16'
// newSetting.controlBtnPosition = 'right'
electronStore_config.set({ version: settingVersion, setting: newSetting })
return { version: settingVersion, setting: newSetting }
}
/**
* 初始化快捷键设置
*/
exports.initHotKey = () => {
const electronStore_hotKey = getStore('hotKey')
let localConfig = electronStore_hotKey.get('local')
if (!localConfig) {
localConfig = defaultHotKey.local
electronStore_hotKey.set('local', localConfig)
}
let globalConfig = electronStore_hotKey.get('global')
// 移除v1.0.1及之前设置的全局声音媒体快捷键接管
if (globalConfig && globalConfig.keys.VolumeUp) {
delete globalConfig.keys.VolumeUp
delete globalConfig.keys.VolumeDown
delete globalConfig.keys.VolumeMute
electronStore_hotKey.set('global', globalConfig)
}
if (!globalConfig) {
globalConfig = defaultHotKey.global
electronStore_hotKey.set('global', globalConfig)
}
return {
global: globalConfig,
local: localConfig,
}
}
+175
View File
@@ -0,0 +1,175 @@
// 非业务工具方法
/**
* 获取两个数之间的随机整数,大于等于min,小于max
* @param {*} min
* @param {*} max
*/
export const getRandom = (min: number, max: number): number => Math.floor(Math.random() * (max - min)) + min
export const sizeFormate = (size: number): string => {
// 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]}`
}
const numFix = (n: number): string => n < 10 ? (`0${n}`) : n.toString()
/**
* 日期格式化
* @param {*} date 时间
* @param {String} format 时间格式,默认YYYY-MM-DD hh:mm:ss
*/
export const dateFormat = (date: string | number | Date, format = 'YYYY-MM-DD hh:mm:ss') => {
if (typeof date != 'object') date = new Date(date)
return format
.replace('YYYY', date.getFullYear().toString())
.replace('MM', numFix(date.getMonth() + 1).toString())
.replace('DD', numFix(date.getDate()))
.replace('hh', numFix(date.getHours()))
.replace('mm', numFix(date.getMinutes()))
.replace('ss', numFix(date.getSeconds()))
}
export const formatPlayTime = (time: number) => {
let m = Math.trunc(time / 60)
let s = Math.trunc(time % 60)
return m == 0 && s == 0 ? '--/--' : numFix(m) + ':' + numFix(s)
}
export const formatPlayTime2 = (time: number) => {
let m = Math.trunc(time / 60)
let s = Math.trunc(time % 60)
return numFix(m) + ':' + numFix(s)
}
const encodeNames = {
'&nbsp;': ' ',
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&apos;': "'",
'&#039;': "'",
} as const
export const decodeName = (str: string | null = '') => {
return str?.replace(/(?:&amp;|&lt;|&gt;|&quot;|&apos;|&#039;|&nbsp;)/gm, (s: string) => encodeNames[s as keyof typeof encodeNames]) ?? ''
}
// 解析URL参数为对象
export const parseUrlParams = (str: string): Record<string, string> => {
const params: Record<string, string> = {}
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
}
/**
* 生成节流函数
* @param fn 回调
* @param delay 延迟
* @returns
*/
export function throttle<Args extends any[]>(fn: (...args: Args) => void | Promise<void>, delay = 100) {
let timer: NodeJS.Timeout | null = null
let _args: Args
return (...args: Args) => {
_args = args
if (timer) return
timer = setTimeout(() => {
timer = null
void fn(..._args)
}, delay)
}
}
/**
* 生成防抖函数
* @param fn 回调
* @param delay 延迟
* @returns
*/
export function debounce<Args extends any[]>(fn: (...args: Args) => void | Promise<void>, delay = 100) {
let timer: NodeJS.Timeout | null = null
let _args: Args
return (...args: Args) => {
_args = args
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
timer = null
void fn(..._args)
}, delay)
}
}
const fileNameRxp = /[\\/:*?#"<>|]/g
export const filterFileName = (name: string): string => name.replace(fileNameRxp, '')
// https://blog.csdn.net/xcxy2015/article/details/77164126#comments
/**
*
* @param a
* @param b
*/
export const similar = (a: string, b: string) => {
if (!a || !b) return 0
if (a.length > b.length) { // 保证 a <= b
let t = b
b = a
a = t
}
let al = a.length
let bl = b.length
let mp = [] // 一个表
let i, j, ai, lt, tmp // ai:字符串a的第i个字符。 lt:左上角的值。 tmp:暂存新的值。
for (i = 0; i <= bl; i++) mp[i] = i
for (i = 1; i <= al; i++) {
ai = a.charAt(i - 1)
lt = mp[0]
mp[0] = mp[0] + 1
for (j = 1; j <= bl; j++) {
tmp = Math.min(mp[j] + 1, mp[j - 1] + 1, lt + (ai == b.charAt(j - 1) ? 0 : 1))
lt = mp[j]
mp[j] = tmp
}
}
return 1 - (mp[bl] / bl)
}
/**
* 排序字符串
* @param arr
* @param data
*/
export const sortInsert = (arr: Array<{ num: number, data: any }>, data: { num: number, data: any }) => {
let key = data.num
let left = 0
let right = arr.length - 1
while (left <= right) {
let middle = Math.trunc((left + right) / 2)
if (key == arr[middle].num) {
left = middle
break
} else if (key < arr[middle].num) {
right = middle - 1
} else {
left = middle + 1
}
}
while (left > 0) {
if (arr[left - 1].num != key) break
left--
}
arr.splice(left, 0, data)
}
@@ -0,0 +1,183 @@
// https://github.com/tholman/cursor-effects/blob/master/src/bubbleCursor.js
class Particle {
constructor(x, y, fillStyle, strokeStyle) {
const lifeSpan = Math.floor(Math.random() * 60 + 60)
this.initialLifeSpan = lifeSpan //
this.lifeSpan = lifeSpan // ms
this.velocity = {
x: (Math.random() < 0.5 ? -1 : 1) * (Math.random() / 10),
y: -0.4 + Math.random() * -1,
}
this.position = { x, y }
this.fillStyle = fillStyle
this.strokeStyle = strokeStyle
this.baseDimension = 4
}
update(context, width) {
this.position.x += this.velocity.x
this.position.y += this.velocity.y
this.velocity.x += ((Math.random() < 0.5 ? -1 : 1) * 2) / 75
this.velocity.y -= Math.random() / 600
if (this.position.x >= width - 2 || this.position.x <= 2) this.lifeSpan = 0
else if (this.position.y <= 5) this.lifeSpan = 0
this.lifeSpan--
const scale =
0.2 + (this.initialLifeSpan - this.lifeSpan) / this.initialLifeSpan
context.fillStyle = this.fillStyle
context.strokeStyle = this.strokeStyle
context.beginPath()
context.arc(
this.position.x - (this.baseDimension / 2) * scale,
this.position.y - this.baseDimension / 2,
this.baseDimension * scale,
0,
2 * Math.PI,
)
context.stroke()
context.fill()
context.closePath()
}
}
export default class BubbleCursor {
constructor({ element, fillStyle = 'rgba(77, 175, 124, 0.1)', strokeStyle = 'rgba(77, 175, 124, 0.3)' } = {}) {
this.hasWrapperEl = element
this.element = this.hasWrapperEl || document.body
this.width = window.innerWidth
this.height = window.innerHeight
this.cursor = { x: this.width / 2, y: this.width / 2 }
this.particles = []
this.canvas = null
this.context = null
this.fillStyle = fillStyle
this.strokeStyle = strokeStyle
this.init()
}
init() {
this.canvas = document.createElement('canvas')
this.context = this.canvas.getContext('2d')
this.canvas.style.top = '0px'
this.canvas.style.left = '0px'
this.canvas.style.pointerEvents = 'none'
this.canvas.style.zIndex = 100
if (this.hasWrapperEl) {
this.canvas.style.position = 'absolute'
this.element.appendChild(this.canvas)
this.canvas.width = this.element.clientWidth
this.canvas.height = this.element.clientHeight
} else {
this.canvas.style.position = 'fixed'
document.body.appendChild(this.canvas)
this.canvas.width = this.width
this.canvas.height = this.height
}
this.bindEvents()
this.loop()
}
bindEvents() {
this.element.addEventListener('mousemove', this.onMouseMove)
this.element.addEventListener('touchmove', this.onTouchMove, { passive: true })
this.element.addEventListener('touchstart', this.onTouchMove, { passive: true })
window.addEventListener('resize', this.onWindowResize)
}
onWindowResize = (e) => {
this.width = window.innerWidth
this.height = window.innerHeight
if (this.hasWrapperEl) {
this.canvas.width = this.element.clientWidth
this.canvas.height = this.element.clientHeight
} else {
this.canvas.width = this.width
this.canvas.height = this.height
}
}
onTouchMove = (e) => {
if (e.touches.length > 0) {
for (let i = 0; i < e.touches.length; i++) {
this.addParticle(
e.touches[i].clientX,
e.touches[i].clientY,
)
}
}
}
onMouseMove = (e) => {
if (this.hasWrapperEl) {
const boundingRect = this.element.getBoundingClientRect()
this.cursor.x = e.clientX - boundingRect.left
this.cursor.y = e.clientY - boundingRect.top
} else {
this.cursor.x = e.clientX
this.cursor.y = e.clientY
}
this.addParticle(this.cursor.x, this.cursor.y)
}
addParticle(x, y) {
this.particles.push(new Particle(x, y, this.fillStyle, this.strokeStyle))
}
updateParticles() {
this.context.clearRect(0, 0, this.width, this.height)
// Update
for (let i = 0; i < this.particles.length; i++) {
this.particles[i].update(this.context, this.width)
}
// Remove dead particles
for (let i = this.particles.length - 1; i >= 0; i--) {
if (this.particles[i].lifeSpan < 0) {
this.particles.splice(i, 1)
}
}
}
loop = () => {
this.updateParticles()
window.requestAnimationFrame(this.loop)
}
setColor(fillStyle, strokeStyle) {
this.fillStyle = fillStyle
this.strokeStyle = strokeStyle
}
destroy() {
this.element.removeEventListener('mousemove', this.onMouseMove)
this.element.removeEventListener('touchmove', this.onTouchMove, { passive: true })
this.element.removeEventListener('touchstart', this.onTouchMove, { passive: true })
window.removeEventListener('resize', this.onWindowResize)
if (this.hasWrapperEl) {
this.element.removeChild(this.canvas)
} else {
document.body.removeChild(this.canvas)
}
this.canvas = null
this.context = null
}
}
@@ -1,70 +1,74 @@
import fs from 'fs'
import path from 'path'
import request from 'request'
import { EventEmitter } from 'events'
import { performance } from 'perf_hooks'
import { STATUS } from './util'
import http from 'http'
import { request, Options as RequestOptions } from './request'
export interface Options {
forceResume: boolean
requestOptions: RequestOptions
}
const defaultChunkInfo = {
path: null,
startByte: 0,
path: '',
startByte: '0',
endByte: '',
}
const defaultRequestOptions = {
method: 'GET',
const defaultRequestOptions: Options['requestOptions'] = {
method: 'get',
headers: {},
}
const defaultOptions = {
const defaultOptions: Options = {
forceResume: true,
requestOptions: { ...defaultRequestOptions },
}
class Task extends EventEmitter {
/**
*
* @param {String} url download url
* @param {Object} chunkInfo
* @param {Object} options
*/
constructor(url, savePath, filename, options = {}) {
resumeLastChunk: Buffer | null
downloadUrl: string
chunkInfo: { path: string, startByte: string, endByte: string }
status: typeof STATUS[keyof typeof STATUS]
options: Options
requestOptions: Options['requestOptions']
ws: fs.WriteStream | null = null
progress = { total: 0, downloaded: 0, speed: 0, progress: 0 }
statsEstimate = { time: 0, bytes: 0, prevBytes: 0 }
requestInstance: http.ClientRequest | null = null
constructor(url: string, savePath: string, filename: string, options: Partial<Options> = {}) {
super()
this.resumeLastChunk = null
this.downloadUrl = url
this.chunkInfo = Object.assign({}, defaultChunkInfo, {
path: path.join(savePath, filename),
startByte: 0,
startByte: '0',
})
if (!this.chunkInfo.endByte) this.chunkInfo.endByte = ''
// if (!this.chunkInfo.endByte) this.chunkInfo.endByte = ''
this.options = Object.assign({}, defaultOptions, options)
this.requestOptions = Object.assign({}, defaultRequestOptions, this.options.requestOptions || {})
if (!this.requestOptions.headers) this.requestOptions.headers = {}
this.requestOptions.headers = this.requestOptions.headers ? { ...this.requestOptions.headers } : {}
this.progress = {
total: 0,
downloaded: 0,
speed: 0,
}
this.statsEstimate = {
time: 0,
bytes: 0,
prevBytes: 0,
}
this.status = STATUS.idle
}
__init() {
this.status = STATUS.init
async __init() {
const { path, startByte, endByte } = this.chunkInfo
this.progress.downloaded = 0
if (startByte != null) this.requestOptions.headers.range = `bytes=${startByte}-${endByte}`
return new Promise((resolve, reject) => {
if (!path) return resolve()
this.progress.progress = 0
this.progress.speed = 0
if (startByte) this.requestOptions.headers!.range = `bytes=${startByte}-${endByte}`
if (!path) return
return new Promise<void>((resolve, reject) => {
fs.stat(path, (errStat, stats) => {
if (errStat) {
// console.log(errStat.code)
if (errStat.code !== 'ENOENT') {
this.__handleError(errStat)
reject(errStat)
@@ -94,7 +98,7 @@ class Task extends EventEmitter {
// console.log(buffer)
this.resumeLastChunk = buffer
this.progress.downloaded = stats.size
this.requestOptions.headers.range = `bytes=${stats.size - 10}-${endByte || ''}`
this.requestOptions.headers!.range = `bytes=${stats.size - 10}-${endByte || ''}`
resolve()
})
})
@@ -106,15 +110,15 @@ class Task extends EventEmitter {
})
}
__httpFetch(url, options) {
__httpFetch(url: string, options: Options['requestOptions']) {
// console.log(options)
this.request = request(url, options)
this.requestInstance = request(url, options)
.on('response', response => {
if (response.statusCode !== 200 && response.statusCode !== 206) {
if (response.statusCode == 416) {
fs.unlink(this.chunkInfo.path, async err => {
await this.__handleError(new Error(response.statusMessage))
this.chunkInfo.startByte = 0
fs.unlink(this.chunkInfo.path, (err) => {
this.__handleError(new Error(response.statusMessage))
this.chunkInfo.startByte = '0'
this.resumeLastChunk = null
this.progress.downloaded = 0
if (err) this.__handleError(err)
@@ -124,13 +128,13 @@ class Task extends EventEmitter {
this.status = STATUS.failed
this.emit('fail', response)
this.__closeRequest()
this.__closeWriteStream()
void this.__closeWriteStream()
return
}
this.emit('response', response)
try {
this.__initDownload(response)
} catch (error) {
} catch (error: any) {
return this.__handleError(error)
}
this.status = STATUS.running
@@ -142,34 +146,44 @@ class Task extends EventEmitter {
this.__handleComplete()
} else {
// this.__handleError(new Error('The connection was terminated while the message was still being sent'))
this.stop()
void this.stop()
}
})
})
.on('error', err => this.__handleError(err))
.on('close', () => this.__closeWriteStream())
.on('close', () => {
void this.__closeWriteStream()
})
.end()
}
__initDownload(response) {
this.progress.total = parseInt(response.headers['content-length'] || 0)
let options = {}
let isResumable = this.options.forceResume || response.headers['accept-ranges'] !== 'none' || (typeof response.headers['accept-ranges'] == 'string' && parseInt(response.headers['accept-ranges'].replace(/^bytes=(\d+)/, '$1')) > 0)
__initDownload(response: http.IncomingMessage) {
this.progress.total = response.headers['content-length'] ? parseInt(response.headers['content-length']) : 0
if (!this.progress.total) return this.__handleError(new Error('Content length is 0'))
let options: any = {}
let isResumable = this.options.forceResume ||
response.headers['accept-ranges'] !== 'none' ||
(typeof response.headers['accept-ranges'] == 'string' &&
parseInt(response.headers['accept-ranges'].replace(/^bytes=(\d+)/, '$1')) > 0)
if (isResumable) {
options.flags = 'a'
if (this.progress.downloaded) this.progress.total -= 10
} else {
if (this.chunkInfo.startByte > 0) return this.__handleError(new Error('The resource cannot be resumed download.'))
if (this.chunkInfo.startByte != '0') return this.__handleError(new Error('The resource cannot be resumed download.'))
}
this.progress.total += this.progress.downloaded
this.statsEstimate.prevBytes = this.progress.downloaded
if (!this.chunkInfo.path) return this.__handleError(new Error('Chunk save Path is not set.'))
this.ws = fs.createWriteStream(this.chunkInfo.path, options)
this.ws.on('finish', () => this.__closeWriteStream())
this.ws.on('finish', () => {
void this.__closeWriteStream()
})
this.ws.on('error', err => {
fs.unlink(this.chunkInfo.path, async unlinkErr => {
await this.__handleError(err)
this.chunkInfo.startByte = 0
fs.unlink(this.chunkInfo.path, (unlinkErr: any) => {
this.__handleError(err)
this.chunkInfo.startByte = '0'
this.resumeLastChunk = null
this.progress.downloaded = 0
if (unlinkErr && unlinkErr.code !== 'ENOENT') this.__handleError(unlinkErr)
@@ -179,7 +193,7 @@ class Task extends EventEmitter {
__handleComplete() {
if (this.status == STATUS.error) return
this.__closeWriteStream().then(() => {
void this.__closeWriteStream().then(() => {
if (this.progress.downloaded == this.progress.total) {
this.status = STATUS.completed
this.emit('completed')
@@ -188,21 +202,22 @@ class Task extends EventEmitter {
this.emit('stop')
}
})
console.log('end')
// console.log('end')
}
__handleError(error) {
__handleError(error: Error) {
if (this.status == STATUS.error) return
this.status = STATUS.error
this.__closeRequest()
this.__closeWriteStream()
void this.__closeWriteStream()
if (error.message == 'aborted') return
this.emit('error', error)
}
__closeWriteStream() {
return new Promise((resolve, reject) => {
async __closeWriteStream() {
return new Promise<void>((resolve, reject) => {
if (!this.ws) return resolve()
console.log('close write stream')
// console.log('close write stream')
this.ws.close(err => {
if (err) {
this.status = STATUS.error
@@ -217,18 +232,28 @@ class Task extends EventEmitter {
}
__closeRequest() {
if (!this.request) return
console.log('close request')
this.request.abort()
this.request = null
if (!this.requestInstance || this.requestInstance.destroyed) return
// console.log('close request')
this.requestInstance.destroy()
this.requestInstance = null
}
__handleWriteData(chunk) {
__handleWriteData(chunk: Buffer) {
if (this.resumeLastChunk) {
chunk = this.__handleDiffChunk(chunk)
if (!chunk) {
const result = this.__handleDiffChunk(chunk)
if (result) chunk = result
else {
this.__handleStop().finally(() => {
this.__handleError(new Error('Resume failed, response chunk does not match.'))
// this.__handleError(new Error('Resume failed, response chunk does not match.'))
// Resume failed, response chunk does not match, remove file and restart download
console.log('Resume failed, response chunk does not match.')
fs.unlink(this.chunkInfo.path, (unlinkErr: any) => {
// this.__handleError(err)
this.chunkInfo.startByte = '0'
this.resumeLastChunk = null
if (unlinkErr && unlinkErr.code !== 'ENOENT') return this.__handleError(unlinkErr)
void this.start()
})
})
return
}
@@ -240,35 +265,32 @@ class Task extends EventEmitter {
if (!err) return
console.log(err)
this.__handleError(err)
this.stop()
void this.stop()
})
}
__handleDiffChunk(chunk) {
__handleDiffChunk(chunk: Buffer): Buffer | null {
// console.log('diff', chunk)
let resumeLastChunkLen = this.resumeLastChunk.length
let resumeLastChunkLen = this.resumeLastChunk!.length
let chunkLen = chunk.length
let isOk
if (chunkLen >= resumeLastChunkLen) {
isOk = chunk.slice(0, resumeLastChunkLen).toString('hex') === this.resumeLastChunk.toString('hex')
isOk = chunk.slice(0, resumeLastChunkLen).toString('hex') === this.resumeLastChunk!.toString('hex')
if (!isOk) return null
this.resumeLastChunk = null
return chunk.slice(resumeLastChunkLen)
} else {
isOk = chunk.slice(0, chunkLen).toString('hex') === this.resumeLastChunk.slice(0, chunkLen).toString('hex')
isOk = chunk.slice(0, chunkLen).toString('hex') === this.resumeLastChunk!.slice(0, chunkLen).toString('hex')
if (!isOk) return null
this.resumeLastChunk = this.resumeLastChunk.slice(chunkLen)
this.resumeLastChunk = this.resumeLastChunk!.slice(chunkLen)
return chunk.slice(chunkLen)
}
}
__handleStop() {
return new Promise((resolve, reject) => {
if (this.request) {
this.request.abort()
this.request = null
}
async __handleStop() {
return new Promise<void>((resolve, reject) => {
this.__closeRequest()
if (this.ws) {
this.ws.close(err => {
if (err) {
@@ -285,7 +307,7 @@ class Task extends EventEmitter {
})
}
__calculateProgress(receivedBytes) {
__calculateProgress(receivedBytes: number) {
const currentTime = performance.now()
const elaspsedTime = currentTime - this.statsEstimate.time
@@ -309,24 +331,26 @@ class Task extends EventEmitter {
}
async start() {
this.status = STATUS.running
this.status = STATUS.init
await this.__init()
if (this.status !== STATUS.init) return
this.status = STATUS.running
this.__httpFetch(this.downloadUrl, this.requestOptions)
this.emit('start')
}
async stop() {
if (this.status === STATUS.stopped) return
if (this.status == STATUS.stopped || this.status == STATUS.completed) return
this.status = STATUS.stopped
await this.__handleStop()
this.emit('stop')
}
refreshUrl(url) {
refreshUrl(url: string) {
this.downloadUrl = url
}
updateSaveInfo(filePath, fileName) {
updateSaveInfo(filePath: string, fileName: string) {
this.chunkInfo.path = path.join(filePath, fileName)
}
}
+100
View File
@@ -0,0 +1,100 @@
import Downloader, { Options as DownloaderOptions } from './Downloader'
import { getRequestAgent } from './util'
import { sizeFormate } from '@common/utils'
import http from 'http'
// these are the default options
// const options = {
// method: 'GET', // Request Method Verb
// // Custom HTTP Header ex: Authorization, User-Agent
// headers: {},
// fileName: '', // Custom filename when saved
// override: false, // if true it will override the file, otherwise will append '(number)' to the end of file
// forceResume: false, // If the server does not return the "accept-ranges" header, can be force if it does support it
// // httpRequestOptions: {}, // Override the http request options
// // httpsRequestOptions: {}, // Override the https request options, ex: to add SSL Certs
// }
export interface Options {
url: string
path: string
fileName: string
method?: DownloaderOptions['requestOptions']['method']
headers?: DownloaderOptions['requestOptions']['headers']
forceResume?: boolean
proxy?: { host: string, port: number }
onCompleted?: () => void
onError?: (error: Error) => void
onFail?: (response: http.IncomingMessage) => void
onStart?: () => void
onStop?: () => void
onProgress?: (progress: LX.Download.ProgressInfo) => void
}
const noop = () => {}
export const createDownload = ({
url,
path,
fileName,
method = 'get',
forceResume,
proxy,
// resumeTime = 5000,
onCompleted = noop,
onError = noop,
onFail = noop,
onStart = noop,
onStop = noop,
onProgress = noop,
}: Options) => {
const dl = new Downloader(url, path, fileName, {
requestOptions: {
method,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
},
agent: getRequestAgent(url, proxy),
timeout: 60 * 1000,
},
forceResume,
})
dl.on('completed', () => {
onCompleted()
}).on('error', (err: any) => {
if (err.message === 'socket hang up') return
onError(err)
}).on('start', () => {
onStart()
// pauseResumeTimer(dl, resumeTime)
}).on('progress', (stats) => {
const speed = sizeFormate(stats.speed)
onProgress({
progress: parseInt(stats.progress.toFixed(2)),
speed,
downloaded: stats.downloaded,
total: stats.total,
})
// if (debugDownload) {
// const downloaded = sizeFormate(stats.downloaded)
// const total = sizeFormate(stats.total)
// console.log(`${speed}/s - ${progress}% [${downloaded}/${total}]`)
// }
}).on('stop', () => {
onStop()
// debugDownload && console.log('paused')
}).on('fail', resp => {
onFail(resp)
// debugDownload && console.log('fail')
})
// debugDownload && console.log('Downloading: ', url)
dl.start().catch(err => {
onError(err)
})
return dl
}
export type DownloaderType = Downloader
+73
View File
@@ -0,0 +1,73 @@
import { URL } from 'url'
import http from 'http'
import https from 'https'
export interface Options {
method: 'get' | 'head' | 'delete' | 'patch' | 'post' | 'put'
params?: Record<string, string>
// body?: Record<string, string>
headers?: Record<string, string>
timeout?: number
agent?: http.Agent
}
const defaultOptions: Options = {
method: 'get',
}
type HttpCallback = (res: http.IncomingMessage) => void
const sendRequest = (url: string, options: Options, callback?: HttpCallback) => {
const urlParse = new URL(url)
const httpOptions: http.RequestOptions | https.RequestOptions = {
host: urlParse.hostname,
port: urlParse.port,
path: urlParse.pathname + urlParse.search,
method: options.method,
}
if (options.params) {
(httpOptions.path as string) += `${urlParse.search ? '&' : '?'}${Object.entries(options.params)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join('&')}`
}
if (options.headers) httpOptions.headers = { ...options.headers }
if (options.agent) httpOptions.agent = options.agent
return urlParse.protocol == 'https:'
? https.request(httpOptions, callback)
: http.request(httpOptions, callback)
}
const applyTimeout = (request: http.ClientRequest, time: number) => {
let timeout: NodeJS.Timeout | null = setTimeout(() => {
timeout = null
if (request.destroyed) return
request.destroy(new Error('Request timeout'))
}, time)
request.on('response', () => {
if (!timeout) return
clearTimeout(timeout)
timeout = null
})
}
// const isRequireRedirect = (response: http.IncomingMessage) => {
// return response.statusCode &&
// response.statusCode > 300 &&
// response.statusCode < 400 &&
// Object.hasOwn(response.headers, 'location') &&
// response.headers.location
// }
// export function request(url: string, callback: HttpCallback)
// export function request(url: string, options: Partial<Options>, callback: HttpCallback)
export function request(url: string, _options: Partial<Options>, callback?: HttpCallback) {
let options: Options = { ...defaultOptions, ..._options }
const request = sendRequest(url, options, callback)
if (options.timeout) applyTimeout(request, options.timeout)
return request
}
+26
View File
@@ -0,0 +1,26 @@
import { httpOverHttp, httpsOverHttp } from 'tunnel'
export const STATUS = {
idle: 'IDLE',
init: 'INIT',
running: 'RUNNING',
paused: 'PAUSED',
stopped: 'STOPPED',
completed: 'COMPLETED',
error: 'ERROR',
failed: 'FAILED',
} as const
const httpsRxp = /^https:/
export const getRequestAgent = (url: string, proxy?: { host: string, port: number }) => {
let options
if (proxy) {
options = {
proxy: {
host: proxy.host,
port: proxy.port,
},
}
}
return options ? (httpsRxp.test(url) ? httpsOverHttp : httpOverHttp)(options) : undefined
}
+38
View File
@@ -0,0 +1,38 @@
import { shell, clipboard } from 'electron'
/**
* 在资源管理器中打开目录
* @param {string} dir
*/
export const openDirInExplorer = (dir: string) => {
shell.showItemInFolder(dir)
}
/**
* 在浏览器打开URL
* @param {*} url
*/
export const openUrl = async(url: string) => {
if (!/^https?:\/\//.test(url)) return
await shell.openExternal(url)
}
/**
* 复制文本到剪贴板
* @param str
*/
export const clipboardWriteText = (str: string) => {
clipboard.writeText(str)
}
/**
* 从剪贴板读取文本
* @returns
*/
export const clipboardReadText = (): string => {
return clipboard.readText()
}
+40
View File
@@ -0,0 +1,40 @@
import log from 'electron-log'
log.transports.file.level = 'info'
export const isLinux = process.platform == 'linux'
export const isWin = process.platform == 'win32'
export const isMac = process.platform == 'darwin'
export const isProd = process.env.NODE_ENV == 'production'
// https://stackoverflow.com/a/53387532
export function compareVer(currentVer: string, targetVer: string): -1 | 0 | 1 {
// treat non-numerical characters as lower version
// replacing them with a negative number based on charcode of each character
const fix = (s: string) => `.${s.toLowerCase().charCodeAt(0) - 2147483647}.`
const currentVerArr: Array<string | number> = ('' + currentVer).replace(/[^0-9.]/g, fix).split('.')
const targetVerArr: Array<string | number> = ('' + targetVer).replace(/[^0-9.]/g, fix).split('.')
let c = Math.max(currentVerArr.length, targetVerArr.length)
for (let i = 0; i < c; i++) {
// convert to integer the most efficient way
currentVerArr[i] = ~~currentVerArr[i]
targetVerArr[i] = ~~targetVerArr[i]
if (currentVerArr[i] > targetVerArr[i]) return 1
else if (currentVerArr[i] < targetVerArr[i]) return -1
}
return 0
}
export const encodePath = (path: string) => {
// https://github.com/lyswhut/lx-music-desktop/issues/963
return path.replaceAll('%', '%25')
}
export {
log,
}
export * from './common'
@@ -2,14 +2,20 @@ const { getNow, TimeoutTools } = require('./utils')
// const fontFormateRxp = /(?=<\d+,\d+>).*?/g
const fontSplitRxp = /(?=<\d+,\d+>).*?/g
const timeRxpAll = /<(\d+),(\d+)>/g
const timeRxp = /<(\d+),(\d+)>/
// Create animation
const createAnimation = (dom, duration) => new window.Animation(new window.KeyframeEffect(dom, [
{ backgroundSize: '0 100%' },
{ backgroundSize: '100% 100%' },
], {
const createAnimation = (dom, duration, isVertical) => new window.Animation(new window.KeyframeEffect(dom, isVertical
? [
{ backgroundSize: '100% 0' },
{ backgroundSize: '100% 100%' },
]
: [
{ backgroundSize: '0 100%' },
{ backgroundSize: '100% 100%' },
], {
duration,
easing: 'linear',
},
@@ -20,25 +26,45 @@ const createAnimation = (dom, duration) => new window.Animation(new window.Keyfr
// https://jsfiddle.net/ceqpnbky/1/
module.exports = class FontPlayer {
constructor({ time = 0, lyric = '', extendedLyrics = '', lineClassName = '', fontClassName = '', extendedLrcClassName = '', lineModeClassName = '', shadowContent = false, shadowClassName = '' }) {
constructor({
time = 0,
lyric = '',
lineContentClassName = 'line-content',
lineClassName = 'line',
shadowClassName = 'shadow',
fontModeClassName = 'font-mode',
lineModeClassName = 'line-mode',
fontLrcClassName = 'font-lrc',
extendedLrcClassName = 'extended',
shadowContent = false,
extendedLyrics = [],
isVertical = false,
}) {
this.time = time
this.lyric = lyric
this.extendedLyrics = extendedLyrics
this.isVertical = isVertical
this.lineContentClassName = lineContentClassName
this.lineClassName = lineClassName
this.fontClassName = fontClassName
this.extendedLrcClassName = extendedLrcClassName
this.lineModeClassName = lineModeClassName
this.shadowContent = shadowContent
this.shadowClassName = shadowClassName
this.extendedLyrics = extendedLyrics
this.fontModeClassName = fontModeClassName
this.fontLrcClassName = fontLrcClassName
this.extendedLrcClassName = extendedLrcClassName
this.lineModeClassName = lineModeClassName
this.isPlay = false
this.curFontNum = 0
this.maxFontNum = 0
this._performanceTime = 0
this._startTime = 0
this.fontContent = null
this.lineContent = null
this.timeoutTools = new TimeoutTools(80)
this.waitPlayTimeout = new TimeoutTools(80)
@@ -53,32 +79,43 @@ module.exports = class FontPlayer {
this.lineContent = document.createElement('div')
this.lineContent.time = this.time
if (this.lineClassName) this.lineContent.classList.add(this.lineClassName)
this.fontContent = document.createElement('div')
this.fontContent.style = 'position:relative;display:inline-block;'
if (this.fontClassName) this.fontContent.classList.add(this.fontClassName)
if (this.shadowContent) {
this.fontShadowContent = document.createElement('div')
this.fontShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
this.fontShadowContent.className = this.shadowClassName
this.fontContent.appendChild(this.fontShadowContent)
}
this.lineContent.appendChild(this.fontContent)
this.lineContent.className = this.lineContentClassName
this.line = document.createElement('div')
this.line.style = 'position:relative;display:inline-block;'
this.line.className = this.lineClassName
this.lineContent.appendChild(this.line)
this.lrcContent = document.createElement('div')
this.lrcContent.className = this.fontLrcClassName
// if (this.shadowContent) {
// this.lrcShadowContent = document.createElement('div')
// this.lrcShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
// this.lrcShadowContent.className = this.shadowClassName
// this.line.appendChild(this.lrcShadowContent)
// }
this.line.appendChild(this.lrcContent)
for (const lrc of this.extendedLyrics) {
const extendedLrcContent = document.createElement('div')
extendedLrcContent.style = 'position:relative;display:inline-block;'
extendedLrcContent.className = this.extendedLrcClassName
extendedLrcContent.textContent = lrc
this.lineContent.appendChild(document.createElement('br'))
this.lineContent.appendChild(extendedLrcContent)
if (this.shadowContent) {
const extendedLrcShadowContent = document.createElement('div')
extendedLrcShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
extendedLrcShadowContent.className = this.shadowClassName
extendedLrcShadowContent.textContent = lrc
extendedLrcContent.appendChild(extendedLrcShadowContent)
}
// if (this.shadowContent) {
// const extendedLrcShadowContent = document.createElement('div')
// extendedLrcShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
// extendedLrcShadowContent.className = this.shadowClassName
// extendedLrcShadowContent.textContent = lrc
// extendedLrcContent.appendChild(extendedLrcShadowContent)
// }
const lineContent = document.createElement('div')
lineContent.className = this.fontLrcClassName
lineContent.textContent = lrc.replace(timeRxpAll, '')
extendedLrcContent.appendChild(lineContent)
}
this._parseLyric()
}
@@ -90,21 +127,24 @@ module.exports = class FontPlayer {
this.maxFontNum = fonts.length - 1
this.fonts = []
let text
// let lineText = ''
let lrcShadowContent
for (const font of fonts) {
text = font.replace(timeRxp, '')
if (RegExp.$2 == '') return this._handleLineParse()
const time = parseInt(RegExp.$2)
const dom = document.createElement('span')
let shadowDom
dom.textContent = text
const animation = createAnimation(dom, time)
this.fontContent.appendChild(dom)
const animation = createAnimation(dom, time, this.isVertical)
this.lrcContent.appendChild(dom)
// lineText += text
if (this.shadowContent) {
shadowDom = document.createElement('span')
if (!lrcShadowContent) lrcShadowContent = document.createElement('div')
const shadowDom = document.createElement('span')
shadowDom.textContent = text
this.fontShadowContent.appendChild(shadowDom)
lrcShadowContent.appendChild(shadowDom)
}
// dom.style = shadowDom.style = this.fontStyle
// dom.className = shadowDom.className = this.fontClassName
@@ -114,29 +154,34 @@ module.exports = class FontPlayer {
startTime: parseInt(RegExp.$1),
time,
dom,
shadowDom,
animation,
})
}
if (this.shadowContent && lrcShadowContent) {
lrcShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
lrcShadowContent.className = this.shadowClassName
this.line.appendChild(lrcShadowContent)
}
this.line.appendChild(this.lrcContent)
this.fonts.at(-1)?.animation.addEventListener('finish', () => {
this.lineContent.classList.add('played')
this.isPlay = false
})
this.lineContent.classList.add(this.fontModeClassName)
// if (this.shadowContent) this.lrcShadowContent.textContent = lineText
// console.log(this.fonts)
}
_handleLineParse() {
this.isLineMode = true
const dom = document.createElement('span')
let shadowDom
dom.classList.add(this.lineModeClassName)
dom.textContent = this.lyric
if (this.shadowContent) {
shadowDom = document.createElement('span')
shadowDom.textContent = this.lyric
this.fontShadowContent.appendChild(shadowDom)
}
this.fontContent.appendChild(dom)
this.lineContent.classList.add(this.lineModeClassName)
this.lrcContent.textContent = this.lyric
// if (this.shadowContent) this.lrcShadowContent.textContent = this.lyric
this.fonts.push({
text: this.lyric,
dom,
shadowDom,
})
}
@@ -157,10 +202,11 @@ module.exports = class FontPlayer {
const driftTime = currentTime - curFont.startTime
if (currentTime > curFont.startTime + curFont.time) {
this._handlePlayFont(curFont, driftTime, true)
this.lineContent.classList.add('played')
this.isPlay = false
this.pause()
} else {
this._handlePlayFont(curFont, driftTime)
this.isPlay = false
}
}
@@ -185,7 +231,12 @@ module.exports = class FontPlayer {
_handlePlayLine(isPlayed) {
this.isPlay = false
this.fonts[0].dom.style.backgroundSize = isPlayed ? '100% 100%' : '100% 0'
if (isPlayed) {
this.lineContent.classList.add('played')
} else {
this.lineContent.classList.remove('played')
}
// this.fonts[0].dom.style.backgroundSize = isPlayed ? '100% 100%' : '100% 0'
}
_handlePauseFont(font) {
@@ -246,6 +297,7 @@ module.exports = class FontPlayer {
this.pause()
if (this.isLineMode) return this._handlePlayLine(true)
this.lineContent.classList.remove('played')
this.isPlay = true
this._performanceTime = getNow()
this._startTime = curTime
@@ -281,6 +333,7 @@ module.exports = class FontPlayer {
finish() {
this.pause()
if (this.isLineMode) return this._handlePlayLine(true)
this.lineContent.classList.add('played')
for (const font of this.fonts) {
font.animation.cancel()
@@ -292,6 +345,7 @@ module.exports = class FontPlayer {
reset() {
this.pause()
if (this.isLineMode) return this._handlePlayLine(false)
this.lineContent.classList.remove('played')
for (const font of this.fonts) {
font.animation.cancel()
font.dom.style.backgroundSize = '0 100%'
@@ -8,33 +8,46 @@ module.exports = class Lyric {
lyric = '',
extendedLyrics = [],
offset = 0,
lineClassName = '',
fontClassName = 'font',
lineContentClassName = 'line-content',
lineClassName = 'line',
shadowClassName = 'shadow',
fontModeClassName = 'font-mode',
lineModeClassName = 'line-mode',
fontLrcClassName = 'font-lrc',
extendedLrcClassName = 'extended',
activeLineClassName = 'active',
lineModeClassName = 'line',
shadowClassName = '',
shadowContent = false,
onPlay = function() { },
onSetLyric = function() { },
isVertical = false,
onPlay = function(line, text) { },
onSetLyric = function(lines, offset) { },
onUpdateLyric = function(lines) { },
}) {
this.lyric = lyric
this.extendedLyrics = extendedLyrics
this.offset = offset
this.onPlay = onPlay
this.onSetLyric = onSetLyric
this.onUpdateLyric = onUpdateLyric
this.lineContentClassName = lineContentClassName
this.lineClassName = lineClassName
this.fontClassName = fontClassName
this.shadowClassName = shadowClassName
this.fontModeClassName = fontModeClassName
this.lineModeClassName = lineModeClassName
this.fontLrcClassName = fontLrcClassName
this.extendedLrcClassName = extendedLrcClassName
this.activeLineClassName = activeLineClassName
this.lineModeClassName = lineModeClassName
this.shadowClassName = shadowClassName
this.shadowContent = shadowContent
this.isVertical = isVertical
this.playingLineNum = -1
this.isLineMode = false
this.initInfo = {
lines: [],
offset: 0,
}
this.linePlayer = new LinePlayer({
offset: this.offset,
onPlay: this._handleLinePlayerOnPlay,
@@ -93,7 +106,7 @@ module.exports = class Lyric {
this.onPlay(num, this._lines[num].text)
}
_handleLinePlayerOnSetLyric = (lyricLines, offset) => {
_initLines = (lyricLines, offset, isUpdate) => {
// console.log(lyricLines)
// this._lines = lyricsLines
this.isLineMode = lyricLines.length && !/^<\d+,\d+>/.test(lyricLines[0].text)
@@ -105,12 +118,15 @@ module.exports = class Lyric {
time: line.time,
lyric: line.text,
extendedLyrics: line.extendedLyrics,
lineContentClassName: this.lineContentClassName,
lineClassName: this.lineClassName,
fontClassName: this.fontClassName,
extendedLrcClassName: this.extendedLrcClassName,
lineModeClassName: this.lineModeClassName,
shadowClassName: this.shadowClassName,
fontModeClassName: this.fontModeClassName,
lineModeClassName: this.lineModeClassName,
fontLrcClassName: this.fontLrcClassName,
extendedLrcClassName: this.extendedLrcClassName,
shadowContent: this.shadowContent,
isVertical: this.isVertical,
})
this._lineFonts.push(fontPlayer)
@@ -127,11 +143,15 @@ module.exports = class Lyric {
time: line.time,
lyric: line.text,
extendedLyrics: line.extendedLyrics,
lineContentClassName: this.lineContentClassName,
lineClassName: this.lineClassName,
fontClassName: this.fontClassName,
extendedLrcClassName: this.extendedLrcClassName,
shadowClassName: this.shadowClassName,
fontModeClassName: this.fontModeClassName,
lineModeClassName: this.lineModeClassName,
fontLrcClassName: this.fontLrcClassName,
extendedLrcClassName: this.extendedLrcClassName,
shadowContent: this.shadowContent,
isVertical: this.isVertical,
})
this._lineFonts.push(fontPlayer)
@@ -148,7 +168,15 @@ module.exports = class Lyric {
let newOffset = this.isLineMode ? this.offset + 60 : this.offset
offset = offset - this.linePlayer.offset + newOffset
this.linePlayer.offset = newOffset
this.onSetLyric(this._lines, offset)
if (isUpdate) this.onUpdateLyric(this._lines)
else this.onSetLyric(this._lines, offset)
}
_handleLinePlayerOnSetLyric = (lyricLines, offset) => {
this._initLines(lyricLines, offset, false)
this.playingLineNum = 0
this.initInfo.lines = lyricLines
this.initInfo.offset = offset
}
play(curTime) {
@@ -159,7 +187,11 @@ module.exports = class Lyric {
pause() {
if (!this.linePlayer) return
this.linePlayer.pause()
if (this.playingLineNum > -1) this._lineFonts[this.playingLineNum].pause()
if (this.playingLineNum > -1) this._lineFonts[this.playingLineNum]?.pause()
}
setOffset(offset) {
this.linePlayer.offset = offset
}
setLyric(lyric, extendedLyrics) {
@@ -167,4 +199,14 @@ module.exports = class Lyric {
this.extendedLyrics = extendedLyrics
this._init()
}
setVertical(isVertical) {
this.isVertical = isVertical
this._initLines(this.initInfo.lines, this.initInfo.offset, true)
if (this.linePlayer.isPlay) {
const num = this.playingLineNum
this.playingLineNum = 0
this._handleLinePlayerOnPlay(num, '', this.linePlayer._currentTime())
} else this.playingLineNum = 0
}
}
@@ -1,6 +1,7 @@
const { getNow, TimeoutTools } = require('./utils')
const timeExp = /^\[([\d:.]*)\]{1}/g
const timeFieldExp = /^(?:\[[\d:.]+\])+/g
const timeExp = /[\d:.]+/g
const tagRegMap = {
title: 'ti',
artist: 'ar',
@@ -15,13 +16,18 @@ const parseExtendedLyric = (lrcLinesMap, extendedLyric) => {
const extendedLines = extendedLyric.split(/\r\n|\n|\r/)
for (let i = 0; i < extendedLines.length; i++) {
const line = extendedLines[i].trim()
let result = timeExp.exec(line)
let result = timeFieldExp.exec(line)
if (result) {
const text = line.replace(timeExp, '').trim()
const timeField = result[0]
const text = line.replace(timeFieldExp, '').trim()
if (text) {
const timeStr = RegExp.$1.replace(/(\.\d\d)0$/, '$1')
const targetLine = lrcLinesMap[timeStr]
if (targetLine) targetLine.extendedLyrics.push(text)
const times = timeField.match(timeExp)
if (times == null) continue
for (const time of times) {
const timeStr = time.replace(/(\.\d\d)0$/, '$1')
const targetLine = lrcLinesMap[timeStr]
if (targetLine) targetLine.extendedLyrics.push(text)
}
}
}
}
@@ -69,21 +75,30 @@ module.exports = class LinePlayer {
const linesMap = {}
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim()
let result = timeExp.exec(line)
let result = timeFieldExp.exec(line)
if (result) {
const text = line.replace(timeExp, '').trim()
const timeField = result[0]
const text = line.replace(timeFieldExp, '').trim()
if (text) {
const timeStr = RegExp.$1.replace(/(\.\d\d)0$/, '$1')
const timeArr = timeStr.split(':')
if (timeArr.length < 3) timeArr.unshift(0)
if (timeArr[2].indexOf('.') > -1) {
timeArr.push(...timeArr[2].split('.'))
timeArr.splice(2, 1)
}
linesMap[timeStr] = {
time: parseInt(timeArr[0]) * 60 * 60 * 1000 + parseInt(timeArr[1]) * 60 * 1000 + parseInt(timeArr[2]) * 1000 + parseInt(timeArr[3] || 0),
text,
extendedLyrics: [],
const times = timeField.match(timeExp)
if (times == null) continue
for (const time of times) {
const timeStr = time.replace(/(\.\d\d)0$/, '$1')
if (linesMap[timeStr]) {
linesMap[timeStr].extendedLyrics.push(text)
continue
}
const timeArr = timeStr.split(':')
if (timeArr.length < 3) timeArr.unshift(0)
if (timeArr[2].indexOf('.') > -1) {
timeArr.push(...timeArr[2].split('.'))
timeArr.splice(2, 1)
}
linesMap[timeStr] = {
time: parseInt(timeArr[0]) * 60 * 60 * 1000 + parseInt(timeArr[1]) * 60 * 1000 + parseInt(timeArr[2]) * 1000 + parseInt(timeArr[3] || 0),
text,
extendedLyrics: [],
}
}
}
}
@@ -17,7 +17,7 @@ exports.TimeoutTools = class TimeoutTools {
// console.log('diff', diff)
if (diff > 0) {
if (diff < this.thresholdTime) return this.run()
return this.timeoutId = setTimeout(() => {
return this.timeoutId = window.setTimeout(() => {
this.timeoutId = null
this.run()
}, diff - this.thresholdTime)
+139
View File
@@ -0,0 +1,139 @@
import { compareVer } from './index'
const oldThemeMap = {
0: 'green',
1: 'blue',
2: 'yellow',
3: 'orange',
4: 'red',
10: 'pink',
5: 'purple',
6: 'grey',
11: 'ming',
12: 'blue2',
13: 'black',
7: 'mid_autumn',
8: 'naruto',
9: 'happy_new_year',
} as const
export default (setting: any): Partial<LX.AppSetting> => {
setting = { ...setting }
// 迁移 v2 之前的配置
if (compareVer(setting.version, '2.0.0') < 0) {
// 迁移列表滚动位置设置 ~0.18.3
if (setting.list.scroll) {
let scroll = setting.list.scroll
if (setting.list?.isSaveScrollLocation) setting.list.isSaveScrollLocation = scroll.enable
delete setting.list.scroll
}
// 修正拼写问题 v1.8.2 及以前
if (setting.player.isShowLyricTransition != null) {
setting.player.isShowLyricTranslation = setting.player.isShowLyricTransition
delete setting.player.isShowLyricTransition
}
// 迁移v1.19.0之前的主题设置
if (setting.themeId != null) {
setting.theme = {
id: setting.themeId,
}
delete setting.themeId
}
setting.tray.enable = setting.tray.isShow
setting['common.windowSizeId'] = setting.windowSizeId
setting['common.startInFullscreen'] = setting.startInFullscreen
setting['common.langId'] = setting.langId
setting['common.apiSource'] = setting.apiSource
setting['common.sourceNameType'] = setting.sourceNameType
setting['common.font'] = setting.font
setting['common.isShowAnimation'] = setting.isShowAnimation
setting['common.randomAnimate'] = setting.randomAnimate
setting['common.isAgreePact'] = setting.isAgreePact
setting['common.controlBtnPosition'] = setting.controlBtnPosition
setting['player.togglePlayMethod'] = setting.player.togglePlayMethod
setting['player.highQuality'] = setting.player.highQuality
setting['player.isShowTaskProgess'] = setting.player.isShowTaskProgess
setting['player.volume'] = setting.player.volume
setting['player.isMute'] = setting.player.isMute
setting['player.mediaDeviceId'] = setting.player.mediaDeviceId
setting['player.isMediaDeviceRemovedStopPlay'] = setting.player.isMediaDeviceRemovedStopPlay
setting['player.isShowLyricTranslation'] = setting.player.isShowLyricTranslation
setting['player.isShowLyricRoma'] = setting.player.isShowLyricRoma
setting['player.isS2t'] = setting.player.isS2t
setting['player.isPlayLxlrc'] = setting.player.isPlayLxlrc
setting['player.isSavePlayTime'] = setting.player.isSavePlayTime
setting['player.audioVisualization'] = setting.player.audioVisualization
setting['player.waitPlayEndStop'] = setting.player.waitPlayEndStop
setting['player.waitPlayEndStopTime'] = setting.player.waitPlayEndStopTime
setting['player.autoSkipOnError'] = setting.player.autoSkipOnError
setting['playDetail.isZoomActiveLrc'] = setting.playDetail.isZoomActiveLrc
setting['playDetail.isShowLyricProgressSetting'] = setting.playDetail.isShowLyricProgressSetting
setting['playDetail.style.fontSize'] = setting.playDetail.style.fontSize
setting['playDetail.style.align'] = setting.playDetail.style.align
setting['desktopLyric.enable'] = setting.desktopLyric.enable
setting['desktopLyric.isLock'] = setting.desktopLyric.isLock
setting['desktopLyric.isAlwaysOnTop'] = setting.desktopLyric.isAlwaysOnTop
setting['desktopLyric.isAlwaysOnTopLoop'] = setting.desktopLyric.isAlwaysOnTopLoop
setting['desktopLyric.width'] = setting.desktopLyric.width
setting['desktopLyric.height'] = setting.desktopLyric.height
setting['desktopLyric.x'] = setting.desktopLyric.x
setting['desktopLyric.y'] = setting.desktopLyric.y
setting['desktopLyric.isLockScreen'] = setting.desktopLyric.isLockScreen
setting['desktopLyric.isDelayScroll'] = setting.desktopLyric.isDelayScroll
setting['desktopLyric.isHoverHide'] = setting.desktopLyric.isHoverHide
setting['desktopLyric.style.font'] = setting.desktopLyric.style.font
setting['desktopLyric.style.fontSize'] = setting.desktopLyric.style.fontSize / 100 * 16
setting['desktopLyric.style.opacity'] = setting.desktopLyric.style.opacity
setting['desktopLyric.style.isZoomActiveLrc'] = setting.desktopLyric.style.isZoomActiveLrc
setting['list.isClickPlayList'] = setting.list.isClickPlayList
setting['list.isShowAlbumName'] = setting.list.isShowAlbumName
setting['list.isShowSource'] = setting.list.isShowSource
setting['list.isSaveScrollLocation'] = setting.list.isSaveScrollLocation
setting['list.addMusicLocationType'] = setting.list.addMusicLocationType
setting['download.enable'] = setting.download.enable
setting['download.savePath'] = setting.download.savePath
setting['download.fileName'] = setting.download.fileName
setting['download.maxDownloadNum'] = setting.download.maxDownloadNum
setting['download.isDownloadLrc'] = setting.download.isDownloadLrc
setting['download.lrcFormat'] = setting.download.lrcFormat
setting['download.isEmbedPic'] = setting.download.isEmbedPic
setting['download.isEmbedLyric'] = setting.download.isEmbedLyric
setting['download.isUseOtherSource'] = setting.download.isUseOtherSource
setting['search.isShowHotSearch'] = setting.search.isShowHotSearch
setting['search.isShowHistorySearch'] = setting.search.isShowHistorySearch
setting['search.isFocusSearchBox'] = setting.search.isFocusSearchBox
setting['network.proxy.enable'] = setting.network.proxy.enable
setting['network.proxy.host'] = setting.network.proxy.host
setting['network.proxy.port'] = setting.network.proxy.port
setting['network.proxy.username'] = setting.network.proxy.username
setting['network.proxy.password'] = setting.network.proxy.password
setting['tray.enable'] = setting.tray.enable
setting['tray.themeId'] = setting.tray.themeId
setting['sync.enable'] = setting.sync.enable
setting['sync.port'] = setting.sync.port
setting['theme.id'] = oldThemeMap[setting.theme.id as keyof typeof oldThemeMap]
setting['theme.lightId'] = oldThemeMap[setting.theme.lightId as keyof typeof oldThemeMap]
setting['theme.darkId'] = oldThemeMap[setting.theme.darkId as keyof typeof oldThemeMap]
setting['odc.isAutoClearSearchInput'] = setting.odc.isAutoClearSearchInput
setting['odc.isAutoClearSearchList'] = setting.odc.isAutoClearSearchList
}
return setting
}
+71
View File
@@ -0,0 +1,71 @@
const http = require('http')
const https = require('https')
const fs = require('fs')
const sendRequest = (url) => {
const urlParse = new URL(url)
const httpOptions = {
method: 'get',
host: urlParse.hostname,
port: urlParse.port,
path: urlParse.pathname + urlParse.search,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
},
}
// console.log(httpOptions)
return url.protocol === 'https:'
? https.request(httpOptions)
: http.request(httpOptions)
}
module.exports = (url, filePath) => {
return new Promise((resolve) => {
sendRequest(url)
.on('response', response => {
// console.log(response.statusCode)
if (response.statusCode !== 200 && response.statusCode != 206) {
response.destroy(new Error('failed'))
return
}
response
.pipe(fs.createWriteStream(filePath))
.on('finish', () => {
// console.log('finish')
if (response.complete) {
// console.log('complete')
// meta.APIC = picPath
// handleWriteMeta(meta, filePath)
resolve(true)
} else {
resolve(false)
fs.unlink(filePath, err => {
if (err) console.log(err.message)
})
}
}).on('error', err => {
// console.log('response error')
if (err) console.log(err.message)
fs.unlink(filePath, err => {
if (err) console.log(err.message)
})
resolve(false)
})
})
.on('error', err => {
if (err) console.log(err.message)
// delete meta.APIC
// handleWriteMeta(meta, filePath)
resolve(false)
})
.end()
})
}
// const url = 'https://y.gtimg.cn/music/photo_new/T002R500x500M000000nfgwP0D6qxd.jpg'
// // const url = 'http://p4.music.126.net/-U2K8GKlASCSXK0cRre1gA==/109951163188718762.jpg'
// const picPath = require('path').join(__dirname, 'test.jpg')
// module.exports(url, picPath).then((sucee) => {
// console.log(sucee)
// })
@@ -2,9 +2,9 @@ const fs = require('fs')
const fsPromises = fs.promises
const path = require('path')
const getImgSize = require('image-size')
const request = require('request')
const download = require('./downloader')
const FlacProcessor = require('./flac-metadata')
const FlacProcessor = require('./flac-metadata/index')
const extReg = /^(\.(?:jpe?g|png)).*$/
const vendor = 'reference libFLAC 1.2.1 20070917'
@@ -67,29 +67,14 @@ module.exports = (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 => {
download(picUrl, picPath).then(success => {
if (success) {
writeMeta(filePath, meta, picPath).finally(() => {
fs.unlink(picPath, err => {
if (err) console.log(err.message)
writeMeta(filePath, meta)
})
})
.on('error', err => {
if (err) console.log(err.message)
writeMeta(filePath, meta)
})
})
} else writeMeta(filePath, meta)
})
}
+8
View File
@@ -0,0 +1,8 @@
export interface MusicMeta {
title: string
artist: string | null
album: string | null
APIC: string | null
lyrics: string | null
}
export function setMeta(filePath: string, meta: MusicMeta): void
+38
View File
@@ -0,0 +1,38 @@
const NodeID3 = require('node-id3')
const path = require('path')
const fs = require('fs')
const download = require('./downloader')
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')
download(meta.APIC, picPath).then(success => {
if (success) {
meta.APIC = picPath
handleWriteMeta(meta, filePath)
fs.unlink(picPath, err => {
if (err) console.log(err.message)
})
} else {
delete meta.APIC
handleWriteMeta(meta, filePath)
}
})
}
+159
View File
@@ -0,0 +1,159 @@
import fs from 'fs'
import crypto from 'crypto'
import { gzip, gunzip } from 'zlib'
import { log } from '@common/utils'
import path from 'path'
export const joinPath = (...paths: string[]): string => path.join(...paths)
export const extname = (p: string): string => path.extname(p)
export const basename = (p: string, ext?: string): string => path.basename(p, ext)
export const dirname = (p: string): string => path.dirname(p)
/**
* 检查路径是否存在
* @param {*} path 路径
*/
export const checkPath = async(path: string): Promise<boolean> => {
return await new Promise(resolve => {
fs.access(path, fs.constants.F_OK, err => {
if (err) return resolve(false)
resolve(true)
})
})
}
export const getFileStats = async(path: string): Promise<fs.Stats | null> => {
return await new Promise(resolve => {
fs.stat(path, (err, stats) => {
if (err) return resolve(null)
resolve(stats)
})
})
}
/**
* 检查路径并创建目录
* @param path
* @returns
*/
export const createDir = async(path: string): Promise<void> => {
return await new Promise((resolve, reject) => {
fs.access(path, fs.constants.F_OK | fs.constants.W_OK, err => {
if (err) {
if (err.code === 'ENOENT') {
fs.mkdir(path, { recursive: true }, err => {
if (err) return reject(err)
resolve()
})
return
}
return reject(err)
}
resolve()
})
})
}
export const removeFile = async(path: string) => new Promise<void>((resolve, reject) => {
fs.access(path, fs.constants.F_OK, err => {
if (err) return err.code == 'ENOENT' ? resolve() : reject(err)
fs.unlink(path, err => {
if (err) return reject(err)
resolve()
})
})
})
export const readFile = async(path: string) => fs.promises.readFile(path)
/**
* 创建 MD5 hash
* @param {*} str
*/
export const toMD5 = (str: string) => crypto.createHash('md5').update(str).digest('hex')
export const gzipData = async(str: string): Promise<Buffer> => {
return await new Promise((resolve, reject) => {
gzip(str, (err, result) => {
if (err) return reject(err)
resolve(result)
})
})
}
export const gunzipData = async(buf: Buffer): Promise<string> => {
return await new Promise((resolve, reject) => {
gunzip(buf, (err, result) => {
if (err) return reject(err)
resolve(result.toString())
})
})
}
/**
* 保存lx配置文件
* @param path 保存路径
* @param data 数据
*/
export const saveLxConfigFile = async(path: string, data: any) => {
if (!path.endsWith('.lxmc')) path += '.lxmc'
fs.writeFile(path, await gzipData(JSON.stringify(data)), 'binary', err => {
console.log(err)
})
}
/**
* 读取lx配置文件
* @param path 文件路径
* @returns 数据
*/
export const readLxConfigFile = async(path: string): Promise<any> => {
let isJSON = path.endsWith('.json')
let data: string | Buffer = await fs.promises.readFile(path, isJSON ? 'utf8' : 'binary')
if (!data || isJSON) return data
data = await gunzipData(Buffer.from(data, 'binary'))
data = JSON.parse(data)
// 修复v1.14.0出现的导出数据被序列化两次的问题
if (typeof data != 'object') {
try {
data = JSON.parse(data)
} catch (err) {
return data
}
}
return data
}
export const saveStrToFile = async(path: string, str: string | Buffer): Promise<void> => {
await new Promise<void>((resolve, reject) => {
fs.writeFile(path, str, err => {
if (err) {
log.error(err)
reject(err)
return
}
resolve()
})
})
}
export const b64DecodeUnicode = (str: string): string => {
// 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(''))
return Buffer.from(str, 'base64').toString()
}
export const copyFile = async(sourcePath: string, distPath: string) => {
return fs.promises.copyFile(sourcePath, distPath)
}
export const moveFile = async(sourcePath: string, distPath: string) => {
return fs.promises.rename(sourcePath, distPath)
}
File diff suppressed because it is too large Load Diff
+68
View File
@@ -0,0 +1,68 @@
const fs = require('fs').promises
const path = require('path')
const sourceFilePath = path.join(__dirname, './kMandarin_8105.txt')
const distFilePath = path.join(__dirname, './pinyin.txt')
const yuanyin = [
['ā', 'a'],
['á', 'a'],
['ǎ', 'a'],
['à', 'a'],
['ē', 'e'],
['é', 'e'],
['ě', 'e'],
['è', 'e'],
['ī', 'i'],
['í', 'i'],
['ǐ', 'i'],
['ì', 'i'],
['ō', 'o'],
['ó', 'o'],
['ǒ', 'o'],
['ò', 'o'],
['ū', 'u'],
['ú', 'u'],
['ǔ', 'u'],
['ù', 'u'],
['ǖ', 'v'],
['ǘ', 'v'],
['ǚ', 'v'],
['ǜ', 'v'],
]
const parse = async() => {
let datas = (await fs.readFile(sourceFilePath)).toString()
datas = datas.replace(/ +=> +(\w|\+)+ */gm, ' ')
for (const [y1, y2] of yuanyin) datas = datas.replaceAll(y1, y2)
// console.log(datas)
const lines = datas.split('\n')
const dict = {}
for (let line of lines) {
if (!line || line.startsWith('#')) continue
line = line.trim().replace(/^[\w+]+: */, '')
let [p1, comment] = line.split('#')
let [z, ps] = comment.split(/(?: *\? *-> *| *-> *)/)
const ys = new Set([p1.trim()])
if (ps != null) ps.split(/(?: +| *, *)/).forEach(y => ys.add(y.trim()))
dict[z.trim()] = Array.from(ys)
}
fs.writeFile(distFilePath, JSON.stringify(dict))
}
parse()
// let dict = {}
// let line = 'U+2CBBF: qi # 𬮿 ?-> gai,ai'
// line = line.trim().replace(/^[\w+]+: */, '')
// let [p1, comment] = line.split('#')
// let [z, ps] = comment.split(/ *\? *-> */)
// const ys = dict[z.trim()] = [p1.trim()]
// console.log(ps)
// if (ps != null) ys.push(...ps.split(/(?: +| *, *)/).map(y => y.trim()))
// console.log(dict)
File diff suppressed because one or more lines are too long
+241
View File
@@ -0,0 +1,241 @@
const easeInOutQuad = (t: number, b: number, c: number, d: number): number => {
t /= d / 2
if (t < 1) return (c / 2) * t * t + b
t--
return (-c / 2) * (t * (t - 2) - 1) + b
}
type Noop = () => void
const noop: Noop = () => {}
const handleScrollY = (element: HTMLElement, to: number, duration = 300, fn = noop): Noop => {
if (!element) {
fn()
return noop
}
// @ts-expect-error
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 {
fn()
return noop
}
const change = to - start
const increment = 10
if (!change) {
fn()
return noop
}
let currentTime = 0
let val
const animateScroll = () => {
currentTime += increment
val = Math.trunc(easeInOutQuad(currentTime, start, change, duration))
if (element.scrollTo) {
element.scrollTo(0, val)
} else {
element.scrollTop = val
}
if (currentTime < duration) {
if (cancel) return fn()
window.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: HTMLElement, to: number, duration = 300, fn = () => {}, delay = 0): () => void => {
let cancelFn: () => void
let timeout: number | null
if (delay) {
let scrollCancelFn: Noop
cancelFn = () => {
timeout == null ? scrollCancelFn?.() : clearTimeout(timeout)
}
timeout = window.setTimeout(() => {
timeout = null
scrollCancelFn = handleScrollY(element, to, duration, fn)
}, delay)
} else {
cancelFn = handleScrollY(element, to, duration, fn) ?? noop
}
return cancelFn
}
const handleScrollX = (element: HTMLElement, to: number, duration = 300, fn = () => {}): () => void => {
if (!element) {
fn()
return noop
}
// @ts-expect-error
const start = element.scrollLeft || element.scrollX || 0
let cancel = false
if (to > start) {
let maxScrollLeft = element.scrollWidth - element.clientWidth
if (to > maxScrollLeft) to = maxScrollLeft
} else if (to < start) {
if (to < 0) to = 0
} else {
fn()
return noop
}
const change = to - start
const increment = 10
if (!change) {
fn()
return noop
}
let currentTime = 0
let val
const animateScroll = () => {
currentTime += increment
val = Math.trunc(easeInOutQuad(currentTime, start, change, duration))
if (element.scrollTo) {
element.scrollTo(val, 0)
} else {
element.scrollLeft = val
}
if (currentTime < duration) {
if (cancel) return fn()
window.setTimeout(animateScroll, increment)
} else {
fn()
}
}
animateScroll()
return () => {
cancel = true
}
}
/**
* 设置滚动条位置
* @param {*} element 要设置滚动的容器 dom
* @param {*} to 滚动的目标位置
* @param {*} duration 滚动完成时间 ms
* @param {*} fn 滚动完成后的回调
* @param {*} delay 延迟执行时间
*/
export const scrollXTo = (element: HTMLElement, to: number, duration = 300, fn = () => {}, delay = 0): () => void => {
let cancelFn: Noop
let timeout: number | null
if (delay) {
let scrollCancelFn: Noop
cancelFn = () => {
timeout == null ? scrollCancelFn?.() : clearTimeout(timeout)
}
timeout = window.setTimeout(() => {
timeout = null
scrollCancelFn = handleScrollX(element, to, duration, fn)
}, delay)
} else {
cancelFn = handleScrollX(element, to, duration, fn)
}
return cancelFn
}
const handleScrollXR = (element: HTMLElement, to: number, duration = 300, fn = () => {}): () => void => {
if (!element) {
fn()
return noop
}
// @ts-expect-error
const start = element.scrollLeft || element.scrollX as number || 0
let cancel = false
if (to < start) {
let maxScrollLeft = -element.scrollWidth + element.clientWidth
if (to < maxScrollLeft) to = maxScrollLeft
} else if (to > start) {
if (to > 0) to = 0
} else {
fn()
return noop
}
const change = to - start
const increment = 10
if (!change) {
fn()
return noop
}
let currentTime = 0
let val
const animateScroll = () => {
currentTime += increment
val = Math.trunc(easeInOutQuad(currentTime, start, change, duration))
if (element.scrollTo) {
element.scrollTo(val, 0)
} else {
element.scrollLeft = val
}
if (currentTime < duration) {
if (cancel) return fn()
window.setTimeout(animateScroll, increment)
} else {
fn()
}
}
animateScroll()
return () => {
cancel = true
}
}
/**
* 设置滚动条位置 (writing-mode: vertical-rl 专用)
* @param element 要设置滚动的容器 dom
* @param to 滚动的目标位置
* @param duration 滚动完成时间 ms
* @param fn 滚动完成后的回调
* @param delay 延迟执行时间
*/
export const scrollXRTo = (element: HTMLElement, to: number, duration = 300, fn = () => {}, delay = 0): () => void => {
let cancelFn: Noop
let timeout: number | null
if (delay) {
let scrollCancelFn: Noop
cancelFn = () => {
timeout == null ? scrollCancelFn?.() : clearTimeout(timeout)
}
timeout = window.setTimeout(() => {
timeout = null
scrollCancelFn = handleScrollXR(element, to, duration, fn)
}, delay)
} else {
cancelFn = handleScrollXR(element, to, duration, fn)
}
return cancelFn
}
/**
* 设置标题
*/
let dom_title = document.getElementsByTagName('title')[0]
export const setTitle = (title: string | null) => {
if (!title) title = '洛雪音乐助手'
dom_title.innerText = title
}
+102
View File
@@ -0,0 +1,102 @@
// 业务工具方法
export const toNewMusicInfo = (oldMusicInfo: any): LX.Music.MusicInfoOnline => {
const meta: Record<string, any> = {
songId: oldMusicInfo.songmid, // 歌曲ID,mg源为copyrightId,local为文件路径
albumName: oldMusicInfo.albumName, // 歌曲专辑名称
albumId: oldMusicInfo.albumId,
picUrl: oldMusicInfo.img, // 歌曲图片链接
qualitys: oldMusicInfo.types,
_qualitys: oldMusicInfo._types,
}
if (meta._qualitys.flac32bit && !meta._qualitys.flac24bit) {
meta._qualitys.flac24bit = meta._qualitys.flac32bit
delete meta._qualitys.flac32bit
meta.qualitys = (meta.qualitys as any[]).map(quality => {
if (quality.type == 'flac32bit') quality.type = 'flac24bit'
return quality
})
}
switch (oldMusicInfo.source) {
case 'kg':
meta.hash = oldMusicInfo.hash
break
case 'tx':
meta.strMediaMid = oldMusicInfo.strMediaMid
meta.albumMid = oldMusicInfo.albumMid
break
case 'mg':
meta.copyrightId = oldMusicInfo.copyrightId
meta.lrcUrl = oldMusicInfo.lrcUrl
meta.mrcUrl = oldMusicInfo.mrcUrl
meta.trcUrl = oldMusicInfo.trcUrl
break
}
return {
id: `${oldMusicInfo.source as string}_${oldMusicInfo.songmid as string}`,
name: oldMusicInfo.name,
singer: oldMusicInfo.singer,
source: oldMusicInfo.source,
interval: oldMusicInfo.interval,
meta: meta as LX.Music.MusicInfoOnline['meta'],
}
}
export const toOldMusicInfo = (minfo: LX.Music.MusicInfoOnline) => {
const oInfo: Record<string, any> = {
name: minfo.name,
singer: minfo.singer,
source: minfo.source,
songmid: minfo.meta.songId,
albumId: minfo.meta.albumId,
interval: minfo.interval,
albumName: minfo.meta.albumName,
img: minfo.meta.picUrl,
types: minfo.meta.qualitys,
_types: minfo.meta._qualitys,
typeUrl: {},
}
switch (minfo.source) {
case 'kg':
oInfo.hash = minfo.meta.hash
break
case 'tx':
oInfo.strMediaMid = minfo.meta.strMediaMid
oInfo.albumMid = minfo.meta.albumMid
break
case 'mg':
oInfo.copyrightId = minfo.meta.copyrightId
oInfo.lrcUrl = minfo.meta.lrcUrl
oInfo.mrcUrl = minfo.meta.mrcUrl
oInfo.trcUrl = minfo.meta.trcUrl
break
}
return oInfo
}
/**
* 修复2.0.0-dev.8之前的新列表数据音质
* @param musicInfo
*/
export const fixNewMusicInfoQuality = (musicInfo: LX.Music.MusicInfo) => {
if (musicInfo.source == 'local') return musicInfo
// @ts-expect-error
if (musicInfo.meta._qualitys.flac32bit && !musicInfo.meta._qualitys.flac24bit) {
// @ts-expect-error
musicInfo.meta._qualitys.flac24bit = musicInfo.meta._qualitys.flac32bit
// @ts-expect-error
delete musicInfo.meta._qualitys.flac32bit
musicInfo.meta.qualitys = musicInfo.meta.qualitys.map(quality => {
// @ts-expect-error
if (quality.type == 'flac32bit') quality.type = 'flac24bit'
return quality
})
}
return musicInfo
}
+2
View File
@@ -0,0 +1,2 @@
export { useRouter, useRoute, onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router'
+85
View File
@@ -0,0 +1,85 @@
import {
ref,
reactive,
computed,
watch,
watchEffect,
nextTick,
onMounted,
onBeforeUnmount,
toRaw,
useCssModule,
toRef,
toRefs,
shallowRef,
unref,
markRaw,
ComputedRef,
Ref,
ShallowRef,
defineProps,
defineEmits,
defineComponent,
shallowReactive,
defineExpose,
} from 'vue'
// import { useStore } from 'vuex'
// export const useState = name => {
// const store = useStore()
// return store.state[name]
// }
// export const useGetter = (...names) => {
// const store = useStore()
// return store.getters[names.join('/')]
// }
// export const useRefGetter = (...names) => {
// const store = useStore()
// return computed(() => store.getters[names.join('/')])
// }
// export const useAction = (...names) => {
// const store = useStore()
// return params => {
// return store.dispatch(names.join('/'), params)
// }
// }
// export const useCommit = (...names) => {
// const store = useStore()
// return params => {
// return store.commit(names.join('/'), params)
// }
// }
export const markRawList = <T extends any[]>(list: T) => {
for (const item of list) {
markRaw(item)
}
return list
}
export {
nextTick,
onBeforeUnmount,
ref,
toRaw,
reactive,
watch,
watchEffect,
computed,
useCssModule,
toRef,
toRefs,
shallowRef,
unref,
onMounted,
markRaw,
ComputedRef,
Ref,
ShallowRef,
defineProps,
defineEmits,
defineComponent,
shallowReactive,
defineExpose,
}
+98 -11
View File
@@ -4,6 +4,9 @@
"alert_button_text": "All right",
"audio_visualization": "Audio visualization (experimental)",
"back": "Back",
"btn_cancel": "Cancel",
"btn_confirm": "Confirm",
"btn_save": "Save",
"cancel_button_text": "Cancel",
"close": "Close",
"comment__hot_load_error": "Hot comments failed to load, click to try to reload",
@@ -16,6 +19,7 @@
"comment__refresh": "Refresh comments",
"comment__show": "Song comments",
"comment__title": "{name} comment",
"comment__unavailable": "This song does not support getting comments",
"confirm_button_text": "Yes",
"copy_tip": " (Click to copy)",
"date_format_hour": "{num} hours ago",
@@ -26,8 +30,8 @@
"default_list": "Recently Played",
"desktop_lyric__back": "Back",
"desktop_lyric__close": "Close",
"desktop_lyric__font_decrease": "Reduce font size (Right click to fine-tune)",
"desktop_lyric__font_increase": "Increase font size (Right click to fine-tune)",
"desktop_lyric__font_decrease": "Reduce font size",
"desktop_lyric__font_increase": "Increase font size",
"desktop_lyric__lock": "Lock Lyrics",
"desktop_lyric__lrc_active_zoom_off": "Unzoom the currently playing lyrics",
"desktop_lyric__lrc_active_zoom_on": "Zoom the currently playing lyrics",
@@ -38,6 +42,11 @@
"desktop_lyric__win_top_off": "Cancel the top lyrics interface",
"desktop_lyric__win_top_on": "Top lyrics interface",
"download": "Downloads",
"download___status_complated": "Download completed",
"download___status_error": "task error",
"download___status_paused": "Pause downloads",
"download___status_runing": "downloading",
"download___status_wating": "waiting to download",
"download__all": "All Tasks",
"download__error": "Error",
"download__finished": "Download complete",
@@ -52,8 +61,20 @@
"download__quality": "Quality",
"download__runing": "Downloading",
"download__status": "Status",
"download_status_error_check_path": "There is an error in checking the download path, please check whether the set download directory is normal",
"download_status_error_refresh_url": "Link is dead, link is being refreshed",
"download_status_error_url_failed": "Failed to get music link",
"download_status_error_write": "The song save location is occupied or does not have write permission, please try to change the song save directory or restart the software or restart the computer, the error details:",
"download_status_start": "start download",
"download_status_url_geting": "Getting music link...",
"duplicate_list_tip": "You have favorited this list [{name}] before, do you need to update the songs in it?",
"export": "Export",
"fullscreen_exit": "Exit Full Screen",
"theme_edit_modal__copy": "Copy theme",
"theme_edit_modal__remove": "delete",
"theme_edit_modal__remove_tip": "Do you really want to remove this thread?",
"theme_edit_modal__save_new": "Save new",
"theme_edit_modal__select_bg_file": "Choose a background image",
"history_clear": "Clear History",
"history_remove": "Right click to remove this entry",
"history_search": "History Searches",
@@ -114,6 +135,7 @@
"list_update_modal__tips": "💡 The list with \"Automatic Updates\" checked will be automatically updated each time the software is launched",
"list_update_modal__title": "List update management",
"list_update_modal__update": "Sync",
"lists__add_local_file_desc": "select song file",
"lists__duplicate": "Duplicate song",
"lists__export": "Export",
"lists__export_part_desc": "Choose where to save the list file",
@@ -129,6 +151,7 @@
"lists__remove_tip": "Do you really want to remove {name}?",
"lists__remove_tip_button": "Yes, that's right",
"lists__rename": "Rename",
"lists__select_local_file": "Add local songs",
"lists__sort_list": "Sort songs",
"lists__source_detail": "Playlist Page",
"lists__sync": "Update",
@@ -157,8 +180,6 @@
"music_duplicate": "Duplicate song",
"music_name": "Name",
"music_singer": "Artist",
"music_sort__btn_cancel": "Cancel",
"music_sort__btn_confirm": "Confirm",
"music_sort__input_tip": "Please input which position you want to adjust to",
"music_sort__title": "Adjust the position of {name} to:",
"music_sort__title_multiple": "Adjust the position of the selected {num} songs to:",
@@ -208,6 +229,8 @@
"player__volume": "Volume: ",
"search": "Search",
"search__hot_search": "Top Searches",
"search__type_music": "Song",
"search__type_songlist": "Song List",
"search__welcome": "Search what I want to 😉",
"setting": "Settings",
"setting__about": "About lx-music-desktop",
@@ -228,6 +251,7 @@
"setting__backup_part_export_setting": "Export settings",
"setting__backup_part_export_setting_desc": "Save the list to...",
"setting__backup_part_import_list": "Import lists",
"setting__backup_part_import_list_confirm": "If the list in the backup file has the same ID as the existing list, the songs in the existing list will be overwritten. Do you want to continue?",
"setting__backup_part_import_list_desc": "Select a list backup",
"setting__backup_part_import_setting": "Import settings",
"setting__backup_part_import_setting_desc": "Select the Settings file",
@@ -237,6 +261,13 @@
"setting__basic_control_btn_position_left": "Left",
"setting__basic_control_btn_position_right": "Right",
"setting__basic_font": "Font",
"setting__basic_font_size": "font size",
"setting__basic_font_size_14px": "Smaller",
"setting__basic_font_size_15px": "Small",
"setting__basic_font_size_16px": "standard",
"setting__basic_font_size_17px": "Big",
"setting__basic_font_size_18px": "Larger",
"setting__basic_font_size_19px": "Very big",
"setting__basic_lang": "Language",
"setting__basic_lang_title": "The language displayed in the software",
"setting__basic_show_animation": "Show switching animation",
@@ -246,7 +277,6 @@
"setting__basic_source_status_success": "Initialization successful",
"setting__basic_source_temp": "Temporary API (some features not available; workaround if Test API unavailable)",
"setting__basic_source_test": "Test API (Available for most software features)",
"setting__basic_source_title": "Choose a music source",
"setting__basic_source_user_api_btn": "Custom Source Management",
"setting__basic_sourcename": "Source name",
"setting__basic_sourcename_alias": "Aliases",
@@ -268,15 +298,28 @@
"setting__click_copy": "Click to copy",
"setting__click_open": "Click to open",
"setting__desktop_lyric": "Desktop Lyric Settings",
"setting__desktop_lyric_align": "Lyric Alignment",
"setting__desktop_lyric_align_center": "Centered",
"setting__desktop_lyric_align_left": "left",
"setting__desktop_lyric_align_right": "right",
"setting__desktop_lyric_always_on_top": "Make the lyrics always above other windows",
"setting__desktop_lyric_always_on_top_loop": "Automatically refresh the top of the lyrics (try to enable this setting when the lyrics are still blocked by some programs)",
"setting__desktop_lyric_audio_visualization": "Audio Visualization (Experimental)",
"setting__desktop_lyric_color_reset": "reset color",
"setting__desktop_lyric_delay_scroll": "Delayed lyrics scroll",
"setting__desktop_lyric_direction_horizontal": "horizontal direction",
"setting__desktop_lyric_direction_vertical": "vertical direction",
"setting__desktop_lyric_ellipsis": "Lyrics are not allowed to wrap",
"setting__desktop_lyric_enable": "Display lyrics",
"setting__desktop_lyric_font": "Lyric font",
"setting__desktop_lyric_font_default": "Default",
"setting__desktop_lyric_font_weight": "Bold font",
"setting__desktop_lyric_hover_hide": "Reduce the transparency of lyrics when the mouse moves into the lyrics area (this feature has platform compatibility issues)",
"setting__desktop_lyric_lock": "Lock lyrics",
"setting__desktop_lyric_lock_screen": "It is not allowed to drag the lyrics window out of the main screen",
"setting__desktop_lyric_played_color": "color played",
"setting__desktop_lyric_shadow_color": "shadow color",
"setting__desktop_lyric_unplay_color": "Color not playing",
"setting__download": "Download",
"setting__download_data_embed": "Whether to embed the following content in the audio file",
"setting__download_embed_lyric": "Embedding lyric",
@@ -297,7 +340,9 @@
"setting__download_path_label": "Current: ",
"setting__download_path_open_label": "Click to open this path",
"setting__download_path_title": "Define the path to downloading",
"setting__download_rlyric": "Also write Roman lyrics to the lyrics file (if any)",
"setting__download_select_save_path": "Select the save path",
"setting__download_tlyric": "Also write the translated lyrics to the lyrics file (if any)",
"setting__download_use_other_source": "Auto source download",
"setting__download_use_other_source_tip": "When the song cannot be downloaded from the original source, try to switch to another source to download. Note: This function does not 100% guarantee that the version of the song after changing the source is consistent with the original version",
"setting__hot_key": "Shortcut Key Settings",
@@ -322,6 +367,7 @@
"setting__is_enable": "Enabled",
"setting__is_show": "Showed",
"setting__list": "List",
"setting__list_action_btn": "Show list action buttons",
"setting__list_add_music_location_type": "Position when adding a song to the list",
"setting__list_add_music_location_type_bottom": "Bottom",
"setting__list_add_music_location_type_top": "Top",
@@ -338,10 +384,17 @@
"setting__odc_clear_search_input": "Clear the search box when you are not searching",
"setting__odc_clear_search_list": "Clear the search list when you are not searching",
"setting__other": "Extras",
"setting__other_play_list_cache": "List cache management",
"setting__other_play_list_cache_clear_btn": "Clear list cache information",
"setting__other_play_list_cache_tip": "The cached song links and playback sources in my list need to be re-acquired when playing or downloading songs after cleaning. Do not clean up if there are no issues related to song playback",
"setting__other_play_list_cache_tip_confirm": "In my list, the cached song links and play alternative sources need to be re-obtained when playing and downloading songs after cleaning. Do not clean up if there are no issues related to song playback. Are you sure to clean up?",
"setting__other_lyric_edited_cache": "Lyric management with adjusted offset time",
"setting__other_lyric_edited_clear_btn": "Clean up time-adjusted lyrics",
"setting__other_lyric_edited_clear_tip_confirm": "This will clear all the lyrics that you have adjusted the offset time before, confirm to clear? \n(Hand shaking to confirm 🤪)",
"setting__other_lyric_edited_label": "Number of lyrics:",
"setting__other_lyric_raw_clear_btn": "Clear Lyrics Cache",
"setting__other_lyric_raw_label": "Number of lyrics:",
"setting__other_music_url_clear_btn": "Clean up song URL cache",
"setting__other_music_url_label": "Number of song URLs:",
"setting__other_other_cache": "Other cache management",
"setting__other_other_source_clear_btn": "Clear the cache of swapped songs",
"setting__other_other_source_label": "Change the number of source song information:",
"setting__other_resource_cache": "Resource cache management",
"setting__other_resource_cache_clear_btn": "Clear resource cache",
"setting__other_resource_cache_confirm": "I want to clear",
@@ -364,7 +417,7 @@
"setting__play_detail_font_size_reset": "Reset",
"setting__play_detail_font_zoom": "Zoom the currently playing lyrics",
"setting__play_detail_lyric_progress": "Allows to adjust playback progress by lyrics",
"setting__play_lyric_lxlrc": "Play with karaoke-style lyrics (if available)",
"setting__play_lyric_lxlrc": "Use karaoke-style lyrics to play (if available, this function consumes more performance, and it is not recommended to enable it on computers with low configuration)",
"setting__play_lyric_roma": "Show lyrics roman",
"setting__play_lyric_s2t": "Convert the playing and downloading lyrics to Traditional Chinese",
"setting__play_lyric_transition": "Show lyrics translation",
@@ -375,6 +428,7 @@
"setting__play_media_device_tip": "This feature conflicts with Audio Visualization, both cannot be enabled at the same time, would you like to turn Audio Visualization off and apply the selected audio output settings?",
"setting__play_quality": "Priority playback of 320K quality songs (if available)",
"setting__play_save_play_time": "Remember playback progress",
"setting__play_startup_auto_play": "Play music automatically after launching the software",
"setting__play_task_bar": "Show playing progress on the taskbar",
"setting__play_timeout": "Timed pause",
"setting__player_audio_visualization_tip": "The custom audio output device will conflict with the audio visualization function. After the audio visualization is enabled, the audio output device will be reset to the default output device. At present, this problem cannot be solved. Do you still want to enable it?",
@@ -402,11 +456,16 @@
"setting__update_progress": "Download progress: ",
"setting__update_unknown": "Unknown",
"song_list": "Playlists",
"songlist__import_input_btn_confirm": "Open",
"songlist__import_input_show_btn": "Open Playlist",
"songlist__import_input_tip": "Enter songlist link or songlist ID",
"songlist__import_input_tip_1": "Cross-source playlists are not supported, please confirm whether the playlist to be opened corresponds to the current playlist source",
"songlist__import_input_tip_2": "If you encounter a link to a playlist that cannot be opened, welcome feedback",
"songlist__import_input_tip_3": "Kugou source does not support opening with playlist ID, but supports Kugou code opening",
"songlist__import_input_tip_4": "Netease source's \"I like\" playlist requires Token to open, see for details",
"songlist__import_input_title": "Open shared playlist",
"songlist__open_list": "open {name} playlist",
"songlist__tag_info_hot_tag": "Hot tags",
"source_alias_all": "Aggregated",
"source_alias_bd": "BD Music",
"source_alias_kg": "KG Music",
@@ -443,14 +502,40 @@
"tag__high_quality": "HQ",
"tag__lossless": "SQ",
"tag__lossless_24bit": "24bit",
"theme_add": "Add a theme",
"theme_auto": "Auto",
"theme_auto_tip": "Right-click to open the light and dark theme settings window",
"theme_black": "Black",
"theme_blue": "Blue",
"theme_blue2": "Purple Blue",
"theme_blue_plus": "Blue Plus",
"theme_edit_modal__app_bg": "Apply background color",
"theme_edit_modal__aside_color": "Sidebar button color",
"theme_edit_modal__badge": "label color",
"theme_edit_modal__badge_primary": "Main color",
"theme_edit_modal__badge_secondary": "Secondary Color",
"theme_edit_modal__badge_tertiary": "Third color",
"theme_edit_modal__bg_image": "Background picture",
"theme_edit_modal__bg_image_add": "Add background image",
"theme_edit_modal__bg_image_change": "Change background image",
"theme_edit_modal__bg_image_remove": "remove background image",
"theme_edit_modal__close_btn": "Closure",
"theme_edit_modal__control_btn": "Left control button color",
"theme_edit_modal__dark": "Dark theme",
"theme_edit_modal__hide_btn": "Hide play details page",
"theme_edit_modal__main_bg": "Content area background color",
"theme_edit_modal__min_btn": "Minimize",
"theme_edit_modal__pick_cancel": "Reset",
"theme_edit_modal__pick_color": "Choose color",
"theme_edit_modal__pick_last_color": "Use previous color",
"theme_edit_modal__pick_save": "Confirm",
"theme_edit_modal__preview": "Preview theme",
"theme_edit_modal__primary": "Theme color",
"theme_edit_modal__title": "Edit topic",
"theme_green": "Green",
"theme_grey": "Grey",
"theme_happy_new_year": "New Year",
"theme_max_tip": "You can only add up to 10 themes, delete some and add more 😜",
"theme_mid_autumn": "Mid-Autumn",
"theme_ming": "Ming",
"theme_naruto": "Naruto",
@@ -460,9 +545,11 @@
"theme_red": "Red",
"theme_selector_modal__dark_title": "dark theme",
"theme_selector_modal__light_title": "Bright theme",
"theme_selector_modal__theme_name": "topic name",
"theme_selector_modal__title": "Follow system theme settings",
"theme_selector_modal__title_tip": "Note: You can set a light theme and a dark theme in advance, and then it will automatically switch to the corresponding theme you set in advance according to the light and dark theme colors of the system.",
"theme_yellow": "Yellow",
"toggle_source_failed": "Failed to automatically change the source, please try to manually search for the song in other sources to play",
"toggle_source_try": "Try switching to another source...",
"user_api__allow_show_update_alert": "Allow update popup to show",
"user_api__btn_export": "Export",
"user_api__btn_import": "Import",
+89
View File
@@ -0,0 +1,89 @@
import { App, ref } from 'vue'
import { messages, Messages, Message } from './index'
type TranslateValues = Record<string, string | number | boolean>
export declare interface I18n {
locale: keyof Messages
fallbackLocale: keyof Messages
availableLocales: Array<keyof Messages>
messages: Messages
message: Message
setLanguage: (locale: string) => void
fillMessage: (message: string, val: TranslateValues) => string
getMessage: (key: keyof Message, val?: TranslateValues) => string
t: (key: keyof Message, val?: TranslateValues) => string
}
const locale = ref('zh-cn')
let i18n: I18n
const trackReactivityValues = (): any => {
return locale.value
}
const i18nPlugin = {
install: (app: App) => {
// inject a globally available $translate() method
app.config.globalProperties.$t = (key: keyof Message, val?: TranslateValues): string => {
// retrieve a nested property in `options`
// using `key` as the path
// return key.split('.').reduce((o, i) => {
// if (o) return o[i]
// }, options)
trackReactivityValues()
return i18n.getMessage(key, val)
}
},
}
const useI18n = () => {
return (key: keyof Message, val?: TranslateValues): string => {
trackReactivityValues()
return i18n.getMessage(key, val)
}
}
const setLanguage = (lang: string) => {
i18n.setLanguage(lang)
}
const createI18n = (): I18n => {
return i18n = {
locale: locale.value,
fallbackLocale: 'zh-cn',
availableLocales: Object.keys(messages),
messages,
message: messages[locale.value],
setLanguage(_locale: string) {
this.locale = _locale
this.message = messages[_locale]
locale.value = _locale
},
fillMessage(message: string, vals: TranslateValues): string {
for (const [key, val] of Object.entries(vals)) {
message = message.replaceAll(`{${key}}`, String(val))
}
return message
},
getMessage(key: keyof Message, val?: TranslateValues): string {
let targetMessage = this.message[key] ?? this.messages[this.fallbackLocale][key] ?? ''
return val ? this.fillMessage(targetMessage, val) : targetMessage
},
t(key: keyof Message, val?: TranslateValues): string {
trackReactivityValues()
return this.getMessage(key, val)
},
}
}
export {
i18nPlugin,
setLanguage,
useI18n,
createI18n,
}
+25 -3
View File
@@ -2,7 +2,20 @@ import zh_cn from './zh-cn.json'
import zh_tw from './zh-tw.json'
import en_us from './en-us.json'
const langs = [
type Message = Record<keyof typeof zh_cn, string>
| Record<keyof typeof zh_tw, string>
| Record<keyof typeof en_us, string>
interface Lang {
name: string
locale: string
alternate?: string
country: string
fallback?: boolean
message: Message
}
const langs: Lang[] = [
{
name: '简体中文',
locale: 'zh-cn',
@@ -26,8 +39,13 @@ const langs = [
},
]
const langList = []
const messages = {}
const langList: Array<{
name: string
locale: string
alternate?: string
}> = []
type Messages = Record<string, Message>
const messages: Messages = {}
langs.forEach(item => {
langList.push({
name: item.name,
@@ -40,4 +58,8 @@ langs.forEach(item => {
export {
langList,
messages,
Messages,
Message,
}
export * from './i18n'
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "esnext", /* Specify what module code is generated. */
"moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */
},
// "include": [
// "**/*.ts",
// "**/*.js",
// "**/*.vue",
// "**/*.json",
// ],
// "exclude": ["dbService"]
}
+100 -11
View File
@@ -4,6 +4,9 @@
"alert_button_text": "好吧",
"audio_visualization": "音频可视化(实验性)",
"back": "返回",
"btn_cancel": "取消",
"btn_confirm": "确定",
"btn_save": "保存",
"cancel_button_text": "我不",
"close": "关闭",
"comment__hot_load_error": "热门评论加载失败,点击尝试重新加载",
@@ -16,6 +19,7 @@
"comment__refresh": "刷新评论",
"comment__show": "歌曲评论",
"comment__title": "{name} 的评论",
"comment__unavailable": "此歌曲不支持获取评论",
"confirm_button_text": "是的",
"copy_tip": "(点击复制)",
"date_format_hour": "{num}小时前",
@@ -26,8 +30,8 @@
"default_list": "试听列表",
"desktop_lyric__back": "返回",
"desktop_lyric__close": "关闭",
"desktop_lyric__font_decrease": "减小字体大小(右击可微调)",
"desktop_lyric__font_increase": "增加字体大小(右击可微调)",
"desktop_lyric__font_decrease": "减小字体大小",
"desktop_lyric__font_increase": "增加字体大小",
"desktop_lyric__lock": "锁定歌词",
"desktop_lyric__lrc_active_zoom_off": "取消缩放当前播放的歌词",
"desktop_lyric__lrc_active_zoom_on": "缩放当前播放的歌词",
@@ -38,6 +42,11 @@
"desktop_lyric__win_top_off": "取消置顶歌词界面",
"desktop_lyric__win_top_on": "置顶歌词界面",
"download": "下载",
"download___status_complated": "下载完成",
"download___status_error": "任务出错",
"download___status_paused": "暂停下载",
"download___status_runing": "正在下载",
"download___status_wating": "等待下载",
"download__all": "全部任务",
"download__error": "出错",
"download__finished": "下载完成",
@@ -52,8 +61,20 @@
"download__quality": "品质",
"download__runing": "正在下载",
"download__status": "状态",
"download_status_error_check_path": "检查下载路径出错,请检查设置的下载目录是否正常",
"download_status_error_refresh_url": "链接失效,正在刷新链接",
"download_status_error_url_failed": "获取音乐链接失败",
"download_status_error_write": "歌曲保存位置被占用或没有写入权限,请尝试更改歌曲保存目录或重启软件或重启电脑,错误详情:",
"download_status_start": "开始下载",
"download_status_url_geting": "音乐链接获取中...",
"duplicate_list_tip": "你之前已收藏过该列表 [{name}],是否需要更新里面的歌曲?",
"export": "导出",
"fullscreen_exit": "退出全屏",
"theme_edit_modal__copy": "复制主题",
"theme_edit_modal__remove": "删除",
"theme_edit_modal__remove_tip": "是否真的要移除这个主题?",
"theme_edit_modal__save_new": "另存",
"theme_edit_modal__select_bg_file": "选择背景图片",
"history_clear": "清空搜索历史",
"history_remove": "右击移除该历史",
"history_search": "历史搜索",
@@ -114,6 +135,7 @@
"list_update_modal__tips": "💡 每次启动软件时将会自动更新已勾选“自动更新”的列表",
"list_update_modal__title": "列表更新管理",
"list_update_modal__update": "立即更新",
"lists__add_local_file_desc": "选择歌曲文件",
"lists__duplicate": "重复歌曲",
"lists__export": "导出",
"lists__export_part_desc": "选择列表文件保存位置",
@@ -129,6 +151,7 @@
"lists__remove_tip": "你真的想要移除 {name} 吗?",
"lists__remove_tip_button": "是的 没错",
"lists__rename": "重命名",
"lists__select_local_file": "添加本地歌曲",
"lists__sort_list": "排序歌曲",
"lists__source_detail": "歌单详情页",
"lists__sync": "更新",
@@ -157,8 +180,6 @@
"music_duplicate": "重复歌曲",
"music_name": "歌曲名",
"music_singer": "艺术家",
"music_sort__btn_cancel": "取消",
"music_sort__btn_confirm": "确定",
"music_sort__input_tip": "请输入要调整到第几个位置",
"music_sort__title": "将 {name} 的位置调整到:",
"music_sort__title_multiple": "将已选的 {num} 首歌曲的位置调整到:",
@@ -208,6 +229,8 @@
"player__volume": "当前音量:",
"search": "搜索",
"search__hot_search": "热门搜索",
"search__type_music": "歌曲",
"search__type_songlist": "歌单",
"search__welcome": "搜我所想~~😉",
"setting": "设置",
"setting__about": "关于洛雪音乐",
@@ -228,6 +251,7 @@
"setting__backup_part_export_setting": "导出设置",
"setting__backup_part_export_setting_desc": "选择设置保存位置",
"setting__backup_part_import_list": "导入列表",
"setting__backup_part_import_list_confirm": "备份文件中列表与现有列表ID相同时,现有列表内歌曲将会被覆盖,是否要继续?",
"setting__backup_part_import_list_desc": "选择列表文件",
"setting__backup_part_import_setting": "导入设置",
"setting__backup_part_import_setting_desc": "选择配置文件",
@@ -237,6 +261,13 @@
"setting__basic_control_btn_position_left": "左边",
"setting__basic_control_btn_position_right": "右边",
"setting__basic_font": "字体",
"setting__basic_font_size": "字体大小",
"setting__basic_font_size_14px": "较小",
"setting__basic_font_size_15px": "小",
"setting__basic_font_size_16px": "标准",
"setting__basic_font_size_17px": "大",
"setting__basic_font_size_18px": "较大",
"setting__basic_font_size_19px": "非常大",
"setting__basic_lang": "语言",
"setting__basic_lang_title": "软件显示的语言",
"setting__basic_show_animation": "显示切换动画",
@@ -246,7 +277,6 @@
"setting__basic_source_status_success": "初始化成功",
"setting__basic_source_temp": "临时接口(软件的某些功能不可用,建议测试接口不可用再使用本接口)",
"setting__basic_source_test": "测试接口(几乎软件的所有功能都可用)",
"setting__basic_source_title": "选择音乐来源",
"setting__basic_source_user_api_btn": "自定义源管理",
"setting__basic_sourcename": "音源名字",
"setting__basic_sourcename_alias": "别名",
@@ -268,15 +298,30 @@
"setting__click_copy": "点击复制",
"setting__click_open": "点击打开",
"setting__desktop_lyric": "桌面歌词设置",
"setting__desktop_lyric_align": "歌词对齐方式",
"setting__desktop_lyric_align_center": "居中",
"setting__desktop_lyric_align_left": "居左",
"setting__desktop_lyric_align_right": "居右",
"setting__desktop_lyric_always_on_top": "使歌词总是在其他窗口之上",
"setting__desktop_lyric_always_on_top_loop": "自动刷新歌词置顶(当歌词置顶后仍被某些程序遮挡时可尝试启用此设置)",
"setting__desktop_lyric_audio_visualization": "音频可视化(实验性)",
"setting__desktop_lyric_color": "歌词字体颜色",
"setting__desktop_lyric_color_reset": "重置颜色",
"setting__desktop_lyric_delay_scroll": "延迟歌词滚动",
"setting__desktop_lyric_direction": "歌词显示方向",
"setting__desktop_lyric_direction_horizontal": "水平方向",
"setting__desktop_lyric_direction_vertical": "垂直方向",
"setting__desktop_lyric_ellipsis": "不允许歌词换行",
"setting__desktop_lyric_enable": "显示歌词",
"setting__desktop_lyric_font": "歌词字体",
"setting__desktop_lyric_font_default": "默认",
"setting__desktop_lyric_font_weight": "加粗字体",
"setting__desktop_lyric_hover_hide": "鼠标移入歌词区域时降低歌词透明度(此功能存在平台兼容性问题)",
"setting__desktop_lyric_lock": "锁定歌词",
"setting__desktop_lyric_lock_screen": "不允许歌词窗口拖出主屏幕之外",
"setting__desktop_lyric_played_color": "已播放颜色",
"setting__desktop_lyric_shadow_color": "阴影颜色",
"setting__desktop_lyric_unplay_color": "未播放颜色",
"setting__download": "下载设置",
"setting__download_data_embed": "是否将以下内容嵌入到音频文件中",
"setting__download_embed_lyric": "歌词嵌入",
@@ -297,7 +342,9 @@
"setting__download_path_label": "当前下载路径:",
"setting__download_path_open_label": "点击打开当前路径",
"setting__download_path_title": "下载歌曲保存的路径",
"setting__download_rlyric": "同时将罗马音歌词写入歌词文件中(如果有)",
"setting__download_select_save_path": "选择歌曲保存路径",
"setting__download_tlyric": "同时将翻译歌词写入歌词文件中(如果有)",
"setting__download_use_other_source": "自动换源下载",
"setting__download_use_other_source_tip": "当无法从歌曲的原始源下载时,尝试切换到其他源下载,注:此功能不100%保证换源后的歌曲版本与原版一致",
"setting__hot_key": "快捷键设置",
@@ -322,6 +369,7 @@
"setting__is_enable": "是否启用",
"setting__is_show": "是否显示",
"setting__list": "列表设置",
"setting__list_action_btn": "显示列表操作按钮",
"setting__list_add_music_location_type": "添加歌曲到列表时的位置",
"setting__list_add_music_location_type_bottom": "底部",
"setting__list_add_music_location_type_top": "顶部",
@@ -338,10 +386,17 @@
"setting__odc_clear_search_input": "离开搜索界面时清空搜索框",
"setting__odc_clear_search_list": "离开搜索界面时清空搜索列表",
"setting__other": "其他",
"setting__other_play_list_cache": "列表缓存管理",
"setting__other_play_list_cache_clear_btn": "清理列表缓存信息",
"setting__other_play_list_cache_tip": "我的列表中已缓存的歌曲链接、播放代替源,清理后播放、下载歌曲时需要重新获取,没有歌曲播放相关的问题不要清理",
"setting__other_play_list_cache_tip_confirm": "我的列表中已缓存的歌曲链接、播放代替源,清理后播放、下载歌曲时需要重新获取,没有歌曲播放相关的问题不要清理,是否确认清理?",
"setting__other_lyric_edited_cache": "已调整过偏移时间的歌词管理",
"setting__other_lyric_edited_clear_btn": "清理已调整过时间的歌词",
"setting__other_lyric_edited_clear_tip_confirm": "这将清理所有你之前已调整过偏移时间的歌词,是否确认清理?(手抖确认🤪)",
"setting__other_lyric_edited_label": "歌词数量:",
"setting__other_lyric_raw_clear_btn": "清理歌词缓存",
"setting__other_lyric_raw_label": "歌词数量:",
"setting__other_music_url_clear_btn": "清理歌曲URL缓存",
"setting__other_music_url_label": "歌曲URL数量:",
"setting__other_other_cache": "其他缓存管理",
"setting__other_other_source_clear_btn": "清理换源歌曲缓存",
"setting__other_other_source_label": "换源歌曲信息数量:",
"setting__other_resource_cache": "资源缓存管理",
"setting__other_resource_cache_clear_btn": "清理资源缓存",
"setting__other_resource_cache_confirm": "我要清掉",
@@ -364,7 +419,7 @@
"setting__play_detail_font_size_reset": "重置",
"setting__play_detail_font_zoom": "缩放当前正在播放的歌词",
"setting__play_detail_lyric_progress": "允许通过歌词调整播放进度",
"setting__play_lyric_lxlrc": "使用卡拉OK式歌词播放(如果可用)",
"setting__play_lyric_lxlrc": "使用卡拉OK式歌词播放(如果可用,此功能比较耗性能,低配置电脑不建议开启)",
"setting__play_lyric_roma": "显示歌词罗马音(如果可用)",
"setting__play_lyric_s2t": "将播放与下载的歌词转换为繁体中文",
"setting__play_lyric_transition": "显示歌词翻译(如果可用)",
@@ -375,6 +430,7 @@
"setting__play_media_device_tip": "此功能与音频可视化功能冲突,两者无法同时启用,是否将音频可视化关闭 并 应用所选音频输出设置?",
"setting__play_quality": "优先播放320K品质的歌曲(如果可用)",
"setting__play_save_play_time": "记住播放进度",
"setting__play_startup_auto_play": "启动软件后自动播放音乐",
"setting__play_task_bar": "在任务栏上显示当前歌曲播放进度",
"setting__play_timeout": "定时暂停",
"setting__player_audio_visualization_tip": "自定义音频输出设备与音频可视化功能会冲突,启用了音频可视化后音频输出设备将会被重置为默认的输出设备,目前此问题暂无法解决,是否仍要开启?",
@@ -402,11 +458,16 @@
"setting__update_progress": "下载进度:",
"setting__update_unknown": "未知",
"song_list": "歌单",
"songlist__import_input_btn_confirm": "打开",
"songlist__import_input_show_btn": "打开歌单",
"songlist__import_input_tip": "输入歌单链接或歌单ID",
"songlist__import_input_tip_1": "不支持跨源打开歌单,请确认要打开的歌单与当前歌单源是否对应",
"songlist__import_input_tip_2": "若遇到无法打开的歌单链接,欢迎反馈",
"songlist__import_input_tip_3": "酷狗源不支持用歌单ID打开,但支持酷狗码打开",
"songlist__import_input_tip_4": "网易源的“我喜欢”歌单需要 Token 才能打开,详情看 ",
"songlist__import_input_title": "打开分享的歌单",
"songlist__open_list": "打开{name}歌单",
"songlist__tag_info_hot_tag": "热门标签",
"source_alias_all": "聚合大会",
"source_alias_bd": "小杜音乐",
"source_alias_kg": "小枸音乐",
@@ -443,14 +504,40 @@
"tag__high_quality": "HQ",
"tag__lossless": "SQ",
"tag__lossless_24bit": "24bit",
"theme_add": "添加主题",
"theme_auto": "道法自然",
"theme_auto_tip": "鼠标 右击 可打开亮、暗主题设置窗口",
"theme_black": "黑灯瞎火",
"theme_blue": "蓝田生玉",
"theme_blue2": "清热版蓝",
"theme_blue_plus": "蛋雅深蓝",
"theme_edit_modal__app_bg": "应用背景颜色",
"theme_edit_modal__aside_color": "侧栏按钮颜色",
"theme_edit_modal__badge": "标签颜色",
"theme_edit_modal__badge_primary": "主颜色",
"theme_edit_modal__badge_secondary": "次要颜色",
"theme_edit_modal__badge_tertiary": "第三颜色",
"theme_edit_modal__bg_image": "背景图片",
"theme_edit_modal__bg_image_add": "背景图片:",
"theme_edit_modal__bg_image_change": "更改背景图片",
"theme_edit_modal__bg_image_remove": "移除背景图片",
"theme_edit_modal__close_btn": "关闭",
"theme_edit_modal__control_btn": "左侧控制按钮颜色",
"theme_edit_modal__dark": "暗色主题",
"theme_edit_modal__hide_btn": "隐藏播放详情页",
"theme_edit_modal__main_bg": "内容区域背景颜色",
"theme_edit_modal__min_btn": "最小化",
"theme_edit_modal__pick_cancel": "重置",
"theme_edit_modal__pick_color": "选择颜色",
"theme_edit_modal__pick_last_color": "使用之前的颜色",
"theme_edit_modal__pick_save": "确认",
"theme_edit_modal__preview": "预览主题",
"theme_edit_modal__primary": "主题色",
"theme_edit_modal__title": "编辑主题",
"theme_green": "绿意盎然",
"theme_grey": "灰常美丽",
"theme_happy_new_year": "新年快乐",
"theme_max_tip": "最多只能添加10个主题哦,删掉一些再添加吧 😜 ",
"theme_mid_autumn": "月里嫦娥",
"theme_ming": "青出于黑",
"theme_naruto": "木叶之村",
@@ -460,9 +547,11 @@
"theme_red": "热情似火",
"theme_selector_modal__dark_title": "暗色主题",
"theme_selector_modal__light_title": "亮色主题",
"theme_selector_modal__theme_name": "主题名称",
"theme_selector_modal__title": "跟随系统主题设置",
"theme_selector_modal__title_tip": "注:你可以预先设置一个亮色主题及暗色主题,此后将根据系统的亮、暗主题色自动切换为你预先设置的相应主题。",
"theme_yellow": "信口雌黄",
"toggle_source_failed": "自动换源失败,请尝试手动在其他源搜索该歌曲播放",
"toggle_source_try": "尝试切换到其他源...",
"user_api__allow_show_update_alert": "允许显示更新弹窗",
"user_api__btn_export": "导出",
"user_api__btn_import": "导入",
+106 -19
View File
@@ -4,6 +4,9 @@
"alert_button_text": "好吧",
"audio_visualization": "音頻可視化(實驗性)",
"back": "返回",
"btn_cancel": "取消",
"btn_confirm": "確定",
"btn_save": "保存",
"cancel_button_text": "取消",
"close": "關閉",
"comment__hot_load_error": "熱門評論加載失敗,點擊嘗試重新加載",
@@ -16,6 +19,7 @@
"comment__refresh": "刷新評論",
"comment__show": "歌曲評論",
"comment__title": "{name} 的評論",
"comment__unavailable": "此歌曲不支持獲取評論",
"confirm_button_text": "是的",
"copy_tip": "(點擊複製)",
"date_format_hour": "{num}小時前",
@@ -26,8 +30,8 @@
"default_list": "試聽列表",
"desktop_lyric__back": "返回",
"desktop_lyric__close": "關閉",
"desktop_lyric__font_decrease": "減小字體大小(右擊可微調)",
"desktop_lyric__font_increase": "增加字體大小(右擊可微調)",
"desktop_lyric__font_decrease": "減小字體大小",
"desktop_lyric__font_increase": "增加字體大小",
"desktop_lyric__lock": "鎖定歌詞",
"desktop_lyric__lrc_active_zoom_off": "取消縮放當前播放的歌詞",
"desktop_lyric__lrc_active_zoom_on": "縮放當前播放的歌詞",
@@ -37,7 +41,12 @@
"desktop_lyric__unlock": "解鎖歌詞",
"desktop_lyric__win_top_off": "取消置頂歌詞界面",
"desktop_lyric__win_top_on": "置頂歌詞界面",
"download": "下載",
"download": "下載管理",
"download___status_complated": "下載完成",
"download___status_error": "任務出錯",
"download___status_paused": "暫停下載",
"download___status_runing": "正在下載",
"download___status_wating": "等待下載",
"download__all": "全部任務",
"download__error": "出錯",
"download__finished": "下載完成",
@@ -52,8 +61,20 @@
"download__quality": "品質",
"download__runing": "正在下載",
"download__status": "狀態",
"download_status_error_check_path": "檢查下載路徑出錯,請檢查設置的下載目錄是否正常",
"download_status_error_refresh_url": "鏈接失效,正在刷新鏈接",
"download_status_error_url_failed": "獲取音樂鏈接失敗",
"download_status_error_write": "歌曲保存位置被佔用或沒有寫入權限,請嘗試更改歌曲保存目錄或重啟軟件或重啟電腦,錯誤詳情:",
"download_status_start": "開始下載",
"download_status_url_geting": "音樂鏈接獲取中...",
"duplicate_list_tip": "你之前已收藏過該列表 [{name}],是否需要更新里面的歌曲?",
"export": "導出",
"fullscreen_exit": "退出全屏",
"theme_edit_modal__copy": "複製主題",
"theme_edit_modal__remove": "刪除",
"theme_edit_modal__remove_tip": "是否真的要移除這個主題?",
"theme_edit_modal__save_new": "另存",
"theme_edit_modal__select_bg_file": "選擇背景圖片",
"history_clear": "清空搜索歷史",
"history_remove": "右擊移除該歷史",
"history_search": "歷史搜索",
@@ -91,7 +112,7 @@
"list_add__btn_title": "把該歌曲添加到 {name}",
"list_add__multiple_btn_title": "把這些歌曲添加到 {name}",
"list_add__multiple_title_add": "添加已選的 {num} 首歌曲到...",
"list_add__multiple_title_move": "移動已選的 {num} 首歌曲到...",
"list_add__multiple_title_move": "添加移動已選的 {num} 首歌曲到...",
"list_add__title_first_add": "添加",
"list_add__title_first_move": "移動",
"list_add__title_last": "到...",
@@ -114,6 +135,7 @@
"list_update_modal__tips": "💡 每次啟動軟件時將會自動更新已勾選“自動更新”的列表",
"list_update_modal__title": "列表更新管理",
"list_update_modal__update": "立即更新",
"lists__add_local_file_desc": "選擇歌曲文件",
"lists__duplicate": "重複歌曲",
"lists__export": "導出",
"lists__export_part_desc": "選擇列表文件保存位置",
@@ -129,14 +151,15 @@
"lists__remove_tip": "你真的想要移除 {name} 嗎?",
"lists__remove_tip_button": "是的 沒錯",
"lists__rename": "重命名",
"lists__select_local_file": "添加本地歌曲",
"lists__sort_list": "排序歌曲",
"lists__source_detail": "歌單詳情頁",
"lists__sync": "更新",
"lists__sync_confirm_tip": "這將會把 {name} 內的歌曲替換成在線列表的歌曲,你確認要更新嗎?",
"load_list_file_error_detail": "我們已經幫你把舊的列表文件備份到{path}\n它以 JSON 格式存儲,你可以嘗試手動修復並恢復它\n\n錯誤詳情:{detail}",
"load_list_file_error_title": "播放列表數據加載錯誤(建議到GitHub或加群反饋)",
"load_list_file_error_title": "播放列表數據加載錯誤",
"loding": "加載中...",
"love_list": "收藏",
"love_list": "收藏列表",
"lyric__load_error": "歌詞獲取失敗",
"lyric__select": "歌詞文本選擇",
"lyric_menu__align": "歌詞對齊方式",
@@ -144,11 +167,11 @@
"lyric_menu__align_left": "居左",
"lyric_menu__lrc_size": "字體大小 [ {size} ]",
"lyric_menu__offset": "歌詞偏移 [ {offset}ms ]",
"lyric_menu__offset_add_10": "加快10毫秒",
"lyric_menu__offset_add_10": "加快10毫秒(右擊加快5毫秒)",
"lyric_menu__offset_add_100": "加快100毫秒",
"lyric_menu__offset_dec_10": "減慢10毫秒",
"lyric_menu__offset_dec_100": "減慢100毫秒",
"lyric_menu__offset_reset": "重置",
"lyric_menu__offset_reset": "重置偏移",
"lyric_menu__size_add": "加大字體(右擊可微調)",
"lyric_menu__size_dec": "減小字體(右擊可微調)",
"lyric_menu__size_reset": "重置",
@@ -157,8 +180,6 @@
"music_duplicate": "重複歌曲",
"music_name": "歌曲名",
"music_singer": "藝術家",
"music_sort__btn_cancel": "取消",
"music_sort__btn_confirm": "確定",
"music_sort__input_tip": "請輸入要調整到第幾個位置",
"music_sort__title": "將 {name} 的位置調整到:",
"music_sort__title_multiple": "將已選的 {num} 首歌曲的位置調整到:",
@@ -179,6 +200,7 @@
"play_timeout_unit": "分鐘",
"play_timeout_update": "更新定時",
"player__add_music_to": "添加當前歌曲到...",
"player__album": "專輯名:",
"player__buffering": "緩衝中...",
"player__desktop_lyric_lock": "右擊鎖定歌詞",
"player__desktop_lyric_off": "關閉桌面歌詞",
@@ -189,7 +211,6 @@
"player__geting_url": "歌曲鏈接獲取中...",
"player__hide_detail_tip": "隱藏詳情頁(界面內右鍵雙擊可快速隱藏詳情頁)",
"player__loading": "音樂加載中...",
"player__music_album": "專輯名:",
"player__music_name": "歌曲名:",
"player__music_singer": "藝術家:",
"player__next": "下一首",
@@ -208,6 +229,8 @@
"player__volume": "當前音量:",
"search": "搜索",
"search__hot_search": "熱門搜索",
"search__type_music": "歌曲",
"search__type_songlist": "歌單",
"search__welcome": "搜我所想~~😉",
"setting": "設置",
"setting__about": "關於洛雪音樂",
@@ -228,6 +251,7 @@
"setting__backup_part_export_setting": "導出設置",
"setting__backup_part_export_setting_desc": "選擇設置保存位置",
"setting__backup_part_import_list": "導入列表",
"setting__backup_part_import_list_confirm": "備份文件中列表與現有列表ID相同時,現有列表內歌曲將會被覆蓋,是否要繼續?",
"setting__backup_part_import_list_desc": "選擇列表文件",
"setting__backup_part_import_setting": "導入設置",
"setting__backup_part_import_setting_desc": "選擇配置文件",
@@ -237,6 +261,13 @@
"setting__basic_control_btn_position_left": "左邊",
"setting__basic_control_btn_position_right": "右邊",
"setting__basic_font": "字體",
"setting__basic_font_size": "字體大小",
"setting__basic_font_size_14px": "較小",
"setting__basic_font_size_15px": "較小",
"setting__basic_font_size_16px": "標準",
"setting__basic_font_size_17px": "大",
"setting__basic_font_size_18px": "較大",
"setting__basic_font_size_19px": "非常大",
"setting__basic_lang": "語言",
"setting__basic_lang_title": "軟件顯示的語言",
"setting__basic_show_animation": "顯示切換動畫",
@@ -246,7 +277,6 @@
"setting__basic_source_status_success": "初始化成功",
"setting__basic_source_temp": "臨時接口(軟件的某些功能不可用,建議測試接口不可用再使用本接口)",
"setting__basic_source_test": "測試接口(幾乎軟件的所有功能都可用)",
"setting__basic_source_title": "選擇音樂來源",
"setting__basic_source_user_api_btn": "自定義源管理",
"setting__basic_sourcename": "音源名字",
"setting__basic_sourcename_alias": "別名",
@@ -268,15 +298,29 @@
"setting__click_copy": "點擊複製",
"setting__click_open": "點擊打開",
"setting__desktop_lyric": "桌面歌詞設置",
"setting__desktop_lyric_align": "歌詞對齊方式",
"setting__desktop_lyric_align_center": "居中",
"setting__desktop_lyric_align_left": "居左",
"setting__desktop_lyric_align_right": "居右",
"setting__desktop_lyric_always_on_top": "使歌詞總是在其他窗口之上",
"setting__desktop_lyric_always_on_top_loop": "自動刷新歌詞置頂(當歌詞置頂後仍被某些程序遮擋時可嘗試啟用此設置)",
"setting__desktop_lyric_audio_visualization": "音頻可視化(實驗性)",
"setting__desktop_lyric_color_reset": "重置顏色",
"setting__desktop_lyric_delay_scroll": "延遲歌詞滾動",
"setting__desktop_lyric_direction": "歌詞顯示方向",
"setting__desktop_lyric_direction_horizontal": "水平方向",
"setting__desktop_lyric_direction_vertical": "垂直方向",
"setting__desktop_lyric_ellipsis": "不允許桌面歌詞換行",
"setting__desktop_lyric_enable": "顯示歌詞",
"setting__desktop_lyric_font": "歌詞字體",
"setting__desktop_lyric_font_default": "默認",
"setting__desktop_lyric_font_weight": "加粗字體",
"setting__desktop_lyric_hover_hide": "鼠標移入歌詞區域時降低歌詞透明度(此功能存在平台兼容性問題)",
"setting__desktop_lyric_lock": "鎖定歌詞",
"setting__desktop_lyric_lock_screen": "不允許歌詞窗口拖出主屏幕之外",
"setting__desktop_lyric_played_color": "已播放顏色",
"setting__desktop_lyric_shadow_color": "陰影顏色",
"setting__desktop_lyric_unplay_color": "未播放顏色",
"setting__download": "下載設置",
"setting__download_data_embed": "是否將以下內容嵌入到音頻文件中",
"setting__download_embed_lyric": "歌詞嵌入",
@@ -297,7 +341,9 @@
"setting__download_path_label": "當前下載路徑:",
"setting__download_path_open_label": "點擊打開當前路徑",
"setting__download_path_title": "下載歌曲保存的路徑",
"setting__download_rlyric": "同時將羅馬音歌詞寫入歌詞文件中(如果有)",
"setting__download_select_save_path": "選擇歌曲保存路徑",
"setting__download_tlyric": "同時將翻譯歌詞寫入歌詞文件中(如果有)",
"setting__download_use_other_source": "自動換源下載",
"setting__download_use_other_source_tip": "當無法從歌曲的原始源下載時,嘗試切換到其他源下載,注:此功能不100%保證換源後的歌曲版本與原版一致",
"setting__hot_key": "快捷鍵設置",
@@ -313,7 +359,7 @@
"setting__hot_key_local_title": "軟件內快捷鍵",
"setting__hot_key_player_next": "下一首歌曲",
"setting__hot_key_player_prev": "上一首歌曲",
"setting__hot_key_player_toggle_play": "播放/暫停控制",
"setting__hot_key_player_toggle_play": "播放/暫停控制​​",
"setting__hot_key_player_volume_down": "減少音量",
"setting__hot_key_player_volume_mute": "靜音切換",
"setting__hot_key_player_volume_up": "增加音量",
@@ -322,6 +368,7 @@
"setting__is_enable": "是否啟用",
"setting__is_show": "是否顯示",
"setting__list": "列表設置",
"setting__list_action_btn": "顯示列表操作按鈕",
"setting__list_add_music_location_type": "添加歌曲到列表時的位置",
"setting__list_add_music_location_type_bottom": "底部",
"setting__list_add_music_location_type_top": "頂部",
@@ -338,10 +385,17 @@
"setting__odc_clear_search_input": "離開搜索界面時清空搜索框",
"setting__odc_clear_search_list": "離開搜索界面時清空搜索列表",
"setting__other": "其他",
"setting__other_play_list_cache": "列表緩存管理",
"setting__other_play_list_cache_clear_btn": "清理列表緩存信息",
"setting__other_play_list_cache_tip": "我的列表中已緩存的歌曲鏈接、播放代替源,清理後播放、下載歌曲時需要重新獲取,沒有歌曲播放相關的問題不要清理",
"setting__other_play_list_cache_tip_confirm": "我的列表中已緩存的歌曲鏈接、播放代替源,清理後播放、下載歌曲時需要重新獲取,沒有歌曲播放相關的問題不要清理,是否確認清理?",
"setting__other_lyric_edited_cache": "已調整過偏移時間的歌詞管理",
"setting__other_lyric_edited_clear_btn": "清理已調整過時間的歌詞",
"setting__other_lyric_edited_clear_tip_confirm": "這將清理所有你之前已調整過偏移時間的歌詞,是否確認清理? \n(手抖確認🤪)",
"setting__other_lyric_edited_label": "歌詞數量:",
"setting__other_lyric_raw_clear_btn": "清理歌詞緩存",
"setting__other_lyric_raw_label": "歌詞數量:",
"setting__other_music_url_clear_btn": "清理歌曲URL緩存",
"setting__other_music_url_label": "歌曲URL數量:",
"setting__other_other_cache": "其他緩存管理",
"setting__other_other_source_clear_btn": "清理換源歌曲緩存",
"setting__other_other_source_label": "換源歌曲信息數量:",
"setting__other_resource_cache": "資源緩存管理",
"setting__other_resource_cache_clear_btn": "清理資源緩存",
"setting__other_resource_cache_confirm": "我要清掉",
@@ -364,7 +418,7 @@
"setting__play_detail_font_size_reset": "重置",
"setting__play_detail_font_zoom": "縮放當前正在播放的歌詞",
"setting__play_detail_lyric_progress": "允許通過歌詞調整播放進度",
"setting__play_lyric_lxlrc": "使用卡拉OK式歌詞播放(如果可用)",
"setting__play_lyric_lxlrc": "使用卡拉OK式歌詞播放(如果可用,此功能比較耗性能,低配置電腦不建議開啟)",
"setting__play_lyric_roma": "顯示歌詞羅馬音(如果可用)",
"setting__play_lyric_s2t": "將播放與下載的歌詞轉換為繁體中文",
"setting__play_lyric_transition": "顯示歌詞翻譯(如果可用)",
@@ -375,6 +429,7 @@
"setting__play_media_device_tip": "此功能與音頻可視化功能衝突,兩者無法同時啟用,是否將音頻可視化關閉 並 應用所選音頻輸出設置?",
"setting__play_quality": "優先播放320K品質的歌曲(如果可用)",
"setting__play_save_play_time": "記住播放進度",
"setting__play_startup_auto_play": "啟動軟件後自動播放音樂",
"setting__play_task_bar": "在任務欄上顯示當前歌曲播放進度",
"setting__play_timeout": "定時暫停",
"setting__player_audio_visualization_tip": "自定義音頻輸出設備與音頻可視化功能會衝突,啟用了音頻可視化後音頻輸出設備將會被重置為默認的輸出設備,目前此問題暫無法解決,是否仍要開啟?",
@@ -402,11 +457,15 @@
"setting__update_progress": "下載進度:",
"setting__update_unknown": "未知",
"song_list": "歌單",
"songlist__import_input_show_btn": "打開歌單",
"songlist__import_input_tip": "輸入歌單鏈接或歌單ID",
"songlist__import_input_tip_1": "不支持跨源打開歌單,請確認要打開的歌單與當前歌單源是否對應",
"songlist__import_input_tip_2": "若遇到無法打開的歌單鏈接,歡迎反饋",
"songlist__import_input_tip_3": "酷狗源不支持用歌單ID打開,但支持酷狗碼打開",
"songlist__import_input_tip_4": "網易源的“我喜歡”歌單需要 Token 才能打開,詳情看",
"songlist__import_input_title": "打開分享的歌單",
"songlist__open_list": "打開{name}歌單",
"songlist__tag_info_hot_tag": "熱門標籤",
"source_alias_all": "聚合大會",
"source_alias_bd": "小杜音樂",
"source_alias_kg": "小枸音樂",
@@ -443,14 +502,40 @@
"tag__high_quality": "HQ",
"tag__lossless": "SQ",
"tag__lossless_24bit": "24bit",
"theme_add": "添加主題",
"theme_auto": "道法自然",
"theme_auto_tip": "鼠標 右擊 可打開亮、暗主題設置窗口",
"theme_black": "黑燈瞎火",
"theme_blue": "藍田生玉",
"theme_blue2": "清熱版藍",
"theme_blue_plus": "蛋雅深藍",
"theme_edit_modal__app_bg": "應用背景顏色",
"theme_edit_modal__aside_color": "側欄按鈕顏色",
"theme_edit_modal__badge": "標籤顏色",
"theme_edit_modal__badge_primary": "主顏色",
"theme_edit_modal__badge_secondary": "次要顏色",
"theme_edit_modal__badge_tertiary": "第三顏色",
"theme_edit_modal__bg_image": "背景圖片",
"theme_edit_modal__bg_image_add": "添加背景圖片",
"theme_edit_modal__bg_image_change": "更改背景圖片",
"theme_edit_modal__bg_image_remove": "移除背景圖片",
"theme_edit_modal__close_btn": "關閉",
"theme_edit_modal__control_btn": "左側控制按鈕顏色",
"theme_edit_modal__dark": "暗色主題",
"theme_edit_modal__hide_btn": "隱藏播放詳情頁",
"theme_edit_modal__main_bg": "內容區域背景顏色",
"theme_edit_modal__min_btn": "最小化",
"theme_edit_modal__pick_cancel": "取消",
"theme_edit_modal__pick_color": "選擇顏色",
"theme_edit_modal__pick_last_color": "使用之前的顏色",
"theme_edit_modal__pick_save": "確認",
"theme_edit_modal__preview": "預覽主題",
"theme_edit_modal__primary": "主題色",
"theme_edit_modal__title": "編輯主題",
"theme_green": "綠意盎然",
"theme_grey": "灰常美麗",
"theme_happy_new_year": "新年快樂",
"theme_max_tip": "最多只能添加10個主題哦,刪掉一些再添加吧 😜",
"theme_mid_autumn": "月裡嫦娥",
"theme_ming": "青出於黑",
"theme_naruto": "木葉之村",
@@ -460,9 +545,11 @@
"theme_red": "熱情似火",
"theme_selector_modal__dark_title": "暗色主題",
"theme_selector_modal__light_title": "亮色主題",
"theme_selector_modal__theme_name": "主題名稱",
"theme_selector_modal__title": "跟隨系統主題設置",
"theme_selector_modal__title_tip": "注:你可以預先設置一個亮色主題及暗色主題,此後將根據系統的亮、暗主題色自動切換為你預先設置的相應主題。",
"theme_yellow": "信口雌黃",
"toggle_source_failed": "自動換源失敗,請嘗試手動在其他源搜索該歌曲播放",
"toggle_source_try": "嘗試切換到其他源...",
"user_api__allow_show_update_alert": "允許顯示更新彈窗",
"user_api__btn_export": "導出",
"user_api__btn_import": "導入",
+244
View File
@@ -0,0 +1,244 @@
import { join, dirname } from 'path'
import { existsSync, mkdirSync } from 'fs'
import { app, shell, screen, nativeTheme } from 'electron'
import { URL_SCHEME_RXP } from '@common/constants'
import { getTheme, initHotKey, initSetting, parseEnvParams } from './utils'
import { navigationUrlWhiteList } from '@common/config'
import defaultSetting from '@common/defaultSetting'
import { closeWindow, isExistWindow as isExistMainWindow, showWindow as showMainWindow } from './modules/winMain'
import { createAppEvent, createListEvent } from '@main/event'
import { encodePath, isMac, log } from '@common/utils'
import createWorkers from './worker'
import { migrateDBData } from './utils/migrate'
export const initGlobalData = () => {
global.isDev = process.env.NODE_ENV !== 'production'
const envParams = parseEnvParams()
global.envParams = {
cmdParams: envParams.cmdParams,
deeplink: envParams.deeplink,
}
if (global.isDev) {
// eslint-disable-next-line no-undef
global.staticPath = webpackStaticPath
} else {
global.staticPath = join(__dirname, '/static')
}
}
export const initSingleInstanceHandle = () => {
// 单例应用程序
if (!app.requestSingleInstanceLock()) {
app.quit()
process.exit(0)
}
app.on('second-instance', (event, argv, cwd) => {
for (const param of argv) {
if (URL_SCHEME_RXP.test(param)) {
global.envParams.deeplink = param
break
}
}
if (isExistMainWindow()) {
if (global.envParams.deeplink) global.lx.event_app.deeplink(global.envParams.deeplink)
else showMainWindow()
} else {
app.quit()
}
})
}
export const applyElectronEnvParams = () => {
// Is disable hardware acceleration
if (global.envParams.cmdParams.dha) app.disableHardwareAcceleration()
if (global.envParams.cmdParams.dhmkh) app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling')
// fix linux transparent fail. https://github.com/electron/electron/issues/25153#issuecomment-843688494
if (process.platform == 'linux') app.commandLine.appendSwitch('use-gl', 'desktop')
// https://github.com/electron/electron/issues/22691
app.commandLine.appendSwitch('wm-window-animations-disabled')
// proxy
if (global.envParams.cmdParams['proxy-server']) {
app.commandLine.appendSwitch('proxy-server', global.envParams.cmdParams['proxy-server'])
app.commandLine.appendSwitch('proxy-bypass-list', global.envParams.cmdParams['proxy-bypass-list'] ?? '<local>')
}
}
export const setUserDataPath = () => {
// windows平台下如果应用目录下存在 portable 文件夹则将数据存在此文件下
if (process.platform == 'win32') {
const portablePath = join(dirname(app.getPath('exe')), '/portable')
if (existsSync(portablePath)) {
app.setPath('appData', portablePath)
const appDataPath = join(portablePath, '/userData')
if (!existsSync(appDataPath)) mkdirSync(appDataPath)
app.setPath('userData', appDataPath)
}
}
const userDataPath = app.getPath('userData')
global.lxDataPath = join(encodePath(userDataPath.replaceAll('\\', '/')), 'LxDatas')
global.lxOldDataPath = encodePath(userDataPath.replaceAll('\\', '/'))
if (!existsSync(global.lxDataPath)) mkdirSync(global.lxDataPath)
}
export const registerDeeplink = (startApp: () => void) => {
if (global.isDev && process.platform === 'win32') {
// Set the path of electron.exe and your app.
// These two additional parameters are only available on windows.
// console.log(process.execPath, process.argv)
app.setAsDefaultProtocolClient('lxmusic', process.execPath, process.argv.slice(1))
} else {
app.setAsDefaultProtocolClient('lxmusic')
}
// deep link
app.on('open-url', (event, url) => {
if (!URL_SCHEME_RXP.test(url)) return
event.preventDefault()
global.envParams.deeplink = url
if (isExistMainWindow()) {
if (global.envParams.deeplink) global.lx.event_app.deeplink(global.envParams.deeplink)
else showMainWindow()
} else {
startApp()
}
})
}
export const listenerAppEvent = (startApp: () => void) => {
app.on('web-contents-created', (event, contents) => {
contents.on('will-navigate', (event, navigationUrl) => {
if (global.isDev) return console.log('navigation to url:', navigationUrl)
if (!navigationUrlWhiteList.some(url => url.test(navigationUrl))) return event.preventDefault()
console.log('navigation to url:', navigationUrl)
})
contents.setWindowOpenHandler(({ url }) => {
if (!/^devtools/.test(url) && /^https?:\/\//.test(url)) {
void shell.openExternal(url)
}
console.log(url)
return { action: 'deny' }
})
contents.on('will-attach-webview', (event, webPreferences, params) => {
// Strip away preload scripts if unused or verify their location is legitimate
delete webPreferences.preload
// delete webPreferences.preloadURL
// Disable Node.js integration
webPreferences.nodeIntegration = false
// Verify URL being loaded
if (!navigationUrlWhiteList.some(url => url.test(params.src))) {
event.preventDefault()
}
})
// disable create dictionary
// https://github.com/lyswhut/lx-music-desktop/issues/773
contents.session.setSpellCheckerDictionaryDownloadURL('http://0.0.0.0')
})
app.on('activate', () => {
if (isExistMainWindow()) {
showMainWindow()
} else {
startApp()
}
})
app.on('window-all-closed', () => {
if (isMac) return
app.quit()
})
const initScreenParams = () => {
global.envParams.workAreaSize = screen.getPrimaryDisplay().workAreaSize
}
app.on('ready', () => {
screen.on('display-metrics-changed', initScreenParams)
initScreenParams()
})
nativeTheme.addListener('updated', (event: any) => {
const themeInfo: Electron.NativeTheme = event.sender
global.lx.event_app.system_theme_change(themeInfo.shouldUseDarkColors)
})
}
const initTheme = () => {
global.lx.theme = getTheme()
const themeConfigKeys = ['theme.id', 'theme.lightId', 'theme.darkId']
global.lx.event_app.on('updated_config', (keys) => {
let requireUpdate = false
for (const key of keys) {
if (themeConfigKeys.includes(key)) {
requireUpdate = true
break
}
}
if (requireUpdate) {
global.lx.theme = getTheme()
global.lx.event_app.theme_change()
}
})
global.lx.event_app.on('system_theme_change', () => {
if (global.lx.appSetting['theme.id'] == 'auto') {
global.lx.theme = getTheme()
global.lx.event_app.theme_change()
}
})
}
let isInitialized = false
export const initAppSetting = async() => {
if (!global.lx) {
const config = await initHotKey()
global.lx = {
isTrafficLightClose: false,
isQuitting: false,
event_app: createAppEvent(),
event_list: createListEvent(),
appSetting: defaultSetting,
worker: createWorkers(),
hotKey: {
enable: true,
config: {
local: config.local,
global: config.global,
},
state: new Map(),
},
theme: {
shouldUseDarkColors: false,
theme: {
id: '',
name: '',
isDark: false,
colors: {},
},
},
}
}
if (!isInitialized) {
const dbFileExists = await global.lx.worker.dbService.init(global.lxDataPath)
global.lx.appSetting = (await initSetting()).setting
if (!dbFileExists) await migrateDBData().catch(err => log.error(err))
initTheme()
}
// global.lx.theme = getTheme()
isInitialized = true
}
export const quitApp = () => {
global.lx.isQuitting = true
closeWindow()
}
-23
View File
@@ -1,23 +0,0 @@
const urlSchemeRxp = /^lxmusic:\/\//
const parseEnv = () => {
const params = {}
const rx = /^-\w+/
for (let param of process.argv) {
if (urlSchemeRxp.test(param)) {
global.envParams.deeplink = param
}
if (!rx.test(param)) continue
param = param.substring(1)
let index = param.indexOf('=')
if (index < 0) {
params[param] = true
} else {
params[param.substring(0, index)] = param.substring(index + 1)
}
}
return params
}
global.envParams.cmdParams = parseEnv()
-4
View File
@@ -1,4 +0,0 @@
global.envParams = {}
require('./cmdParams')
require('./screen')
-11
View File
@@ -1,11 +0,0 @@
const { app, screen } = require('electron')
const initScreenParams = () => {
global.envParams.workAreaSize = screen.getPrimaryDisplay().workAreaSize
}
app.on('ready', () => {
screen.on('display-metrics-changed', initScreenParams)
initScreenParams()
})
-34
View File
@@ -1,34 +0,0 @@
const { common: COMMON_EVENT_NAME, mainWindow: MAIN_WINDOW_EVENT_NAME } = require('./events/_name')
const { mainSend, NAMES: { mainWindow: ipcMainWindowNames } } = require('./../common/ipc')
const { getAppHotKeyConfig } = require('./utils')
global.lx_event.common.on(COMMON_EVENT_NAME.configStatus, name => {
if (MAIN_WINDOW_EVENT_NAME.name === name) return
if (global.modules.mainWindow) mainSend(global.modules.mainWindow, ipcMainWindowNames.set_config, global.appSetting)
})
global.lx_event.common.on(COMMON_EVENT_NAME.initHotKey, () => {
global.appHotKey.config = getAppHotKeyConfig()
})
global.lx_event.mainWindow.on(MAIN_WINDOW_EVENT_NAME.quit, () => {
global.isTrafficLightClose = false
global.isQuitting = true
global.modules.mainWindow.close()
})
global.lx_event.mainWindow.on(MAIN_WINDOW_EVENT_NAME.toggle_minimize, () => {
if (global.modules.mainWindow.isMinimized()) {
if (!global.modules.mainWindow.isVisible()) {
global.modules.mainWindow.show()
}
global.modules.mainWindow.restore()
global.modules.mainWindow.focus()
} else {
global.modules.mainWindow.minimize()
}
})
global.lx_event.mainWindow.on(MAIN_WINDOW_EVENT_NAME.toggle_hide, () => {
global.modules.mainWindow.isVisible()
? global.modules.mainWindow.hide()
: global.modules.mainWindow.show()
})
+101
View File
@@ -0,0 +1,101 @@
import { EventEmitter } from 'events'
import { saveAppHotKeyConfig, updateSetting } from '@main/utils'
export class Event extends EventEmitter {
// closeAll() {
// this.emit(COMMON_EVENT_NAME.closeAll)
// }
// initSetting() {
// this.emit(COMMON_EVENT_NAME.initConfig)
// // this.configStatus(null)
// }
/**
* 初始化APP
*/
app_inited() {
this.emit('app_inited')
}
/**
* 已更新的配置
* @param keys 已更新配置的key
* @param setting 已更新配置
*/
updated_config(keys: Array<keyof LX.AppSetting>, setting: Partial<LX.AppSetting>) {
this.emit('updated_config', keys, setting)
}
/**
* 更新配置
* @param setting 新设置
*/
update_config(setting: Partial<LX.AppSetting>) {
const { setting: newSetting, updatedSettingKeys, updatedSetting } = updateSetting(setting)
global.lx.appSetting = newSetting
if (!updatedSettingKeys.length) return
this.emit('update_config', newSetting)
// console.log(updatedSetting)
this.updated_config(updatedSettingKeys, updatedSetting)
}
system_theme_change(isDark: boolean) {
this.emit('system_theme_change', isDark)
}
theme_change() {
this.emit('theme_change')
}
deeplink(link: string) {
this.emit('deeplink', link)
}
hot_key_down(keyInfo: LX.HotKeyDownInfo) {
this.emit('hot_key_down', keyInfo)
}
hot_key_config_update(config: LX.HotKeyConfigAll) {
saveAppHotKeyConfig(config)
this.emit('hot_key_config_update', config)
}
main_window_ready_to_show() {
this.emit('main_window_ready_to_show')
}
main_window_inited() {
this.emit('main_window_inited')
}
main_window_show() {
this.emit('main_window_show')
}
main_window_hide() {
this.emit('main_window_hide')
}
main_window_focus() {
this.emit('main_window_focus')
}
main_window_blur() {
this.emit('main_window_blur')
}
main_window_close() {
this.emit('main_window_close')
}
}
type EventMethods = Omit<EventType, keyof EventEmitter>
declare class EventType extends Event {
on<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this
once<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this
off<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this
}
export declare type Type = Omit<EventType, keyof Omit<EventEmitter, 'on' | 'off' | 'once'>>
+174
View File
@@ -0,0 +1,174 @@
import { EventEmitter } from 'events'
// import {
// // getAllUserList as getAllUserListByDB,
// createUserLists,
// removeUserLists,
// updateUserLists,
// updateUserListsPosition,
// musicsAdd,
// musicsMove,
// musicsRemove,
// musicsUpdate,
// musicsClear,
// musicsPositionUpdate,
// musicOverwrite,
// } from '@main/workers/dbService/modules/list'
export class Event extends EventEmitter {
list_changed() {
this.emit('list_changed')
}
/**
* 覆盖整个列表数据
* @param listData 列表数据
* @param isRemote 是否属于远程操作
*/
async list_data_overwrite(listData: MakeOptional<LX.List.ListDataFull, 'tempList'>, isRemote: boolean = false) {
await global.lx.worker.dbService.listDataOverwrite(listData)
this.emit('list_data_overwrite', listData, isRemote)
this.list_changed()
}
/**
* 批量创建列表
* @param position 列表位置
* @param lists 列表信息
* @param isRemote 是否属于远程操作
*/
async list_create(position: number, lists: LX.List.UserListInfo[], isRemote: boolean = false) {
await global.lx.worker.dbService.createUserLists(position, lists)
this.emit('list_create', position, lists, isRemote)
this.list_changed()
}
/**
* 批量删除列表及列表内歌曲
* @param ids 列表ids
* @param isRemote 是否属于远程操作
*/
async list_remove(ids: string[], isRemote: boolean = false) {
await global.lx.worker.dbService.removeUserLists(ids)
this.emit('list_remove', ids, isRemote)
this.list_changed()
}
/**
* 批量更新列表信息
* @param lists 列表信息
* @param isRemote 是否属于远程操作
*/
async list_update(lists: LX.List.UserListInfo[], isRemote: boolean = false) {
await global.lx.worker.dbService.updateUserLists(lists)
this.emit('list_update', lists, isRemote)
this.list_changed()
}
/**
* 批量更新列表位置
* @param position 列表位置
* @param ids 列表ids
* @param isRemote 是否属于远程操作
*/
async list_update_position(position: number, ids: string[], isRemote: boolean = false) {
await global.lx.worker.dbService.updateUserListsPosition(position, ids)
this.emit('list_update_position', position, ids, isRemote)
this.list_changed()
}
/**
* 覆盖列表内歌曲
* @param listId 列表id
* @param musicInfos 音乐信息
* @param isRemote 是否属于远程操作
*/
async list_music_overwrite(listId: string, musicInfos: LX.Music.MusicInfo[], isRemote: boolean = false) {
await global.lx.worker.dbService.musicOverwrite(listId, musicInfos)
this.emit('list_music_overwrite', listId, musicInfos, isRemote)
this.list_changed()
}
/**
* 批量添加歌曲到列表
* @param listId 列表id
* @param musicInfos 添加的歌曲信息
* @param addMusicLocationType 添加在到列表的位置
* @param isRemote 是否属于远程操作
*/
async list_music_add(listId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType, isRemote: boolean = false) {
await global.lx.worker.dbService.musicsAdd(listId, musicInfos, addMusicLocationType)
this.emit('list_music_add', listId, musicInfos, addMusicLocationType, isRemote)
this.list_changed()
}
/**
* 批量移动歌曲
* @param fromId 源列表id
* @param toId 目标列表id
* @param musicInfos 移动的歌曲信息
* @param addMusicLocationType 添加在到列表的位置
* @param isRemote 是否属于远程操作
*/
async list_music_move(fromId: string, toId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType, isRemote: boolean = false) {
await global.lx.worker.dbService.musicsMove(fromId, toId, musicInfos, addMusicLocationType)
this.emit('list_music_move', fromId, toId, musicInfos, addMusicLocationType, isRemote)
this.list_changed()
}
/**
* 批量移除歌曲
* @param listId
* @param listId 列表Id
* @param ids 要删除歌曲的id
* @param isRemote 是否属于远程操作
*/
async list_music_remove(listId: string, ids: string[], isRemote: boolean = false) {
await global.lx.worker.dbService.musicsRemove(listId, ids)
this.emit('list_music_remove', listId, ids, isRemote)
this.list_changed()
}
/**
* 批量更新歌曲信息
* @param musicInfos 歌曲&列表信息
* @param isRemote 是否属于远程操作
*/
async list_music_update(musicInfos: LX.List.ListActionMusicUpdate, isRemote: boolean = false) {
await global.lx.worker.dbService.musicsUpdate(musicInfos)
this.emit('list_music_update', musicInfos, isRemote)
this.list_changed()
}
/**
* 清空列表内的歌曲
* @param listId 列表Id
* @param isRemote 是否属于远程操作
*/
async list_music_clear(listId: string, isRemote: boolean = false) {
await global.lx.worker.dbService.musicsClear(listId)
this.emit('list_music_clear', listId, isRemote)
this.list_changed()
}
/**
* 批量更新歌曲位置
* @param listId 列表ID
* @param position 新位置
* @param ids 歌曲id
* @param isRemote 是否属于远程操作
*/
async list_music_update_position(listId: string, position: number, ids: string[], isRemote: boolean = false) {
await global.lx.worker.dbService.musicsPositionUpdate(listId, position, ids)
this.emit('list_music_update_position', listId, position, ids, isRemote)
this.list_changed()
}
}
type EventMethods = Omit<EventType, keyof EventEmitter>
declare class EventType extends Event {
on<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this
once<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this
off<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this
}
export declare type Type = Omit<EventType, keyof Omit<EventEmitter, 'on' | 'off' | 'once'>>
+16
View File
@@ -0,0 +1,16 @@
import { Event as App, Type as AppType } from './AppEvent'
import { Event as List, Type as ListType } from './ListEvent'
export {
AppType,
ListType,
}
export const createAppEvent = (): AppType => {
return new App()
}
export const createListEvent = (): ListType => {
return new List()
}
-20
View File
@@ -1,20 +0,0 @@
const { EventEmitter } = require('events')
const { common: COMMON_EVENT_NAME } = require('./_name')
class Common extends EventEmitter {
initSetting() {
this.emit(COMMON_EVENT_NAME.initConfig)
this.configStatus(null)
}
configStatus(name) {
this.emit(COMMON_EVENT_NAME.configStatus, name)
}
saveMyList(data) {
this.emit(COMMON_EVENT_NAME.saveMyList, data)
}
}
module.exports = Common
-21
View File
@@ -1,21 +0,0 @@
const { EventEmitter } = require('events')
const { hotKey: HOT_KEY_EVENT_NAME } = require('./_name')
const { saveAppHotKeyConfig } = require('../utils')
class HotKey extends EventEmitter {
init() {
this.emit(HOT_KEY_EVENT_NAME.init)
}
saveConfig(config, source) {
if (config) saveAppHotKeyConfig(config)
this.emit(HOT_KEY_EVENT_NAME.config, config, source)
}
keyDown(info) {
this.emit(HOT_KEY_EVENT_NAME.keyDown, info)
}
}
module.exports = HotKey
-42
View File
@@ -1,42 +0,0 @@
const { EventEmitter } = require('events')
const { mainWindow: MAIN_WINDOW_EVENT_NAME } = require('./_name')
class MainWindow extends EventEmitter {
setLyricInfo(info) {
this.emit(MAIN_WINDOW_EVENT_NAME.setLyricInfo, info)
}
quit() {
this.emit(MAIN_WINDOW_EVENT_NAME.quit)
}
toggleMinimize() {
this.emit(MAIN_WINDOW_EVENT_NAME.toggle_minimize)
}
toggleHide() {
this.emit(MAIN_WINDOW_EVENT_NAME.toggle_hide)
}
readyToShow() {
this.emit(MAIN_WINDOW_EVENT_NAME.ready_to_show)
}
show() {
this.emit(MAIN_WINDOW_EVENT_NAME.show)
}
focus() {
this.emit(MAIN_WINDOW_EVENT_NAME.focus)
}
blur() {
this.emit(MAIN_WINDOW_EVENT_NAME.blur)
}
hide() {
this.emit(MAIN_WINDOW_EVENT_NAME.hide)
}
}
module.exports = MainWindow
-14
View File
@@ -1,14 +0,0 @@
const { EventEmitter } = require('events')
const { tray: TRAY_EVENT_NAME } = require('./_name')
class Tray extends EventEmitter {
create() {
this.emit(TRAY_EVENT_NAME.create)
}
destroy() {
this.emit(TRAY_EVENT_NAME.destroy)
}
}
module.exports = Tray
-15
View File
@@ -1,15 +0,0 @@
const { EventEmitter } = require('events')
const { winLyric: WIN_LYRIC_EVENT_NAME } = require('./_name')
// const { updateSetting } = require('../utils')
class WinLyric extends EventEmitter {
create() {
this.emit(WIN_LYRIC_EVENT_NAME.create)
}
close() {
this.emit(WIN_LYRIC_EVENT_NAME.close)
}
}
module.exports = WinLyric
-38
View File
@@ -1,38 +0,0 @@
exports.common = {
initConfig: 'initConfig',
configStatus: 'config',
saveMyList: 'saveMyList',
}
exports.mainWindow = {
name: 'mainWindow',
setLyricInfo: 'setLyricInfo',
destroy: 'destroy',
quit: 'quit',
toggle_minimize: 'toggle_minimize',
toggle_hide: 'toggle_hide',
ready_to_show: 'ready_to_show',
show: 'show',
hide: 'hide',
focus: 'focus',
blur: 'blur',
}
exports.tray = {
name: 'tray',
create: 'create',
destroy: 'destroy',
}
exports.winLyric = {
name: 'winLyric',
create: 'create',
close: 'close',
}
exports.hotKey = {
name: 'hotKey',
init: 'init',
config: 'config',
keyDown: 'keyDown',
}
-21
View File
@@ -1,21 +0,0 @@
if (!global.lx_event) global.lx_event = {}
const Common = require('./Common')
const MainWindow = require('./MainWindow')
const Tray = require('./Tray')
const WinLyric = require('./WinLyric')
const HotKey = require('./HotKey')
const { Event: TaskBar } = require('../modules/taskbar')
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()
if (!global.lx_event.tray) global.lx_event.tray = new Tray()
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.taskbar) global.lx_event.taskbar = new TaskBar()
if (!global.lx_event.userApi) global.lx_event.userApi = new UserApi()
if (!global.lx_event.sync) global.lx_event.sync = new Sync()
@@ -4,9 +4,10 @@
* modify this file, but it can be used to extend your development
* environment.
*/
const electron = require('electron')
const electronDebug = require('electron-debug')
const { default: installExtension, VUEJS_DEVTOOLS } = require('electron-devtools-installer')
import { app } from 'electron'
import * as electronDebug from 'electron-debug'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
// Install `electron-debug` with `devtron`
electronDebug({
showDevTools: true,
@@ -14,10 +15,10 @@ electronDebug({
})
// Install `vue-devtools`
electron.app.on('ready', () => {
app.on('ready', () => {
installExtension(VUEJS_DEVTOOLS)
.then(name => console.log(`Added Extension: ${name}`))
.catch(err => console.log('An error occurred: ', err))
.then((name: string) => console.log(`Added Extension: ${name}`))
.catch((err: Error) => console.log('An error occurred: ', err))
})
// Require `main` process to boot app
-250
View File
@@ -1,250 +0,0 @@
const { app, BrowserWindow, shell, nativeTheme } = require('electron')
const path = require('path')
const urlSchemeRxp = /^lxmusic:\/\//
// 单例应用程序
if (!app.requestSingleInstanceLock()) {
app.quit()
process.exit(0)
}
if (!global.modules) global.modules = {}
app.on('second-instance', (event, argv, cwd) => {
for (const param of argv) {
if (urlSchemeRxp.test(param)) {
global.envParams.deeplink = param
break
}
}
if (global.modules.mainWindow) {
if (global.modules.mainWindow.isMinimized()) {
global.modules.mainWindow.restore()
}
if (global.modules.mainWindow.isVisible()) {
global.modules.mainWindow.focus()
} else {
global.modules.mainWindow.show()
}
} else {
app.quit()
}
})
// windows平台下如果应用目录下存在 portable 文件夹则将数据存在此文件下
if (process.platform === 'win32') {
const fs = require('fs')
const portablePath = path.join(path.dirname(app.getPath('exe')), '/portable')
if (fs.existsSync(portablePath)) {
app.setPath('appData', portablePath)
const appDataPath = path.join(portablePath, '/userData')
if (!fs.existsSync(appDataPath)) fs.mkdirSync(appDataPath)
app.setPath('userData', appDataPath)
}
}
const isDev = global.isDev = process.env.NODE_ENV !== 'production'
require('./env')
// console.log(global.envParams.cmdParams)
// Is disable hardware acceleration
if (global.envParams.cmdParams.dha) app.disableHardwareAcceleration()
if (global.envParams.cmdParams.dt == null && global.envParams.cmdParams.nt != null) global.envParams.cmdParams.dt = global.envParams.cmdParams.nt
if (global.envParams.cmdParams.dhmkh) app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling')
// fix linux transparent fail. https://github.com/electron/electron/issues/25153#issuecomment-843688494
if (process.platform == 'linux') app.commandLine.appendSwitch('use-gl', 'desktop')
// https://github.com/electron/electron/issues/22691
app.commandLine.appendSwitch('wm-window-animations-disabled')
// proxy
if (global.envParams.cmdParams['proxy-server']) {
app.commandLine.appendSwitch('proxy-server', global.envParams.cmdParams['proxy-server'])
app.commandLine.appendSwitch('proxy-bypass-list', global.envParams.cmdParams['proxy-bypass-list'] ?? '<local>')
}
// if (global.envParams.cmdParams['proxy-pac-url']) app.commandLine.appendSwitch('proxy-pac-url', global.envParams.cmdParams['proxy-pac-url'])
// deep link
app.on('open-url', (event, url) => {
if (!urlSchemeRxp.test(url)) return
event.preventDefault()
global.envParams.deeplink = url
if (global.modules.mainWindow) {
if (global.modules.mainWindow.isMinimized()) {
global.modules.mainWindow.restore()
}
if (global.modules.mainWindow.isVisible()) {
global.modules.mainWindow.focus()
} else {
global.modules.mainWindow.show()
}
} else if (global.modules.mainWindow === null) {
init()
}
})
// if (isDev && process.platform === 'win32') {
// // Set the path of electron.exe and your app.
// // These two additional parameters are only available on windows.
// // console.log(process.execPath, process.argv)
// app.setAsDefaultProtocolClient('lxmusic', process.execPath, process.argv.slice(1))
// }
if (!isDev) app.setAsDefaultProtocolClient('lxmusic')
const { navigationUrlWhiteList, themes } = require('../common/config')
const { getWindowSizeInfo, initSetting, updateSetting } = require('./utils')
const { isMac, isLinux, initHotKey } = require('../common/utils')
// https://github.com/electron/electron/issues/18397
// 开发模式下为true时 多次引入native模块会导致渲染进程卡死
// https://github.com/electron/electron/issues/22791
// app.allowRendererProcessReuse = !isDev
app.on('web-contents-created', (event, contents) => {
contents.on('will-navigate', (event, navigationUrl) => {
if (isDev) return console.log('navigation to url:', navigationUrl)
if (!navigationUrlWhiteList.some(url => url.test(navigationUrl))) return event.preventDefault()
console.log('navigation to url:', navigationUrl)
})
contents.setWindowOpenHandler(({ url }) => {
if (!/^devtools/.test(url) && /^https?:\/\//.test(url)) {
shell.openExternal(url)
}
console.log(url)
return { action: 'deny' }
})
contents.on('will-attach-webview', (event, webPreferences, params) => {
// Strip away preload scripts if unused or verify their location is legitimate
delete webPreferences.preload
delete webPreferences.preloadURL
// Disable Node.js integration
webPreferences.nodeIntegration = false
// Verify URL being loaded
if (!navigationUrlWhiteList.some(url => url.test(params.src))) {
event.preventDefault()
}
})
// disable create dictionary
// https://github.com/lyswhut/lx-music-desktop/issues/773
contents.session.setSpellCheckerDictionaryDownloadURL('http://0.0.0.0')
})
require('../common/error')
require('./events')
require('./event')
require('./rendererEvents')
const winEvent = require('./rendererEvents/winEvent')
const autoUpdate = require('./utils/autoUpdate')
let winURL
if (isDev) {
// eslint-disable-next-line no-undef
global.__static = __static
winURL = 'http://localhost:9080'
} else {
global.__static = path.join(__dirname, '/static')
winURL = `file://${path.join(__dirname, 'index.html')}`
}
function createWindow() {
const windowSizeInfo = getWindowSizeInfo(global.appSetting)
/**
* Initial window options
*/
const options = {
height: windowSizeInfo.height,
useContentSize: true,
width: windowSizeInfo.width,
frame: false,
transparent: !global.envParams.cmdParams.dt,
enableRemoteModule: false,
// icon: path.join(global.__static, isWin ? 'icons/256x256.ico' : 'icons/512x512.png'),
resizable: false,
maximizable: false,
fullscreenable: true,
show: false,
webPreferences: {
contextIsolation: false,
webSecurity: !isDev,
nodeIntegration: true,
spellcheck: false, // 禁用拼写检查器
},
}
if (global.appSetting.startInFullscreen) {
options.fullscreen = true
if (isLinux) options.resizable = true
}
global.modules.mainWindow = new BrowserWindow(options)
const shouldUseDarkColors = nativeTheme.shouldUseDarkColors
const themeId = global.appSetting.theme.id == 'auto'
? shouldUseDarkColors
? global.appSetting.theme.darkId
: global.appSetting.theme.lightId
: global.appSetting.theme.id
const themeClass = themes.find(t => t.id == themeId)?.className ?? themes[0].className
global.modules.mainWindow.loadURL(winURL + `?dt=${!!global.envParams.cmdParams.dt}&dark=${shouldUseDarkColors}&theme=${themeClass}`)
winEvent(global.modules.mainWindow)
if (global.envParams.cmdParams.odt) require('@main/utils').openDevTools(global.modules.mainWindow.webContents)
// global.modules.mainWindow.webContents.openDevTools()
if (!isDev) autoUpdate()
}
global.appHotKey = {
enable: true,
config: {},
state: null,
}
global.lx_core = {
setAppConfig(setting, name) {
updateSetting(setting)
global.lx_event.common.configStatus(name)
},
}
function init() {
console.log('init')
initSetting()
global.appHotKey.config = initHotKey()
global.lx_event.common.initSetting()
global.lx_event.hotKey.init()
createWindow()
}
// https://github.com/electron/electron/issues/16809
app.on('ready', isLinux ? () => setTimeout(init, 300) : init)
app.on('activate', () => {
if (global.modules.mainWindow) {
if (global.modules.mainWindow.isMinimized()) {
global.modules.mainWindow.restore()
} else if (global.modules.mainWindow.isVisible()) {
global.modules.mainWindow.focus()
} else {
global.modules.mainWindow.show()
}
} else if (global.modules.mainWindow === null) {
init()
}
})
app.on('window-all-closed', () => {
if (isMac) {
global.lx_event.tray.destroy()
} else {
app.quit()
}
})
require('./modules')
+33
View File
@@ -0,0 +1,33 @@
import { app } from 'electron'
import '@common/error'
import {
initGlobalData,
initSingleInstanceHandle,
applyElectronEnvParams,
setUserDataPath,
registerDeeplink,
listenerAppEvent,
} from './app'
import { isLinux } from '@common/utils'
import { initAppSetting } from '@main/app'
import registerModules from '@main/modules'
// 初始化应用
const init = () => {
console.log('init')
void initAppSetting().then(() => {
registerModules()
global.lx.event_app.app_inited()
})
}
initGlobalData()
initSingleInstanceHandle()
applyElectronEnvParams()
setUserDataPath()
registerDeeplink(init)
listenerAppEvent(init)
// https://github.com/electron/electron/issues/16809
void app.whenReady().then(() => isLinux ? setTimeout(init, 300) : init())
-48
View File
@@ -1,48 +0,0 @@
const { app, Menu } = require('electron')
const { isMac } = require('../../common/utils')
if (isMac) {
const template = [
{
label: app.getName(),
submenu: [
{ label: '关于洛雪音乐', role: 'about' },
{ type: 'separator' },
{ label: '隐藏', role: 'hide' },
{ type: 'separator' },
{
label: '退出',
accelerator: 'Command+Q',
click() {
global.isQuitting = true
app.quit()
},
},
],
},
{
label: '窗口',
role: 'window',
submenu: [
{ label: '最小化', role: 'minimize', accelerator: 'Command+W' },
{ label: '关闭', role: 'close' },
],
},
{
label: '编辑',
submenu: [
{ label: '撤销', accelerator: 'Command+Z', role: 'undo' },
{ label: '恢复', accelerator: 'Shift+Command+Z', role: 'redo' },
{ type: 'separator' },
{ label: '剪切', accelerator: 'Command+X', role: 'cut' },
{ label: '复制', accelerator: 'Command+C', role: 'copy' },
{ label: '粘贴', accelerator: 'Command+V', role: 'paste' },
{ label: '选择全部', accelerator: 'Command+A', role: 'selectAll' },
],
},
]
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
} else {
Menu.setApplicationMenu(null)
}
+51
View File
@@ -0,0 +1,51 @@
import { app, Menu } from 'electron'
import { isMac } from '@common/utils'
import { quitApp } from '@main/app'
export default () => {
if (isMac) {
const template: Electron.MenuItemConstructorOptions[] = [
{
label: app.getName(),
submenu: [
{ label: '关于洛雪音乐', role: 'about' },
{ type: 'separator' },
{ label: '隐藏', role: 'hide' },
{ type: 'separator' },
{
label: '退出',
accelerator: 'Command+Q',
click() {
quitApp()
},
},
],
},
{
label: '窗口',
role: 'window',
submenu: [
{ label: '最小化', role: 'minimize', accelerator: 'Command+W' },
{ label: '关闭', role: 'close' },
],
},
{
label: '编辑',
submenu: [
{ label: '撤销', accelerator: 'Command+Z', role: 'undo' },
{ label: '恢复', accelerator: 'Shift+Command+Z', role: 'redo' },
{ type: 'separator' },
{ label: '剪切', accelerator: 'Command+X', role: 'cut' },
{ label: '复制', accelerator: 'Command+C', role: 'copy' },
{ label: '粘贴', accelerator: 'Command+V', role: 'paste' },
{ label: '选择全部', accelerator: 'Command+A', role: 'selectAll' },
],
},
]
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
} else {
Menu.setApplicationMenu(null)
}
}
@@ -0,0 +1,3 @@
export { registerRendererEvents } from './winRendererEvent'
export { default } from './rendererEvent'
@@ -0,0 +1,26 @@
import { mainHandle, mainOn } from '@common/mainIpc'
import { CMMON_EVENT_NAME } from '@common/ipcNames'
import { getFonts } from '@main/utils/fontManage'
// 公共操作事件(公共,只注册一次)
export default () => {
mainHandle<LX.AppSetting>(CMMON_EVENT_NAME.get_app_setting, async() => {
return global.lx.appSetting
})
mainHandle<Partial<LX.AppSetting>>(CMMON_EVENT_NAME.set_app_setting, async({ params: config }) => {
global.lx.event_app.update_config(config)
})
mainHandle<LX.EnvParams>(CMMON_EVENT_NAME.get_env_params, async() => {
return global.envParams
})
mainOn(CMMON_EVENT_NAME.clear_env_params_deeplink, () => {
global.envParams.deeplink = null
})
mainHandle<string[]>(CMMON_EVENT_NAME.get_system_fonts, async() => {
return await getFonts()
})
}

Some files were not shown because too many files have changed in this diff Show More